memory-eternal 0.10.37 → 0.10.39
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 +5 -3
- package/README.md +5 -3
- package/index.js +14 -1
- package/lib/align.js +22 -0
- package/lib/client.js +1 -1
- package/lib/setup.js +73 -11
- package/lib/watchdog.js +3 -0
- package/package.json +2 -2
- package/web/app.js +1 -1
|
@@ -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.10.
|
|
4
|
+
"version": "0.10.39",
|
|
5
5
|
"description": "记忆核心(Memory Eternal):对话结束后自动沉淀知识卡到本地记忆库(~/.dsh/memory-vault),支持检索 / 召回 / 审核中心。",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "EternalNight996",
|
package/README.en.md
CHANGED
|
@@ -361,14 +361,16 @@ curl http://127.0.0.1:7999/memory-eternal/api/web-info # is the standalone we
|
|
|
361
361
|
|
|
362
362
|
| Version | Date | Highlights |
|
|
363
363
|
|---|---|---|
|
|
364
|
-
| **v0.10.
|
|
365
|
-
| **v0.10.
|
|
364
|
+
| **v0.10.39** | 2026-10-17 | **Wiring self-healing: codex TOML parsed per section, single quotes and escaping supported, plus a read-only startup check (items ②/④ of #44)**. ① Fixed a real bug: setup / align parsed ~/.codex/config.toml **without scoping to [mcp_servers.memory]**, so they picked up the first command = of another server (on this machine it read MCP_DOCKER docker.exe), and only double quotes were understood while codex itself writes single quotes — so the entry was **neither readable nor fixable, yet setup still printed "updated"**. Reading and writing now share pure helpers (section scoping, both quote styles, unescaping), the writer verifies the text really changed, is idempotent (already-aligned entries are not counted as changes) and honestly reports section unparseable when it cannot edit. ② A new read-only startup check staleWiringCheck() logs a warn in the auto-capture log when MCP/hooks reference a **non-existent** node path or an entry that differs from the current version (dsh-memory setup fixes it) — nobody runs align daily, so silent failures must become visible. ③ Verified locally: all three agent wirings are now ✓ in align (codex previously kept reporting "node path differs"). npm test now runs **379 checks** |
|
|
365
|
+
| **v0.10.38** | 2026-10-17 | **The claim is released once a takeover succeeds (fixing a 0.10.35 side effect), plus real-machine A/B numbers for #45**. In 0.10.35 the claim was never released on the path where the helper becomes the resident watchdog (that process never exits), so a restart within 60 seconds of a completed takeover was blocked as in-flight by itself (reproduced locally). bootWatchdog() is now immediately followed by releaseReplaceClaim(), making the mutual-exclusion window exactly "a takeover is in progress". **#45 verified on a real machine (same host, same batch of 20 /version-check calls)**: 0.10.36 gave 9 successes / **11 refusals**, TIME_WAIT 24 to **1976** (+1952, about 100 connections per request, i.e. the recursion depth) and a 2389 ms first frame; 0.10.37 gave **20/20 successes**, TIME_WAIT 1969 to **1971** (+2) and 18-34 ms. npm test now runs **376 checks** |
|
|
366
366
|
|
|
367
367
|
<details>
|
|
368
|
-
<summary>Older release notes (v0.10.
|
|
368
|
+
<summary>Older release notes (v0.10.37 and earlier · 64 releases, click to expand)</summary>
|
|
369
369
|
|
|
370
370
|
| Version | Date | Highlights |
|
|
371
371
|
|---|---|---|
|
|
372
|
+
| **v0.10.37** | 2026-10-17 | **Fixes #45 (a severe regression I introduced in 0.10.36: the self-probing recursion in /version-check, amplified by retries), #44 (hardcoded node paths / zombie-watchdog misjudgement) and #43 (the official route fields for follow-session)**. ① **#45**: selfPort() is this process own port, yet /version-check called probeServedVersion to **request /version-check from it** — handler to itself to handler, with depth bounded only by timeouts and a new connection per level, which saturated the listen backlog and produced **intermittent ECONNREFUSED (17–37ms)**, 4402 extra TIME_WAIT entries in 40 idle seconds and an empty served value; the 0.10.36 retry multiplied it by three. Self-probes now **answer locally** (probedSelf:true, no HTTP), /restart-self has the same gate, and /version-check accepts ?port= for other ports. ② **#44**: nodeBinary() now **verifies existence** before using a cached path and falls back to PATH when execPath vanished (brew upgrade removing the old directory), so long-lived watchdogs heal themselves; inspectResident gains portServing and decideResidentAction treats "the lock says a watchdog is alive but the port answers nobody" as restart (watchdog-port-dead) instead of delegate; status prints the actual node binary and whether it exists. ③ **#43**: sessionRouteFrom() is async and prefers the official fields, session.requestHeader().config then agent.options, with the old sniffing as a fallback. npm test now runs **375 checks** |
|
|
373
|
+
| **v0.10.36** | 2026-10-16 | **Both new states now surface at every entry point (align / doctor), plus one test-hygiene fix**. ① align gains a finding: **the resident instance is host-managed** (the lock is unwritable, so the watchdog path is bypassed and the host stops/starts the instance itself and keeps it alive) — **warn level**, stating explicitly that "the instance is managed, it is not an orphan, and it returns to the watchdog once the lock becomes writable" (align.ok is unaffected; warn is not a failure); ② dsh-memory doctor prints a new line **resident instance management:** (watchdog / host-managed / cannot replace (environment limit)), so together with journal mode, write probe, lock writability and the backup list you can see at a glance which state a machine is in; ③ **test hygiene**: the /restart-self alreadyCurrent case depended on the **developer machine real lock file** (my local lock held only an endedAt record, so it stopped reporting alreadyCurrent and the check went red) — reading the lock is now injectable (deps.readWatchdogLock) and the case injects a live registration, making it independent of the dev machine. npm test now runs **371 checks** |
|
|
372
374
|
| **v0.10.35** | 2026-10-16 | **Single-flight for takeover helpers (issue #30, sixth retest round: the host tick and the CLI each spawn a --replace and step on each other)**. The reporter observed that during a failed restart **two takeover helpers were alive on the same port without knowing each other** (parent = the DSH host / parent = the CLI): neither wrote the lock and each reaped the other web, making the scene even harder to diagnose. Fix: a new **claim file** (memory-eternal-replace.claim, next to the watchdog lock) — ① **a helper claims it exclusively before starting**; if it cannot, it **exits immediately** (yielding to whoever is already running) and releases it on exit; ② **when restartResident sees a takeover in flight** (holder alive and less than 60s old) it **does not spawn a second helper**, returning stage in-flight with who is running it and how long ago it started; ③ **fail-open**: in restricted environments where the claim cannot be written the takeover still proceeds (permission problems must not block takeover for good), but the reason is surfaced in the result and the log. Tests cover: first-come-first-served / a live holder makes the second yield / stale and dead-pid claims can be taken over / only your own claim is released / the in-flight branch of restartResident. npm test now runs **370 checks** |
|
|
373
375
|
| **v0.10.34** | 2026-10-16 | **Root fix: when the lock is unwritable the HOST manages the resident instance itself (adopting the reporter's fourth point in issue #30)**. Their diagnosis: on that machine the "registration" happens to live in the only process that cannot write the lock, so automatic replacement is permanently refused. Fixes: ① the host's `autoWebMode=interval` keep-alive loop now probes **the version served on the port**, not just liveness — if it lags and the lock is writable the watchdog/drift path still handles it (the host does not compete); if it lags and the **lock is unwritable** the **host takes over**: `ensureWebServer({forceTakeover:true})` stops only **our own** web, starts a new instance on the **same port** and keeps it alive; ② a testable `takeoverOwnWeb()` with a safety gate that only acts on occupants where `looksLikeOurWeb` is true (foreign processes are never touched); ③ "host-managed" is recorded as state (`recordHostManaged`) so `/version-check` reports `resident.managedBy='host'` and the panel says "**host-managed**" instead of "unmanaged / orphan"; the state clears itself once a watchdog registers again. `npm test` now runs **366 checks** |
|
|
374
376
|
| **v0.10.33** | 2026-10-15 | **Distillation provider: dynamic dropdowns, best-effort follow-session, first-candidate-only and a test button (first slice of issue #42)**. ① **Dynamic dropdowns**: a new read-only `/llm-routes` endpoint (backed by the host's listProviders/listModels) turns the "distill provider / model" fields from hand-typed IDs into selects (falling back to text inputs when unavailable); ② **Follow the current session** (their top priority): the new `captureFollowSession` config makes host distillation **try** the session's provider/model first (`sessionRouteFrom()` sniffs `agent / session / session.route / session.options`), and **falls back to the explicit config with a log line** when nothing is exposed (DSH does not guarantee a route on the session object — that log is the basis for a future adapter rather than a guess); background/no-session tasks (CLI, watchdog, hooks) can only use the explicit config; ③ **First-candidate-only + test button**: `captureFirstOnly` makes routing take just the first candidate and stop on failure (no more "hit the official provider first, burn a call, then rotate"); `POST /llm-test` makes one small real call and echoes **provider / model / latency / candidate count / error code**, which also verifies ① and ②; ④ also fixes **probe jitter**: `probeServedVersion()` now retries with backoff three times (the reporter measured "version-check 200 at one moment, then status says no service 2s later"), so single-shot probes are no longer fooled by restart windows or transient refusals. `npm test` now runs **363 checks** |
|
package/README.md
CHANGED
|
@@ -393,14 +393,16 @@ curl http://127.0.0.1:7999/memory-eternal/api/web-info # 独立 web 是否活
|
|
|
393
393
|
|
|
394
394
|
| 版本 | 日期 | 关键改动 |
|
|
395
395
|
|---|---|---|
|
|
396
|
-
| **v0.10.
|
|
397
|
-
| **v0.10.
|
|
396
|
+
| **v0.10.39** | 2026-10-17 | **接线自愈:codex 的 TOML 按段解析 + 单引号/转义兼容 + 启动期只读体检(#44 第②④条)**。① 修了一个真 bug:setup / align 解析 ~/.codex/config.toml 时**没有限定 [mcp_servers.memory] 段**,会抓到别的 server 的第一个 command =(本机实测读成了 MCP_DOCKER 的 docker.exe);而且只认双引号,而 codex 自己写出来是单引号 → **既读不到也改不掉,却照样打印「已更新」**。现在读写共用纯函数(按段 → 单/双引号 → 反转义),**改完校验真的变了**,幂等(已是目标形态不算改动),改不了就如实报 section unparseable。② 新增启动期**只读**体检 staleWiringCheck():MCP/hooks 里若写着**已不存在**的 node 路径、或条目与当前版本不一致,就在自动沉淀日志里留一条 warn(dsh-memory setup 可修)—— 没人会天天跑 align,静默失效要变成可见。③ 本机验证:align 三端接线全部 ✓(修前 codex 一直报「node 路径与当前不一致」)。npm test 共 **379 项** |
|
|
397
|
+
| **v0.10.38** | 2026-10-17 | **接管成功即释放 claim(修 0.10.35 的副作用)+ #45 真机 A/B 验证数据**。0.10.35 的 claim 在「助手变成常驻看门狗」这条路径上永远不会被释放(那个进程不退出),于是接管完成后 60 秒内的下一次 restart 会被自己挡成 in-flight(本机实测踩到)。现在 bootWatchdog() 之后立刻 releaseReplaceClaim() —— 互斥窗口严格等于「接管进行中」。**#45 真机验证(同一台机、同一组 20 次 /version-check)**:0.10.36 → 9 成功 / **11 拒连**,TIME_WAIT 24→**1976**(+1952 ≈ 每请求 100 条连接 = 递归深度),首帧 2389ms;0.10.37 → **20/20 成功**,TIME_WAIT 1969→**1971**(+2),18–34ms。npm test 共 **376 项** |
|
|
398
398
|
|
|
399
399
|
<details>
|
|
400
|
-
<summary>更早的更新日志(v0.10.
|
|
400
|
+
<summary>更早的更新日志(v0.10.37 及以前 · 共 64 版,点开查看)</summary>
|
|
401
401
|
|
|
402
402
|
| 版本 | 日期 | 关键改动 |
|
|
403
403
|
|---|---|---|
|
|
404
|
+
| **v0.10.37** | 2026-10-17 | **修 #45(我 0.10.36 引入的严重回归:/version-check 自我探测递归被重试放大)+ #44(node 路径硬编码 / 僵尸看门狗误判)+ #43(跟随会话的官方字段)**。① **#45**:selfPort() 就是本进程自己的端口,而 /version-check 会用 probeServedVersion **再向它发一条 /version-check** → 处理器 → 自己 → 处理器…… 深度只受超时约束、每层各开一条连接 → listen backlog 打满 → **间歇 ECONNREFUSED(17–37ms)**、TIME_WAIT 静置 40 秒涨 4402、served 反成空串;0.10.36 的「重试 3 次」把它**乘以 3**。现在自探一律**直接作答**(probedSelf:true,不发 HTTP),/restart-self 同一道闸门;/version-check 也支持 ?port= 问别的端口。② **#44**:nodeBinary() 缓存命中前**验存在性**,execPath 失效(brew 升级删掉旧目录)时回退 PATH —— 长命 watchdog 从此自愈;inspectResident 新增 portServing,decideResidentAction 遇「登记说 watchdog 活着、端口却没人应答」判 restart(watchdog-port-dead)而**不再 delegate**(那正是「沉淀静默停摆、日志只写 delegated」的现场);status 打印实际用的 node 与存在性。③ **#43**:sessionRouteFrom() 改为 async,按官方字段优先取 session.requestHeader().config → agent.options → 旧嗅探兜底。npm test 共 **375 项** |
|
|
405
|
+
| **v0.10.36** | 2026-10-16 | **把两种新状态接到所有入口(align / doctor)+ 修一处测试卫生**。① align 新增一条结论:**常驻实例由宿主托管**(锁不可写 → 看门狗路径被绕过,改由宿主停旧起新并保活)—— **warn 级**,明确写「实例是有人管的,不是孤儿;锁恢复可写后自动交还看门狗」(align.ok 不受影响,warn 不算失败);② dsh-memory doctor 增印一行 **常驻实例托管:**(看门狗 / 宿主托管 / 不可替换(环境限制)),配合已有的 journal 模式、写探针、锁可写性、备份清单,一次看清这台机器处于哪种状态;③ **测试卫生**:/restart-self 的 alreadyCurrent 用例依赖**开发者本机真实的锁文件**(我本机锁里只剩一条 endedAt 记录,于是它不再判 alreadyCurrent、用例翻红)—— 把「读锁」做成可注入(deps.readWatchdogLock),用例自己注入一条活登记,从此与开发机状态无关。npm test 共 **371 项** |
|
|
404
406
|
| **v0.10.35** | 2026-10-16 | **接管助手单飞互斥(#30 第六轮实测:宿主 tick 与 CLI 各拉一个 --replace 互相踩)**。报告者观察到失败的 restart 期间同端口上**同时存在两个互不相识的接管助手**(父 = DSH 宿主 / 父 = CLI),谁都不写锁、还互相收对方的 web —— 现场更难复盘。修法:新增 **claim 文件**(memory-eternal-replace.claim,与看门狗锁同目录)—— ① **助手开工前独占抢 claim**,抢不到就**直接退出**(把接管让给已经在跑的那个),并在退出时释放;② **restartResident 发现接管进行中**(持有者活着且 <60s)就**不再拉第二个助手**,返回 stage「in-flight」并说明是谁在跑、多久前启动;③ **失败开放**:受限环境写不了 claim 时**照常接管**(不能让权限问题把接管彻底堵死),但把原因写进返回值 / 日志。测试覆盖:先到先得 / 活持有者让位 / 过期与死 pid 可接管 / 只清自己的 claim / restartResident 的 in-flight 分支。npm test 共 **370 项** |
|
|
405
407
|
| **v0.10.34** | 2026-10-16 | **治本:锁不可写时由「宿主」自己托管常驻实例(采纳 #30 报告者第四点建议)**。他的诊断:那台机器上「登记」恰好落在**唯一写不了锁的那个进程**上 → 自动替换被永久拒绝。修法:① 宿主的 `autoWebMode=interval` 保活循环**不再只探存活**,还探**端口上服务的版本** —— 落后且锁可写 → 仍交给看门狗 / 漂移路径(宿主不抢);落后且**锁不可写** → **宿主自己接管**(`ensureWebServer({forceTakeover:true})`:只收掉**自家** web、在**同端口**起新实例、由宿主保活);② 新增可测试的 `takeoverOwnWeb()`(安全闸门:只对 `looksLikeOurWeb` 为真的占用者下手,别人的进程一个都不动);③ 「宿主托管」记成状态(`recordHostManaged`)→ `/version-check` 报 `resident.managedBy='host'`,面板显示「**宿主托管**」而不是「未托管 / 孤儿」;看门狗重新登记后自动清除。`npm test` 共 **366 项** |
|
|
406
408
|
| **v0.10.33** | 2026-10-15 | **蒸馏供应商:动态下拉 + 跟随当前会话(尽力)+ 仅试首选 + 测试按钮(issue #42 第一刀)**。① **动态下拉**:新增只读接口 `/llm-routes`(宿主侧的 listProviders/listModels),配置页的「蒸馏 provider / model」从**手填编号**变成**下拉选择**(取不到时自动回退成文本输入);② **跟随当前会话**(他的最高优先级):新增配置 `captureFollowSession`,开启后宿主蒸馏**先尝试**用这一轮会话的 provider/model(`sessionRouteFrom()` 按 `agent / session / session.route / session.options` 逐个嗅探),**读不到就回退显式配置并记一条日志**(DSH 未保证会话对象暴露 route —— 这条日志就是以后补适配的依据,不猜);后台 / 无会话任务(CLI、watchdog、hooks)天然只能回退配置;③ **仅试首选 + 测试按钮**:`captureFirstOnly` 让选路只取第一个候选、失败即停(避免「先打官方 → 白烧一次调用 → 再轮换」);`POST /llm-test` 用真实调用路径打一次小请求,回显 **provider / model / 耗时 / 候选数 / 错误码**,也是 ①② 的验证手段。④ 顺带修一处**探测抖动**:`probeServedVersion()` 现在退避重试 3 次(报告者实测「同一时刻 version-check 200、2 秒后 status 说没有服务」),单发探测不再被重启窗口/瞬时拒绝骗到。`npm test` 共 **363 项** |
|
package/index.js
CHANGED
|
@@ -1305,7 +1305,20 @@ export function apply(ctx, config) {
|
|
|
1305
1305
|
.catch(() => {})
|
|
1306
1306
|
}
|
|
1307
1307
|
|
|
1308
|
-
// (2)
|
|
1308
|
+
// (2) 持久接线体检(#44):MCP / hooks 里若写着**已不存在**的 node 路径,
|
|
1309
|
+
// 那台机器上的外部 agent 会静默连不上 —— 没人会天天跑 align,所以启动时只读检查一次并记日志。
|
|
1310
|
+
import('./lib/align.js')
|
|
1311
|
+
.then((m) => m.staleWiringCheck())
|
|
1312
|
+
.then((r) => {
|
|
1313
|
+
if (!r) return
|
|
1314
|
+
const parts = []
|
|
1315
|
+
if (r.missingNode && r.missingNode.length) parts.push('node 路径已不存在:' + r.missingNode.join(';'))
|
|
1316
|
+
if (r.stale && r.stale.length) parts.push('条目与当前不一致:' + r.stale.join('、'))
|
|
1317
|
+
if (parts.length) logCapture('system', 'warn', '持久接线需要重写(' + parts.join(' / ') + ')→ 执行 dsh-memory setup(或 align --fix)')
|
|
1318
|
+
})
|
|
1319
|
+
.catch(() => {})
|
|
1320
|
+
|
|
1321
|
+
// (3) web server:根据 autoWeb + autoWebMode 决策
|
|
1309
1322
|
const mode = cfg0.autoWebMode || 'init'
|
|
1310
1323
|
const port = Number(cfg0.webPort) || 7999
|
|
1311
1324
|
const ensureOpts = { port, vaultRoot: vaultDir() }
|
package/lib/align.js
CHANGED
|
@@ -16,6 +16,7 @@ import path from 'node:path'
|
|
|
16
16
|
import { getSetupStatus, PACKAGE_ROOT } from './setup.js'
|
|
17
17
|
import { configFilePath } from './capture-run.js'
|
|
18
18
|
import { probeServedVersion } from './watchdog.js'
|
|
19
|
+
import { nodeBinary } from './node-bin.js'
|
|
19
20
|
|
|
20
21
|
/**
|
|
21
22
|
* 纯函数:把「版本现状 + 接线现状」翻译成结论清单(便于单测,无任何 IO)。
|
|
@@ -149,6 +150,27 @@ async function hookPathsFrom(file, agent, pick) {
|
|
|
149
150
|
* 实际检查(IO 版):运行时版本 + 持久接线。宿主调用时把 hostVersion / latest / knownKeys 传进来。
|
|
150
151
|
* @param {{hostVersion?:string, residentPort?:number, latest?:string, updateCommand?:string, knownKeys?:string[]|null}} opts
|
|
151
152
|
*/
|
|
153
|
+
/**
|
|
154
|
+
* 启动期用:**只读**检查持久接线里有没有「指向已不存在的 node 路径」的条目(issue #44 建议 4)。
|
|
155
|
+
* 不跑网络、不改任何文件 —— 只为了让「静默失效」变可见:没人会天天跑 align。
|
|
156
|
+
*
|
|
157
|
+
* @returns {Promise<{stale:string[], missingNode:string[]}>}
|
|
158
|
+
*/
|
|
159
|
+
export async function staleWiringCheck() {
|
|
160
|
+
const out = { stale: [], missingNode: [] }
|
|
161
|
+
try {
|
|
162
|
+
const st = await getSetupStatus({ nodePath: nodeBinary() })
|
|
163
|
+
for (const a of (st.agents || [])) {
|
|
164
|
+
if (!a || !a.installed) continue
|
|
165
|
+
const configured = String(a.configuredNodePath || '')
|
|
166
|
+
const isNodeLike = /node(\.exe)?$/i.test(configured) || configured.includes('Cellar') || configured.includes('nodejs')
|
|
167
|
+
if (configured && isNodeLike && !fs.existsSync(configured)) out.missingNode.push(a.name + ' → ' + configured)
|
|
168
|
+
else if (a.mcpMatchesCurrentNode === false) out.stale.push(a.name)
|
|
169
|
+
}
|
|
170
|
+
} catch { /* 读不到就算了:这只是体检,不能影响启动 */ }
|
|
171
|
+
return out
|
|
172
|
+
}
|
|
173
|
+
|
|
152
174
|
export async function inspectAlignment({ hostVersion = '', residentPort = 7999, latest = '', updateCommand = '', knownKeys = null } = {}) {
|
|
153
175
|
let onDisk = ''
|
|
154
176
|
try { onDisk = String(JSON.parse(fs.readFileSync(path.join(PACKAGE_ROOT, 'package.json'), 'utf8')).version || '') } catch { onDisk = '' }
|
package/lib/client.js
CHANGED
|
@@ -298,7 +298,7 @@ Option A, because:
|
|
|
298
298
|
height: calc(100vh - var(--dsh-windows-titlebar-height, 0px));
|
|
299
299
|
max-height: calc(100vh - var(--dsh-windows-titlebar-height, 0px));
|
|
300
300
|
}
|
|
301
|
-
`;function Ca(t){t.effect(()=>t.locale.register(st,{zh:Jt,en:Zt}),"memory-eternal: locale");let i=t.locale.bind(st);t.effect(()=>t.slots.inject("settings.section",()=>t.slots.register({name:"settings.section",id:st,order:25,label:()=>i("nav"),locale:st,inject:()=>({})},()=>o.default.createElement(Ma,{t:i}))),"memory-eternal: settings section"),t.effect(()=>t.slots.inject("sidebar.footer.action",()=>t.slots.register({name:"sidebar.footer.action",id:`${st}:footer`,order:100,label:()=>i("nav"),locale:st,inject:()=>({})},r=>o.default.createElement(Ra,{t:i,locale:t.locale,wide:!(r&&r.wide===!1)}))),"memory-eternal: sidebar footer action")}function Na(){let[t,i]=(0,o.useState)(O+"/ui/app");return(0,o.useEffect)(()=>{let r=!0,g=()=>fetch("/memory-eternal/api/web-info").then(c=>c.json()).then(c=>{r&&c&&c.ok&&c.alive!==!1&&c.url&&i(c.url.endsWith("/")?c.url:c.url+"/")}).catch(()=>{});return fetch(O+"/ui/app",{method:"HEAD"}).then(c=>{if(r&&!c.ok)return g()}).catch(()=>r?g():void 0),()=>{r=!1}},[]),t}function Ma({t}){return(0,e.jsxs)("div",{style:{display:"flex",flexDirection:"column",height:"100%",minHeight:0},children:[(0,e.jsx)("style",{children:Ee}),(0,e.jsx)(aa,{t,onReload:()=>{},version:0})]})}function Ra({t,locale:i,wide:r}){let[g,c]=(0,o.useState)(!1);return(0,e.jsxs)("div",{className:`me-footer${r?"":" rail"}`,children:[(0,e.jsx)("style",{children:Ee}),(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)(ea,{})}),(0,e.jsx)("span",{className:"me-footer-label",children:t("nav")})]}),g&&(0,e.jsx)(Ta,{t,locale:i,onClose:()=>c(!1)})]})}function Ta({t,locale:i,onClose:r,tab:g}){let[c,l]=(0,o.useState)(!1),C=(0,o.useRef)(null),T=Na(),S=(0,o.useCallback)(R=>i&&typeof i.subscribe=="function"?i.subscribe(R):()=>{},[i]),H=(0,o.useCallback)(()=>i&&typeof i.getSnapshot=="function"?i.getSnapshot():null,[i]),k=(0,o.useSyncExternalStore)(S,H),x=k&&(k.active==="zh"||k.active==="en")?k.active:null,m=(0,o.useRef)(null);m.current===null&&x&&(m.current=x);let z=(0,o.useMemo)(()=>{let R=m.current?`lang=${m.current}`:"";if(g==="config")return`${O}/ui/config?tab=config${R?`&${R}`:""}`;let w=T.replace(/\/$/,""),j=w.endsWith("/ui/app")?"?":"/?";return g?`${w}${j}tab=${g}${R?`&${R}`:""}`:R?`${w}${j}${R}`:T},[T,g]);return(0,o.useEffect)(()=>{let R=C.current&&C.current.contentWindow;R&&x&&R.postMessage({source:"memory-eternal",type:"locale",lang:x},"*")},[x]),(0,o.useEffect)(()=>{let R=w=>{w.key==="Escape"&&r()};return window.addEventListener("keydown",R),()=>window.removeEventListener("keydown",R)},[r]),(0,e.jsxs)("div",{className:"me-overlay-top",onClick:r,children:[(0,e.jsx)("style",{children:Ee}),(0,e.jsx)("div",{className:"me-modal"+(c?" me-modal-full":""),onClick:R=>R.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:()=>l(R=>!R),"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:r,"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:C,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 ea(){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 yt="0.10.
|
|
301
|
+
`;function Ca(t){t.effect(()=>t.locale.register(st,{zh:Jt,en:Zt}),"memory-eternal: locale");let i=t.locale.bind(st);t.effect(()=>t.slots.inject("settings.section",()=>t.slots.register({name:"settings.section",id:st,order:25,label:()=>i("nav"),locale:st,inject:()=>({})},()=>o.default.createElement(Ma,{t:i}))),"memory-eternal: settings section"),t.effect(()=>t.slots.inject("sidebar.footer.action",()=>t.slots.register({name:"sidebar.footer.action",id:`${st}:footer`,order:100,label:()=>i("nav"),locale:st,inject:()=>({})},r=>o.default.createElement(Ra,{t:i,locale:t.locale,wide:!(r&&r.wide===!1)}))),"memory-eternal: sidebar footer action")}function Na(){let[t,i]=(0,o.useState)(O+"/ui/app");return(0,o.useEffect)(()=>{let r=!0,g=()=>fetch("/memory-eternal/api/web-info").then(c=>c.json()).then(c=>{r&&c&&c.ok&&c.alive!==!1&&c.url&&i(c.url.endsWith("/")?c.url:c.url+"/")}).catch(()=>{});return fetch(O+"/ui/app",{method:"HEAD"}).then(c=>{if(r&&!c.ok)return g()}).catch(()=>r?g():void 0),()=>{r=!1}},[]),t}function Ma({t}){return(0,e.jsxs)("div",{style:{display:"flex",flexDirection:"column",height:"100%",minHeight:0},children:[(0,e.jsx)("style",{children:Ee}),(0,e.jsx)(aa,{t,onReload:()=>{},version:0})]})}function Ra({t,locale:i,wide:r}){let[g,c]=(0,o.useState)(!1);return(0,e.jsxs)("div",{className:`me-footer${r?"":" rail"}`,children:[(0,e.jsx)("style",{children:Ee}),(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)(ea,{})}),(0,e.jsx)("span",{className:"me-footer-label",children:t("nav")})]}),g&&(0,e.jsx)(Ta,{t,locale:i,onClose:()=>c(!1)})]})}function Ta({t,locale:i,onClose:r,tab:g}){let[c,l]=(0,o.useState)(!1),C=(0,o.useRef)(null),T=Na(),S=(0,o.useCallback)(R=>i&&typeof i.subscribe=="function"?i.subscribe(R):()=>{},[i]),H=(0,o.useCallback)(()=>i&&typeof i.getSnapshot=="function"?i.getSnapshot():null,[i]),k=(0,o.useSyncExternalStore)(S,H),x=k&&(k.active==="zh"||k.active==="en")?k.active:null,m=(0,o.useRef)(null);m.current===null&&x&&(m.current=x);let z=(0,o.useMemo)(()=>{let R=m.current?`lang=${m.current}`:"";if(g==="config")return`${O}/ui/config?tab=config${R?`&${R}`:""}`;let w=T.replace(/\/$/,""),j=w.endsWith("/ui/app")?"?":"/?";return g?`${w}${j}tab=${g}${R?`&${R}`:""}`:R?`${w}${j}${R}`:T},[T,g]);return(0,o.useEffect)(()=>{let R=C.current&&C.current.contentWindow;R&&x&&R.postMessage({source:"memory-eternal",type:"locale",lang:x},"*")},[x]),(0,o.useEffect)(()=>{let R=w=>{w.key==="Escape"&&r()};return window.addEventListener("keydown",R),()=>window.removeEventListener("keydown",R)},[r]),(0,e.jsxs)("div",{className:"me-overlay-top",onClick:r,children:[(0,e.jsx)("style",{children:Ee}),(0,e.jsx)("div",{className:"me-modal"+(c?" me-modal-full":""),onClick:R=>R.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:()=>l(R=>!R),"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:r,"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:C,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 ea(){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 yt="0.10.39";function Da({t,inModal:i,onClose:r,onFull:g,full:c}){let[l,C]=(0,o.useState)(null),[T,S]=(0,o.useState)([]),[H,k]=(0,o.useState)(!0),[x,m]=(0,o.useState)(null),[z,R]=(0,o.useState)(""),[w,j]=(0,o.useState)("all"),[L,K]=(0,o.useState)(""),[te,V]=(0,o.useState)("all"),[F,Ie]=(0,o.useState)("all"),[G,ge]=(0,o.useState)(()=>{try{let b=new URLSearchParams(typeof location<"u"?location.search:"").get("tab"),n={cards:"cards",graph:"graph",usage:"stats",stats:"stats",audit:"audit",recycle:"optimize",optimize:"optimize",config:"config"};if(b&&n[b])return n[b]}catch{}return"cards"}),[X,$]=(0,o.useState)(()=>new Set([G])),le=(0,o.useCallback)(b=>{$(n=>n.has(b)?n:new Set(n).add(b)),ge(b)},[]),[he,Ae]=(0,o.useState)("recent"),[ke,y]=(0,o.useState)(null),ue=(0,o.useRef)(null),Te=(0,o.useRef)(null),[h,U]=(0,o.useState)(!1),[re,ye]=(0,o.useState)(null),[ee,Z]=(0,o.useState)(!1),[Be,Ce]=(0,o.useState)(!1),[Re,Ne]=(0,o.useState)(!0),[s,N]=(0,o.useState)(null),[I,Q]=(0,o.useState)(0),we=(0,o.useCallback)(()=>Q(b=>b+1),[]),de=l?.quarantine?.pending??0,ae=l?.quarantine?.deleted??0,Y=(0,o.useRef)(null),[We,Le]=(0,o.useState)(0),[p,d]=(0,o.useState)(!1),_=(0,o.useRef)(null),se=(0,o.useRef)(null),ce=(0,o.useCallback)(async(b=w,n=L,v=F,D={})=>{let B=D.append===!0;try{B?d(!0):(k(!0),R(""));let E=new URLSearchParams;b&&b!=="all"&&E.set("kind",b),n.trim()&&E.set("q",n.trim()),E.set("status",v||"all"),E.set("limit",String(xa)),E.set("offset",String(D.offset||0)),he==="title"&&E.set("sort","title");let J=await(await fetch(`${O}/cards?${E.toString()}`)).json();if(J.ok){let ve=J.cards||[];S(De=>B?[...De,...ve.filter(Pe=>!De.some(vt=>vt.path===Pe.path))]:ve),Le(Number(J.total)||ve.length)}}catch(E){R(String(E&&E.message?E.message:E))}finally{B?d(!1):k(!1)}},[w,L,F,he]),fe=(0,o.useCallback)(async()=>{try{let[b,n]=await Promise.all([fetch(`${O}/overview`).then(v=>v.json()),fetch(`${O}/capture-log`).then(v=>v.json()).catch(()=>null)]);b.ok&&C(b),m(n&&n.ok&&n.available&&n.health||null)}catch(b){R(String(b&&b.message?b.message:b))}finally{k(!1)}},[]),a=async b=>{try{let v=await(await fetch(`${O}/delete?path=${encodeURIComponent(b)}`)).json();v.ok?(y({ok:!0,msg:t("deleted")}),await fe(),we()):y({ok:!1,msg:v.error||t("deleteFail")})}catch{y({ok:!1,msg:t("deleteFail")})}ue.current&&clearTimeout(ue.current),ue.current=setTimeout(()=>y(null),2e3)};(0,o.useEffect)(()=>(fe(),()=>{Y.current&&clearTimeout(Y.current)}),[fe]);let Fe=b=>{K(b),Y.current&&clearTimeout(Y.current),Y.current=setTimeout(()=>ce(w,b,F),280)},Je=b=>{j(b),ce(b,L,F)};(0,o.useEffect)(()=>{G==="cards"&&ce(w,L,F)},[F,G,I,he]),(0,o.useEffect)(()=>{if(G!=="cards"||H||p||T.length>=We)return;let b=_.current,n=se.current;if(!b)return;let v=new IntersectionObserver(D=>{D[0]&&D[0].isIntersecting&&ce(w,L,F,{append:!0,offset:T.length})},{root:n||null,rootMargin:"400px"});return v.observe(b),()=>v.disconnect()},[G,T,We,H,p,w,L,F,ce]);let et=async b=>{try{let n=b.status;if(!n&&b.path)try{let W=((await fetch(`${O}/cards?status=all&q=${encodeURIComponent(b.title||"")}&limit=50`).then(J=>J.json())).cards||[]).find(J=>J.path===b.path);W&&(n=W.status)}catch{}let v=n&&n!=="approved"?`&status=${encodeURIComponent(n)}`:"",D=await fetch(`${O}/card?path=${encodeURIComponent(b.path||b.id)}${v}`),B=await D.json();B.ok?ye({path:B.path,title:b.title,text:B.text}):R(B.error||`HTTP ${D.status}`)}catch(n){R(String(n&&n.message?n.message:n))}},at=async(b,n)=>{U(!0);try{let v=await fetch(`${O}/export`);if(!v.ok)throw new Error("HTTP "+v.status);let D=await v.json();if(!D.ok)throw new Error(D.error||"export failed");let B,E,W;if(b==="json"){E="memory-vault.json",W="application/json";let De=D.cards.map(Pe=>({path:Pe.path,title:Pe.title,kind:Pe.kind,status:Pe.status,store:Pe.store,text:Pe.text}));B=JSON.stringify({format:D.format||"memory-eternal-vault",formatVersion:D.formatVersion||1,exportedAt:D.exportedAt||new Date().toISOString(),count:De.length,cards:De},null,2)}else E="memory-vault.md",W="text/markdown;charset=utf-8",B=D.cards.map(De=>De.text.trim()).filter(Boolean).join(`
|
|
302
302
|
|
|
303
303
|
---
|
|
304
304
|
|
package/lib/setup.js
CHANGED
|
@@ -178,6 +178,56 @@ async function setupClaudeHooks({ nodePath, dryRun, log }) {
|
|
|
178
178
|
|
|
179
179
|
// -- Codex CLI:~/.codex/config.toml 的 [mcp_servers.memory] --
|
|
180
180
|
|
|
181
|
+
/**
|
|
182
|
+
* 纯函数:从 codex 的 config.toml 里解析 `[mcp_servers.memory]` 段(issue #44)。
|
|
183
|
+
*
|
|
184
|
+
* 三个坑都在这一个函数里收口:
|
|
185
|
+
* ① 必须**按段**解析 —— 否则会抓到别的 server 的第一个 `command =`(本机真实踩到:读成了 MCP_DOCKER 的 docker.exe);
|
|
186
|
+
* ② 单引号是合法 TOML(codex 自己写的就是 'C:\…');
|
|
187
|
+
* ③ 双引号串里的 \\ 是转义,读出来要反转义,否则与当前路径永远不相等。
|
|
188
|
+
*/
|
|
189
|
+
export function parseCodexMemoryEntry(text) {
|
|
190
|
+
const src = String(text || '')
|
|
191
|
+
const present = /^\s*\[mcp_servers\.memory\]\s*$/m.test(src)
|
|
192
|
+
if (!present) return { present: false, command: '', args: [] }
|
|
193
|
+
const lines = src.split(/\r?\n/)
|
|
194
|
+
let inSec = false
|
|
195
|
+
const sec = []
|
|
196
|
+
for (const line of lines) {
|
|
197
|
+
if (/^\s*\[/.test(line)) { inSec = /^\s*\[mcp_servers\.memory\]\s*$/.test(line.trim()); continue }
|
|
198
|
+
if (inSec) sec.push(line)
|
|
199
|
+
}
|
|
200
|
+
const body = sec.join('\n')
|
|
201
|
+
const unesc = (s) => String(s || '').replace(/\\\\/g, '\\')
|
|
202
|
+
const cmd = /command\s*=\s*["']([^"']+)["']/.exec(body)
|
|
203
|
+
let args = []
|
|
204
|
+
const argsLine = /^\s*args\s*=\s*\[([^\]]*)\]\s*$/m.exec(body)
|
|
205
|
+
if (argsLine) {
|
|
206
|
+
for (const m of argsLine[1].matchAll(/["']([^"']+)["']/g)) args.push(unesc(m[1]))
|
|
207
|
+
}
|
|
208
|
+
return { present: true, command: cmd ? unesc(cmd[1]) : '', args }
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* 纯函数:把 `[mcp_servers.memory]` 段对齐到 want(只动这一段的两行)。
|
|
213
|
+
* @returns {{text:string, changed:boolean}}
|
|
214
|
+
*/
|
|
215
|
+
export function alignCodexMemorySection(text, want) {
|
|
216
|
+
const lines = String(text || '').split(/\r?\n/)
|
|
217
|
+
let inSec = false
|
|
218
|
+
let changed = false
|
|
219
|
+
for (let i = 0; i < lines.length; i++) {
|
|
220
|
+
if (/^\s*\[/.test(lines[i])) { inSec = /^\s*\[mcp_servers\.memory\]\s*$/.test(lines[i].trim()); continue }
|
|
221
|
+
if (!inSec) continue
|
|
222
|
+
// 只在**真的不同**时才算改动(幂等:已经是目标形态就不该报 changed,否则每次 setup 都像改过)
|
|
223
|
+
const wantCmd = 'command = ' + JSON.stringify(want.command)
|
|
224
|
+
const wantArgs = 'args = ' + JSON.stringify(want.args)
|
|
225
|
+
if (/^\s*command\s*=/.test(lines[i])) { if (lines[i] !== wantCmd) { lines[i] = wantCmd; changed = true } }
|
|
226
|
+
else if (/^\s*args\s*=/.test(lines[i])) { if (lines[i] !== wantArgs) { lines[i] = wantArgs; changed = true } }
|
|
227
|
+
}
|
|
228
|
+
return { text: changed ? lines.join('\n') : String(text || ''), changed }
|
|
229
|
+
}
|
|
230
|
+
|
|
181
231
|
export async function setupCodex({ nodePath, dryRun = false, log = () => {} } = {}) {
|
|
182
232
|
const codexToml = path.join(home, '.codex', 'config.toml')
|
|
183
233
|
if (!(await exists(codexToml))) {
|
|
@@ -188,22 +238,31 @@ export async function setupCodex({ nodePath, dryRun = false, log = () => {} } =
|
|
|
188
238
|
const text = await fs.readFile(codexToml, 'utf8')
|
|
189
239
|
const sectionRe = /^\[mcp_servers\.memory\]$/m
|
|
190
240
|
if (sectionRe.test(text)) {
|
|
191
|
-
//
|
|
192
|
-
|
|
193
|
-
|
|
241
|
+
// 已存在:把这一节**整块**对齐到当前版本写出的形态。
|
|
242
|
+
// issue #44 现场的教训:TOML 里可能是**单引号**(`command = 'C:\…'`),老代码只认双引号 →
|
|
243
|
+
// 既读不到(status 报「配置里是 (空)」)也改不掉(`replace` 不命中),**却照样打印「已更新」**。
|
|
244
|
+
// 所以:① 读写都认两种引号;② 改完必须**校验文本真的变了**,没变就如实报失败。
|
|
245
|
+
const curCmd = /command\s*=\s*["']([^"']*)["']/.exec(text)?.[1] || ''
|
|
246
|
+
const curArgs = /^args\s*=\s*\[([^\]]*)\]\s*$/m.exec(text)?.[1] || ''
|
|
247
|
+
const argsOk = want.args.every((a) => curArgs.includes(JSON.stringify(a)) || curArgs.includes("'" + a + "'"))
|
|
248
|
+
if (curCmd === want.command && argsOk) {
|
|
194
249
|
log('✓ Codex MCP 已是最新,跳过')
|
|
195
250
|
return [{ agent: 'codex', ok: true, reason: 'section exists' }]
|
|
196
251
|
}
|
|
197
|
-
//
|
|
198
|
-
const newText
|
|
199
|
-
if (!dryRun) {
|
|
252
|
+
// 只改 `[mcp_servers.memory]` 这一段(按行扫描,避免误伤别的 server 的同名字段)
|
|
253
|
+
const { text: newText, changed } = alignCodexMemorySection(text, want)
|
|
254
|
+
if (changed && !dryRun) {
|
|
200
255
|
await backupOnce(codexToml)
|
|
201
256
|
await fs.writeFile(codexToml, newText, 'utf8')
|
|
202
|
-
log(`✓ Codex MCP node
|
|
203
|
-
} else {
|
|
204
|
-
log('[dry-run] 将更新 Codex MCP
|
|
257
|
+
log(`✓ Codex MCP 已对齐到当前 node / 脚本:${want.command} ${want.args.join(' ')}`)
|
|
258
|
+
} else if (dryRun) {
|
|
259
|
+
log('[dry-run] 将更新 Codex MCP 的 command/args')
|
|
260
|
+
}
|
|
261
|
+
if (!changed) {
|
|
262
|
+
log('✗ Codex MCP 条目存在但没找到可改写的 command/args 行 —— 请手工检查 ~/.codex/config.toml')
|
|
263
|
+
return [{ agent: 'codex', ok: false, reason: 'section unparseable' }]
|
|
205
264
|
}
|
|
206
|
-
return [{ agent: 'codex', ok: true, reason: '
|
|
265
|
+
return [{ agent: 'codex', ok: true, reason: changed ? 'aligned' : 'section exists' }]
|
|
207
266
|
}
|
|
208
267
|
const block = [
|
|
209
268
|
'',
|
|
@@ -327,7 +386,10 @@ export async function getSetupStatus({ nodePath = process.execPath } = {}) {
|
|
|
327
386
|
const text = await fs.readFile(codexToml, 'utf8')
|
|
328
387
|
const has = /^\[mcp_servers\.memory\]$/m.test(text)
|
|
329
388
|
let configuredNodePath = ''
|
|
330
|
-
|
|
389
|
+
// 必须**先切出 [mcp_servers.memory] 这一段**再读 command —— 否则会抓到别的 server 的
|
|
390
|
+
// 第一个 `command =`(本机真实踩到:读成了 MCP_DOCKER 的 docker.exe)。
|
|
391
|
+
// 另外:单引号也是合法 TOML(codex 自己写的就是 'C:\…');双引号串里的 \\ 是转义,读出来要反转义。
|
|
392
|
+
if (has) configuredNodePath = parseCodexMemoryEntry(text).command
|
|
331
393
|
agents.push({
|
|
332
394
|
name: 'codex',
|
|
333
395
|
installed: true,
|
package/lib/watchdog.js
CHANGED
|
@@ -1406,6 +1406,9 @@ if (process.argv[1] && /[\\/]watchdog\.js$/.test(process.argv[1])) {
|
|
|
1406
1406
|
}
|
|
1407
1407
|
await noteReplace('boot', summary)
|
|
1408
1408
|
bootWatchdog()
|
|
1409
|
+
// 接管**已完成**(助手从此就是常驻看门狗,不退出)→ 立刻释放 claim。
|
|
1410
|
+
// 否则 claim 会一直留到「60s 过期」:下次 restart 在窗口内会被自己挡住,报 in-flight(实测踩到)。
|
|
1411
|
+
releaseReplaceClaim()
|
|
1409
1412
|
}
|
|
1410
1413
|
} else {
|
|
1411
1414
|
bootWatchdog()
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "memory-eternal",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.39",
|
|
4
4
|
"description": "记忆核心(Memory Eternal):自研的 DeepSeek Harness 记忆插件,不移植任何既有记忆框架——对话结束后自动沉淀知识卡到本地 Markdown Vault(自研去重、自研 CJK 检索、可 git 管理),设置页提供图形化知识库(统计 / 搜索 / 知识图谱 + 侧边栏一键弹窗),Agent 通过 memory_recall 工具按需召回历史上下文。零人工干预。",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
"prepublishOnly": "node build.mjs",
|
|
65
65
|
"release": "node scripts/release.mjs",
|
|
66
66
|
"release:dry": "node scripts/release.mjs --dry-run",
|
|
67
|
-
"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/ssr.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"
|
|
67
|
+
"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/ssr.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"
|
|
68
68
|
},
|
|
69
69
|
"dependencies": {
|
|
70
70
|
"@deepseek-ai/schemastery": "^3.18.1"
|
package/web/app.js
CHANGED
|
@@ -300,7 +300,7 @@ Option A, because:
|
|
|
300
300
|
height: calc(100vh - var(--dsh-windows-titlebar-height, 0px));
|
|
301
301
|
max-height: calc(100vh - var(--dsh-windows-titlebar-height, 0px));
|
|
302
302
|
}
|
|
303
|
-
`;function Kg(){return(0,i.jsxs)("svg",{viewBox:"0 0 24 24","aria-hidden":"true",children:[(0,i.jsx)("defs",{children:(0,i.jsxs)("linearGradient",{id:"mg-brain",x1:"0",y1:"0",x2:"1",y2:"1",children:[(0,i.jsx)("stop",{offset:"0%",stopColor:"#FFC46B"}),(0,i.jsx)("stop",{offset:"50%",stopColor:"#F97316"}),(0,i.jsx)("stop",{offset:"100%",stopColor:"#EA580C"})]})}),(0,i.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,i.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,i.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,i.jsx)("circle",{cx:"12",cy:"8.6",r:"1.1",fill:"rgba(255,255,255,0.85)"})]})}var Ko="0.10.
|
|
303
|
+
`;function Kg(){return(0,i.jsxs)("svg",{viewBox:"0 0 24 24","aria-hidden":"true",children:[(0,i.jsx)("defs",{children:(0,i.jsxs)("linearGradient",{id:"mg-brain",x1:"0",y1:"0",x2:"1",y2:"1",children:[(0,i.jsx)("stop",{offset:"0%",stopColor:"#FFC46B"}),(0,i.jsx)("stop",{offset:"50%",stopColor:"#F97316"}),(0,i.jsx)("stop",{offset:"100%",stopColor:"#EA580C"})]})}),(0,i.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,i.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,i.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,i.jsx)("circle",{cx:"12",cy:"8.6",r:"1.1",fill:"rgba(255,255,255,0.85)"})]})}var Ko="0.10.39";function tf({t:e,inModal:t,onClose:n,onFull:r,full:o}){let[l,a]=(0,x.useState)(null),[s,u]=(0,x.useState)([]),[p,h]=(0,x.useState)(!0),[v,m]=(0,x.useState)(null),[C,M]=(0,x.useState)(""),[b,j]=(0,x.useState)("all"),[d,c]=(0,x.useState)(""),[y,S]=(0,x.useState)("all"),[E,H]=(0,x.useState)("all"),[D,A]=(0,x.useState)(()=>{try{let P=new URLSearchParams(typeof location<"u"?location.search:"").get("tab"),g={cards:"cards",graph:"graph",usage:"stats",stats:"stats",audit:"audit",recycle:"optimize",optimize:"optimize",config:"config"};if(P&&g[P])return g[P]}catch{}return"cards"}),[Q,F]=(0,x.useState)(()=>new Set([D])),ne=(0,x.useCallback)(P=>{F(g=>g.has(P)?g:new Set(g).add(P)),A(P)},[]),[ke,it]=(0,x.useState)("recent"),[Le,R]=(0,x.useState)(null),xe=(0,x.useRef)(null),Qe=(0,x.useRef)(null),[L,Z]=(0,x.useState)(!1),[pe,Ne]=(0,x.useState)(null),[ie,oe]=(0,x.useState)(!1),[at,Ae]=(0,x.useState)(!1),[$e,He]=(0,x.useState)(!0),[w,O]=(0,x.useState)(null),[U,le]=(0,x.useState)(0),Fe=(0,x.useCallback)(()=>le(P=>P+1),[]),ge=l?.quarantine?.pending??0,se=l?.quarantine?.deleted??0,te=(0,x.useRef)(null),[Tt,pt]=(0,x.useState)(0),[N,k]=(0,x.useState)(!1),J=(0,x.useRef)(null),fe=(0,x.useRef)(null),ye=(0,x.useCallback)(async(P=b,g=d,_=E,W={})=>{let K=W.append===!0;try{K?k(!0):(h(!0),M(""));let G=new URLSearchParams;P&&P!=="all"&&G.set("kind",P),g.trim()&&G.set("q",g.trim()),G.set("status",_||"all"),G.set("limit",String(Ug)),G.set("offset",String(W.offset||0)),ke==="title"&&G.set("sort","title");let re=await(await fetch(`${q}/cards?${G.toString()}`)).json();if(re.ok){let Te=re.cards||[];u(Xe=>K?[...Xe,...Te.filter(wt=>!Xe.some(Go=>Go.path===wt.path))]:Te),pt(Number(re.total)||Te.length)}}catch(G){M(String(G&&G.message?G.message:G))}finally{K?k(!1):h(!1)}},[b,d,E,ke]),be=(0,x.useCallback)(async()=>{try{let[P,g]=await Promise.all([fetch(`${q}/overview`).then(_=>_.json()),fetch(`${q}/capture-log`).then(_=>_.json()).catch(()=>null)]);P.ok&&a(P),m(g&&g.ok&&g.available&&g.health||null)}catch(P){M(String(P&&P.message?P.message:P))}finally{h(!1)}},[]),f=async P=>{try{let _=await(await fetch(`${q}/delete?path=${encodeURIComponent(P)}`)).json();_.ok?(R({ok:!0,msg:e("deleted")}),await be(),Fe()):R({ok:!1,msg:_.error||e("deleteFail")})}catch{R({ok:!1,msg:e("deleteFail")})}xe.current&&clearTimeout(xe.current),xe.current=setTimeout(()=>R(null),2e3)};(0,x.useEffect)(()=>(be(),()=>{te.current&&clearTimeout(te.current)}),[be]);let xt=P=>{c(P),te.current&&clearTimeout(te.current),te.current=setTimeout(()=>ye(b,P,E),280)},sn=P=>{j(P),ye(P,d,E)};(0,x.useEffect)(()=>{D==="cards"&&ye(b,d,E)},[E,D,U,ke]),(0,x.useEffect)(()=>{if(D!=="cards"||p||N||s.length>=Tt)return;let P=J.current,g=fe.current;if(!P)return;let _=new IntersectionObserver(W=>{W[0]&&W[0].isIntersecting&&ye(b,d,E,{append:!0,offset:s.length})},{root:g||null,rootMargin:"400px"});return _.observe(P),()=>_.disconnect()},[D,s,Tt,p,N,b,d,E,ye]);let bn=async P=>{try{let g=P.status;if(!g&&P.path)try{let X=((await fetch(`${q}/cards?status=all&q=${encodeURIComponent(P.title||"")}&limit=50`).then(re=>re.json())).cards||[]).find(re=>re.path===P.path);X&&(g=X.status)}catch{}let _=g&&g!=="approved"?`&status=${encodeURIComponent(g)}`:"",W=await fetch(`${q}/card?path=${encodeURIComponent(P.path||P.id)}${_}`),K=await W.json();K.ok?Ne({path:K.path,title:P.title,text:K.text}):M(K.error||`HTTP ${W.status}`)}catch(g){M(String(g&&g.message?g.message:g))}},Vn=async(P,g)=>{Z(!0);try{let _=await fetch(`${q}/export`);if(!_.ok)throw new Error("HTTP "+_.status);let W=await _.json();if(!W.ok)throw new Error(W.error||"export failed");let K,G,X;if(P==="json"){G="memory-vault.json",X="application/json";let Xe=W.cards.map(wt=>({path:wt.path,title:wt.title,kind:wt.kind,status:wt.status,store:wt.store,text:wt.text}));K=JSON.stringify({format:W.format||"memory-eternal-vault",formatVersion:W.formatVersion||1,exportedAt:W.exportedAt||new Date().toISOString(),count:Xe.length,cards:Xe},null,2)}else G="memory-vault.md",X="text/markdown;charset=utf-8",K=W.cards.map(Xe=>Xe.text.trim()).filter(Boolean).join(`
|
|
304
304
|
|
|
305
305
|
---
|
|
306
306
|
|