memory-eternal 0.21.0 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/vault.js CHANGED
@@ -341,7 +341,7 @@ export async function listCards(root, { status, kind, agent, tag = '', limit, of
341
341
  rows.push(...db.prepare(`SELECT * FROM ${store.table} WHERE path IN (${ph})`).all(...paths))
342
342
  }
343
343
  const byPath = new Map(rows.map((r) => [r.path, rowToCard(r)]))
344
- return page.map((r) => byPath.get(r.path)).filter(Boolean)
344
+ return attachRevisions(db, page.map((r) => byPath.get(r.path)).filter(Boolean))
345
345
  }
346
346
 
347
347
  const tail = Number.isInteger(limit) && limit > 0
@@ -355,7 +355,7 @@ export async function listCards(root, { status, kind, agent, tag = '', limit, of
355
355
  { kind, agent, tag, tail },
356
356
  )
357
357
  const finalArgs = Number.isInteger(limit) && limit > 0 ? [...args, limit, Math.max(0, offset | 0)] : args
358
- return db.prepare(sql).all(...finalArgs).map(rowToCard)
358
+ return attachRevisions(db, db.prepare(sql).all(...finalArgs).map(rowToCard))
359
359
  }
360
360
 
361
361
  /** 计数(分页接口要用 total 判断还有没有下一页)。 */
@@ -420,13 +420,42 @@ function moveCardBetweenStores(db, from, to, rel, { status, deletedAt = null, qu
420
420
  })
421
421
  }
422
422
 
423
- /** 回收站:隔离区里 status=deleted,按 deleted_at 倒序。 */
423
+ /**
424
+ * 回收站:隔离区里 status ∈ {deleted, rejected}。
425
+ *
426
+ * issue #57:过去只查 deleted,于是「审核里点驳回」的卡永远进不了回收站 —— 用户在回收站
427
+ * 怎么等都等不到,只能一张张去审核队列里删。驳回与删除都是「这张卡不会被召回」,
428
+ * 对清理者而言该出现在同一处;两者用 status 区分,恢复语义不同(见 restoreCard)。
429
+ * 排序用 COALESCE:驳回卡没有 deleted_at。
430
+ */
424
431
  export async function recycleList(root) {
425
432
  const db = getDb(root)
426
- const rows = db.prepare('SELECT * FROM quarantine WHERE status = ? ORDER BY deleted_at DESC').all('deleted')
433
+ const rows = db.prepare(
434
+ "SELECT * FROM quarantine WHERE status IN ('deleted', 'rejected') ORDER BY COALESCE(deleted_at, updated_at) DESC"
435
+ ).all()
427
436
  return rows.map(rowToCard)
428
437
  }
429
438
 
439
+ /**
440
+ * 永久清空回收站(deleted + rejected)。
441
+ *
442
+ * issue #57 的直接诉求:「清空」要能真清空,而不是只清 30 天前的。
443
+ * 与 purgeExpired 分开:那个是保留期策略(自动),这个是用户显式点「清空」,
444
+ * 语义是"把我在回收站看到的东西全删掉"。逐个走 deleteCard(permanent) 才会
445
+ * 连带清掉 card_updates / feedback / 修改副本关联。
446
+ */
447
+ export async function purgeAllRecycle(root) {
448
+ const db = getDb(root)
449
+ const rows = db.prepare("SELECT path FROM quarantine WHERE status IN ('deleted', 'rejected')").all()
450
+ let purged = 0
451
+ const failed = []
452
+ for (const row of rows) {
453
+ try { await deleteCard(root, row.path, { permanent: true }); purged++ }
454
+ catch (e) { failed.push(row.path) }
455
+ }
456
+ return { ok: true, purged, total: rows.length, failed }
457
+ }
458
+
430
459
  /** 清理超过 retentionDays 天的回收卡(永久删除)。 */
431
460
  export async function purgeExpired(root, retentionDays = 30) {
432
461
  const db = getDb(root)
@@ -464,12 +493,15 @@ export async function readCard(root, rel, { allowUnapproved = false } = {}) {
464
493
  }
465
494
  const status = String(row.status || '')
466
495
  const { text } = rowToMarkdown(row)
496
+ // 修改副本要带上「改的是哪张卡」,审核界面据此标注「修改」而不是「新卡」。
497
+ const rev = getRevision(db, rel)
498
+ const revision = rev ? { targetPath: rev.target_path, targetId: rev.target_id } : null
467
499
  const updates = db.prepare('SELECT content, created_at FROM card_updates WHERE card_id = ? ORDER BY created_at ASC').all(row.id)
468
500
  if (updates.length > 0) {
469
501
  const updateLines = updates.map((u) => `- ${u.created_at.slice(0, 10)}:${u.content}`)
470
- return { path: rel, status, store: mainRow ? 'main' : 'quarantine', text: text + '\n## 更新记录\n\n' + updateLines.join('\n') + '\n' }
502
+ return { path: rel, status, store: mainRow ? 'main' : 'quarantine', text: text + '\n## 更新记录\n\n' + updateLines.join('\n') + '\n', ...(revision ? { revision } : {}) }
471
503
  }
472
- return { path: rel, status, store: mainRow ? 'main' : 'quarantine', text }
504
+ return { path: rel, status, store: mainRow ? 'main' : 'quarantine', text, ...(revision ? { revision } : {}) }
473
505
  }
474
506
 
475
507
  /** 状态中文标签(错误信息与日志用)。 */
@@ -499,6 +531,11 @@ export async function deleteCard(root, rel, { permanent = false } = {}) {
499
531
  db.prepare('DELETE FROM card_updates WHERE card_id = ?').run(id)
500
532
  db.prepare('DELETE FROM feedback WHERE card_path = ?').run(rel)
501
533
  }
534
+ // 修改副本与原卡的关联也一起清掉:删原卡时,指向它的待审副本没有应用对象了
535
+ const revs = db.prepare('SELECT revision_path FROM card_revisions WHERE target_path = ?').all(rel)
536
+ for (const r of revs) db.prepare('DELETE FROM quarantine WHERE path = ?').run(r.revision_path)
537
+ db.prepare('DELETE FROM card_revisions WHERE target_path = ?').run(rel)
538
+ db.prepare('DELETE FROM card_revisions WHERE revision_path = ?').run(rel)
502
539
  db.prepare('DELETE FROM cards WHERE path = ?').run(rel)
503
540
  db.prepare('DELETE FROM quarantine WHERE path = ?').run(rel)
504
541
  })
@@ -517,11 +554,27 @@ export async function deleteCard(root, rel, { permanent = false } = {}) {
517
554
  return { ok: true, soft: true }
518
555
  }
519
556
 
520
- /** 恢复回收站的卡:从隔离区搬回主库,status=approved。写审计日志。 */
557
+ /**
558
+ * 恢复回收站里的卡。写审计日志。
559
+ *
560
+ * 两种来源语义不同(issue #57):
561
+ * - 已删除(deleted):搬回主库,status=approved —— 它本来就是"删了一下",恢复即可用。
562
+ * - 已驳回(rejected):**只能退回审核队列(pending)**。驳回意味着"内容不该入库",
563
+ * 若恢复即 approved,回收站就成了绕过审核批准一条被驳回内容的旁路。
564
+ * 主库物理上只存 approved(见 db.js MAIN_STATUS),所以这里原地改状态、不搬表。
565
+ */
521
566
  export async function restoreCard(root, rel) {
522
567
  const db = getDb(root)
523
568
  const existing = db.prepare('SELECT status FROM quarantine WHERE path = ?').get(rel)
524
569
  if (!existing) throw new Error('卡片不在回收站/隔离区: ' + rel)
570
+ // 修改副本不能「恢复」成一张独立卡 —— 那会跟原卡重复。它只能被批准(覆盖原卡)或删除。
571
+ if (getRevision(db, rel)) throw new Error('这是知识卡的修改副本:请在审核中心「批准」以覆盖原卡,或直接删除')
572
+ const now = new Date().toISOString()
573
+ if (existing.status === 'rejected') {
574
+ db.prepare('UPDATE quarantine SET status = ?, deleted_at = NULL, updated_at = ? WHERE path = ?').run('pending', now, rel)
575
+ db.prepare('INSERT INTO audit_log (card_path, old_status, new_status, changed_by, reason) VALUES (?, ?, ?, ?, ?)').run(rel, 'rejected', 'pending', 'restore', '驳回卡退回审核队列')
576
+ return { ok: true, restored: true, status: 'pending' }
577
+ }
525
578
  const r = moveCardBetweenStores(db, 'quarantine', 'cards', rel, { status: MAIN_STATUS, deletedAt: null })
526
579
  db.prepare('INSERT INTO audit_log (card_path, old_status, new_status, changed_by, reason) VALUES (?, ?, ?, ?, ?)').run(rel, existing.status, MAIN_STATUS, 'restore', '回收站恢复')
527
580
  return { ok: true, restored: true, ...r }
@@ -539,6 +592,12 @@ export async function setCardStatus(root, rel, status, { changedBy = 'system', r
539
592
  const existing = findCardRow(db, rel)
540
593
  if (!existing) throw new Error('卡片不存在: ' + rel)
541
594
 
595
+ // 审核制修改:待审副本被批准 = 用副本内容覆盖原卡(身份/状态/类型不变),副本随即消失。
596
+ if (target === MAIN_STATUS && existing.table === 'quarantine') {
597
+ const rev = getRevision(db, rel)
598
+ if (rev) return applyRevision(db, rel, rev, { changedBy, reason })
599
+ }
600
+
542
601
  if (target === MAIN_STATUS) {
543
602
  if (existing.table === 'cards') {
544
603
  db.prepare('UPDATE cards SET status = ?, updated_at = ? WHERE path = ?').run(target, new Date().toISOString(), rel)
@@ -702,6 +761,99 @@ export async function updateCardContent(root, rel, { title, tags, body, reason }
702
761
  return { ok: true, path: rel, store: found.table, updated: now }
703
762
  }
704
763
 
764
+ // ── 审核制修改(agent 提交 → 人工审核 → 覆盖原卡)────────────────────────
765
+
766
+ /** 查「待审副本 → 原卡」映射;不是修改副本时返回 null。 */
767
+ function getRevision(db, revisionPath) {
768
+ return db.prepare('SELECT * FROM card_revisions WHERE revision_path = ?').get(revisionPath) || null
769
+ }
770
+
771
+ /** 给卡片对象挂上 revision 信息(审核中心据此标「修改」而不是「新卡」)。 */
772
+ function attachRevisions(db, cards) {
773
+ if (!cards.length) return cards
774
+ const ph = cards.map(() => '?').join(',')
775
+ const rows = db.prepare(`SELECT revision_path, target_path, target_id FROM card_revisions WHERE revision_path IN (${ph})`).all(...cards.map((c) => c.path))
776
+ if (!rows.length) return cards
777
+ const byPath = new Map(rows.map((r) => [r.revision_path, r]))
778
+ const targets = [...new Set(rows.map((r) => r.target_path))]
779
+ const tph = targets.map(() => '?').join(',')
780
+ const titles = new Map(db.prepare(`SELECT path, title FROM cards WHERE path IN (${tph})`).all(...targets).map((r) => [r.path, r.title]))
781
+ return cards.map((c) => {
782
+ const r = byPath.get(c.path)
783
+ return r ? { ...c, revision: { targetPath: r.target_path, targetId: r.target_id, targetTitle: titles.get(r.target_path) || '' } } : c
784
+ })
785
+ }
786
+
787
+ /**
788
+ * 提交一张「审核制修改」:把已批准的卡片复制一份到隔离区(status=pending),
789
+ * 内容用调用方给的新标题/标签/正文。**这里绝不改原卡** —— 只有审核通过(setCardStatus)
790
+ * 才会用副本覆盖原卡。agent 走 /card/revise 或 MCP memory_revise,只能到这一步。
791
+ *
792
+ * 同一张卡已有待审修改时就地更新,避免审核队列里堆一串同一张卡的副本。
793
+ */
794
+ export async function reviseCard(root, { target, title, tags, body, reason = '', source = '', submittedBy = '' } = {}) {
795
+ const db = getDb(root)
796
+ const rel = String(target || '').trim()
797
+ if (!rel) throw new Error('缺少要修改的卡片 path')
798
+ const found = findCardRow(db, rel)
799
+ if (!found) throw new Error('原卡片不存在: ' + rel)
800
+ if (found.table !== 'cards') throw new Error('只能修改已审核通过的卡片(当前状态:' + found.row.status + ')')
801
+ const targetRow = found.row
802
+ // 已有待审修改时,未传的字段沿用副本当前值(否则第二次提交会把上次的改动抹掉)
803
+ const pending = db.prepare('SELECT q.* FROM card_revisions r JOIN quarantine q ON q.path = r.revision_path WHERE r.target_path = ? AND q.status = ?').get(rel, 'pending')
804
+ const baseRow = pending || targetRow
805
+ const nextTitle = title === undefined ? baseRow.title : String(title).trim()
806
+ const nextTags = tags === undefined ? normalizeTags(baseRow.tags) : normalizeTags(tags)
807
+ const nextBody = body === undefined ? String(baseRow.body).trim() : String(body).trim()
808
+ if (!nextTitle) throw new Error('标题不能为空')
809
+ if (!nextBody) throw new Error('正文不能为空')
810
+ const now = new Date().toISOString()
811
+ if (pending) {
812
+ db.prepare('UPDATE quarantine SET title = ?, tags = ?, body = ?, summary = ?, updated_at = ?, reason = ? WHERE path = ?')
813
+ .run(nextTitle, JSON.stringify(nextTags), nextBody, nextBody.slice(0, 200), now, reason || pending.reason || '', pending.path)
814
+ db.prepare('UPDATE card_revisions SET updated_at = ? WHERE revision_path = ?').run(now, pending.path)
815
+ return { ok: true, path: pending.path, target: rel, targetId: targetRow.id, reused: true, status: 'pending' }
816
+ }
817
+ // 副本放在独立目录 99-Revisions/:不与任何 kind 根目录冲突,也永远不会被当成新卡批准进主库
818
+ const base = `99-Revisions/${safeSlug(targetRow.title || 'card')}`
819
+ let revisionPath = `${base}.md`
820
+ let i = 2
821
+ while (pathTaken(db, revisionPath)) { revisionPath = `${base}-${i}.md`; i++ }
822
+ const id = nextCardId(db)
823
+ db.prepare(`
824
+ INSERT INTO quarantine
825
+ (id, path, kind, title, tags, body, summary, status, source, submitted_by, severity, reason, created_at, updated_at, quarantined_at, quarantine_reason)
826
+ VALUES (?, ?, ?, ?, ?, ?, ?, 'pending', ?, ?, 'info', ?, ?, ?, datetime('now'), '修改待审')
827
+ `).run(id, revisionPath, targetRow.kind, nextTitle, JSON.stringify(nextTags), nextBody, nextBody.slice(0, 200), source, submittedBy, reason || ('修改知识卡:' + rel), now, now)
828
+ db.prepare('INSERT INTO card_revisions (revision_path, target_path, target_id, created_at, updated_at) VALUES (?, ?, ?, ?, ?)').run(revisionPath, rel, targetRow.id, now, now)
829
+ db.prepare('INSERT INTO audit_log (card_path, old_status, new_status, changed_by, reason) VALUES (?, ?, ?, ?, ?)').run(revisionPath, null, 'pending', submittedBy || 'agent', '提交修改:' + rel)
830
+ return { ok: true, path: revisionPath, target: rel, targetId: targetRow.id, reused: false, status: 'pending' }
831
+ }
832
+
833
+ /**
834
+ * 审核通过修改副本:用副本内容覆盖原卡,副本及其映射一起消失。
835
+ * 只改内容(标题 / 标签 / 正文 / 摘要 + updated_at);状态、类型、来源、创建时间仍属原卡。
836
+ */
837
+ function applyRevision(db, revisionPath, rev, { changedBy = 'user', reason = '' } = {}) {
838
+ return inTransaction(db, () => {
839
+ const revRow = db.prepare('SELECT * FROM quarantine WHERE path = ?').get(revisionPath)
840
+ if (!revRow) throw new Error('修改副本不存在: ' + revisionPath)
841
+ const target = db.prepare('SELECT * FROM cards WHERE path = ?').get(rev.target_path)
842
+ if (!target) throw new Error('原卡片已不存在(可能已被删除):' + rev.target_path)
843
+ const now = new Date().toISOString()
844
+ const tags = normalizeTags(revRow.tags)
845
+ db.prepare('UPDATE cards SET title = ?, tags = ?, body = ?, summary = ?, updated_at = ? WHERE path = ?')
846
+ .run(revRow.title, JSON.stringify(tags), revRow.body, String(revRow.body).trim().slice(0, 200), now, rev.target_path)
847
+ db.prepare('INSERT INTO card_updates (card_id, content, created_at) VALUES (?, ?, ?)')
848
+ .run(target.id, '经审核修改:' + (reason || ('由 ' + (revRow.submitted_by || 'agent') + ' 提交,' + changedBy + ' 批准')), now)
849
+ db.prepare('DELETE FROM quarantine WHERE path = ?').run(revisionPath)
850
+ db.prepare('DELETE FROM card_revisions WHERE revision_path = ?').run(revisionPath)
851
+ db.prepare('INSERT INTO audit_log (card_path, old_status, new_status, changed_by, reason) VALUES (?, ?, ?, ?, ?)')
852
+ .run(rev.target_path, MAIN_STATUS, MAIN_STATUS, changedBy, '应用修改(来源 ' + revisionPath + ')')
853
+ return { ok: true, status: MAIN_STATUS, store: 'cards', applied: true, path: rev.target_path, revisionPath, updated: now }
854
+ })
855
+ }
856
+
705
857
  /** 合并多张卡为一张。 */
706
858
  export async function mergeCards(root, paths) {
707
859
  const list = (Array.isArray(paths) ? paths : []).map((p) => String(p || '').trim()).filter(Boolean)
@@ -3,7 +3,7 @@
3
3
  "id": "memory-eternal",
4
4
  "publisher": "eternalnight996",
5
5
  "name": "memory-eternal",
6
- "version": "0.21.0",
6
+ "version": "0.23.0",
7
7
  "description": "Memory Atlas for Orca: open the local memory vault, pending review and knowledge graph from a sandboxed Orca panel.",
8
8
  "author": {
9
9
  "name": "EternalNight996",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memory-eternal",
3
- "version": "0.21.0",
3
+ "version": "0.23.0",
4
4
  "description": "记忆核心(Memory Eternal):自研的 DeepSeek Harness 记忆插件,不移植任何既有记忆框架——对话结束后自动沉淀知识卡到本地 Markdown Vault(自研去重、自研 CJK 检索、可 git 管理),设置页提供图形化知识库(统计 / 搜索 / 知识图谱 + 侧边栏一键弹窗),Agent 通过 memory_recall 工具按需召回历史上下文。零人工干预。",
5
5
  "icon": "assets/icon.png",
6
6
  "license": "MIT",
@@ -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/graph-force.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/skill-cards.test.mjs && node tests/audit-search.test.mjs && node tests/news.test.mjs && node tests/news-feed.test.mjs && node tests/ledger.test.mjs && node tests/issue-55-timezone.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/graph-force.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/skill-cards.test.mjs && node tests/audit-search.test.mjs && node tests/news.test.mjs && node tests/news-feed.test.mjs && node tests/ledger.test.mjs && node tests/issue-55-timezone.test.mjs && node tests/recycle-center.test.mjs && node tests/news-follows.test.mjs && node tests/news-digest.test.mjs"
74
74
  },
75
75
  "dependencies": {
76
76
  "@deepseek-ai/schemastery": "^3.18.1"