@summerscar/dsh-mnemon-sync 0.1.1 → 0.1.3

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/README.md CHANGED
@@ -1,86 +1,43 @@
1
1
  # dsh-mnemon-sync
2
2
 
3
- 为 [dsh-mnemon](https://github.com/omdsh-dev/dsh-mnemon) 提供备份与恢复:把 Mnemon 的记忆组件打包上传到 S3 兼容存储或 WebDAV,需要时再挑一个包恢复回来。
3
+ 为 [dsh-mnemon](https://github.com/omdsh-dev/dsh-mnemon) 提供备份与恢复:把记忆组件打包上传到 S3 兼容存储或 WebDAV,需要时挑一个包恢复回来。安装后在记忆系统里多一个「同步备份」标签页。
4
4
 
5
- 在记忆系统里以「同步备份」标签页出现,和「概览」「检索」「内容」并排。
6
-
7
- ## 能备份什么
8
-
9
- 按 dsh-mnemon 自己的组件划分,任意组合,多选:
10
-
11
- | 组件 | 内容 | 说明 |
12
- |---|---|---|
13
- | 记忆空间 | `<MNEMON_DATA_DIR>/data/` | 每个空间的 `mnemon.db` 与空间清单,数据库经 SQLite 在线备份 |
14
- | 项目档案 | `<MNEMON_DATA_DIR>/documents/` | `index.json` 与全部 Markdown 文档 |
15
- | 运行时记忆 | `<MNEMON_DATA_DIR>/runtime/` | `memories.json` 与两份投影文件 |
16
-
17
- `MNEMON_DATA_DIR` 未设置时取 `~/.mnemon`。
18
-
19
- ## 为什么不用 litestream
20
-
21
- litestream 是很好的工具,但和「目录级、多空间、可配置目标」这个需求是正交的:
5
+ ## 安装
22
6
 
23
- - 一个 litestream 实例只盯一个数据库文件,v0.5 起每个数据库只允许一个副本。要备份整个 data 目录就得为每个记忆空间养一个常驻进程,空间增删还得动态改配置。
24
- - 它接管 WAL checkpoint 并长期持有读锁。实测会让 `mnemon.db-wal` 停在非零字节,而 dsh-mnemon 自己的备份导出有这条检查:
7
+ ```sh
8
+ dsh plugin --profile web add @summerscar/dsh-mnemon-sync
9
+ ```
25
10
 
26
- ```js
27
- if (existsSync(wal) && statSync(wal).size > 0)
28
- throw new Error(`Memory Space ${id} is busy (mnemon.db-wal is not checkpointed)`)
29
- ```
11
+ 装完重启 DSH 生效。没有 `dsh-mnemon` 时插件仍可运行,只是不显示标签页。
30
12
 
31
- 也就是说上了 litestream,dsh-mnemon 自带的备份会直接报错。
13
+ ## 能备份什么
32
14
 
33
- 这里改用 `node:sqlite` 的 `backup()`:它走 SQLite 自己的在线备份 API,写入方持有 WAL 也不会把副本撕开,并发写只会让备份重跑当前批次,不产生常驻进程。
15
+ 三个组件任意组合,默认全选:
34
16
 
35
- ## 一致性
17
+ | 组件 | 内容 |
18
+ |---|---|
19
+ | 记忆空间 | `<MNEMON_DATA_DIR>/data/`,每个空间的 `mnemon.db` 与空间清单 |
20
+ | 项目档案 | `<MNEMON_DATA_DIR>/documents/`,`index.json` 与全部 Markdown |
21
+ | 运行时记忆 | `<MNEMON_DATA_DIR>/runtime/`,`memories.json` 与两份投影 |
36
22
 
37
- - **数据库**:`backup()` 生成事务一致的副本,产物是已 checkpoint 的单文件,不含 `-wal` / `-shm`。
38
- - **非数据库文件**:JSON 与 Markdown 由存储层原子写入,逐字节复制。
39
- - **校验**:每个文件在打包前记录 SHA-256,manifest 与内容一起入包;上传前先自校验,恢复前再校验一次。
40
- - **恢复顺序**:先下载 → 校验全部校验和 → 报告缺失组件 → 只写回确认的组件。不带 `apply` 时是纯预览,不碰磁盘。
41
- - **覆盖数据库前**会先删除同名的 `-wal` / `-shm`:残留的 WAL 会让 SQLite 重放恢复文件里并不存在的事务。
23
+ `MNEMON_DATA_DIR` 未设置时取 `~/.mnemon`。
42
24
 
43
- ## 目标
25
+ ## 备份目标
44
26
 
45
- - **S3 兼容**:AWS Signature V4 手写实现,已用 AWS 官方文档的测试向量逐字节校验。适用于 Cloudflare R2、MinIO、Backblaze B2、阿里云 OSS 等。
46
- - **WebDAV**:`PUT` 上传、`PROPFIND depth:1` 列目录、`DELETE` 清理,Basic 认证。
27
+ - **S3 兼容** — 手写 AWS Signature V4,适用于 Cloudflare R2、MinIO、Backblaze B2 等
28
+ - **WebDAV** — Basic 认证,自动创建远端目录
47
29
 
48
- 凭据保存在 `<MNEMON_DATA_DIR>/state/mnemon-sync.json`,权限 `0600`,不进 DSH 配置,也不会被备份包带走(`state/` 不属于任何备份组件)。页面回显密钥时只给掩码,回传掩码表示"不修改"。
30
+ 凭据存在 `<MNEMON_DATA_DIR>/state/mnemon-sync.json`,权限 `0600`,不写入 DSH 配置,也不进备份包。页面只回显掩码,回传掩码表示不修改。
49
31
 
50
32
  ## 自动备份
51
33
 
52
- 间隔与 dsh-mnemon 自己的 Git 备份保持一致,两项功能读起来一样:
53
-
54
34
  关闭 · 每小时 · 每 3 / 6 / 12 小时 · 每天 · 每 3 天 · 每 7 天
55
35
 
56
- 调度是一个 Host 内唯一的 `setTimeout` 自循环,从 `finally` 里重新排期。每次触发前重新读配置,改间隔不用重启;运行中再次触发会被丢弃而不是排队;失败只记录不抛出。
36
+ ## 恢复
57
37
 
58
- ## 安装
38
+ 从备份列表查看明细,选择要恢复的组件后执行;缺少的组件会明确提示。恢复默认只做预览,确认后才写盘。
59
39
 
60
- ```sh
61
- git clone https://github.com/summerscar/dsh-mnemon-sync.git
62
- ```
63
-
64
- 在 DSH 里用插件管理器安装这个目录,或直接加入 profile 的依赖:
65
-
66
- ```sh
67
- dsh plugin --profile web add /path/to/dsh-mnemon-sync
68
- ```
69
-
70
- 需要在 profile 里启用,并确保 `mnemon.sync` 这一层出现在 `memoryTopology.layers` 中。插件会注册一个最小 Source 来承载自己的页面 —— dsh-mnemon 只会为真实存在的 Source 渲染标签页,且标签高亮按 `sourceTypeId` 分组,借用别人的类型会导致两个标签同时点亮。
71
-
72
- ## 目录结构
73
-
74
- ```
75
- index.js Host 入口:HTTP 接口、调度装配、Source 注册
76
- source.js 承载页面身份的最小 Source(不存记忆)
77
- snapshot.js 组件识别、数据库在线备份、manifest 与校验
78
- scheduler.js 定时器与运行历史
79
- store.js 设置存取(0600)与规范化
80
- targets.js S3 SigV4 / WebDAV 的上传、下载、列目录、删除
81
- zip.js 零依赖 ZIP 读写(node:zlib)
82
- client.js 浏览器端页面与标签注册
83
- ```
40
+ 数据库用 `node:sqlite` 的 `backup()` 在线备份,副本一致;写回前会删掉同名 `-wal` / `-shm`,避免 SQLite 重放不存在的事务。每个文件都有 SHA-256 校验。
84
41
 
85
42
  ## 许可
86
43
 
package/client.js CHANGED
@@ -40,8 +40,8 @@ window.__ModuleLoader__.load({
40
40
 
41
41
  const zh = {
42
42
  tabLabel: '同步备份',
43
- title: '记忆空间同步备份',
44
- subtitle: '把 Mnemon 的记忆组件打包上传到 S3 或 WebDAV',
43
+ title: '同步备份',
44
+ subtitle: '把选定的记忆组件打包备份到 S3 兼容存储或 WebDAV,并可随时查看备份列表与恢复。',
45
45
  config: '备份配置',
46
46
  configNote: '以下三项一起保存。',
47
47
  scope: '备份范围',
@@ -144,8 +144,8 @@ window.__ModuleLoader__.load({
144
144
 
145
145
  const en = {
146
146
  tabLabel: 'Sync backup',
147
- title: 'Memory space sync backup',
148
- subtitle: 'Pack Mnemon\u2019s memory components to S3 or WebDAV',
147
+ title: 'Sync backup',
148
+ subtitle: 'Back up selected memory components to S3-compatible storage or WebDAV, with an archive list and restore.',
149
149
  config: 'Backup settings',
150
150
  configNote: 'All three sections below save together.',
151
151
  scope: 'Backup scope',
package/cordis.patch.yml CHANGED
@@ -1,3 +1,11 @@
1
+ # Host entry: HTTP API, scheduler and backup engine. Needs only webServer.
1
2
  - insert:
2
3
  - id: mnemon-sync
3
4
  name: '@summerscar/dsh-mnemon-sync'
5
+ # Memory Source entry, mounted exactly like the shipped dsh-mnemon-source-*
6
+ # packages: only id + name here, and the module itself exports
7
+ # `inject = ['mnemonMemory']` so the Loader waits for that service.
8
+ # Kept as a separate Entry so a profile without dsh-mnemon still gets a
9
+ # working backup engine (just no tab).
10
+ - id: mnemon-sync-source
11
+ name: '@summerscar/dsh-mnemon-sync/source'
package/index.js CHANGED
@@ -14,14 +14,13 @@
14
14
  * resident process, which is both simpler and compatible with the store.
15
15
  *
16
16
  * The browser half only ever sees JSON: credentials are masked on the way out
17
- * and are never echoed back by a read.
17
+ * and are never echoed back by a read. The memory Source that owns the page is
18
+ * a separate Cordis entry (./source.js), mounted by the bundle patch.
18
19
  */
19
20
 
20
21
  import { createHash } from 'node:crypto'
21
22
  import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs'
22
- import { createRequire } from 'node:module'
23
23
  import { dirname, join, resolve } from 'node:path'
24
- import { pathToFileURL } from 'node:url'
25
24
 
26
25
  import { SyncHistory, SyncScheduler } from './scheduler.js'
27
26
  import {
@@ -165,7 +164,6 @@ export function apply(ctx, config = {}) {
165
164
  // — not UI contributions. Standing up this minimal Source is what lets the
166
165
  // page use its own type instead of borrowing `memory-spaces`, which is the
167
166
  // only way its tab can highlight independently.
168
- ctx.effect(() => registerSyncSource(ctx, dataRoot), 'mnemon-sync: memory source')
169
167
 
170
168
  ctx.effect(() => ctx.webServer.register({
171
169
  kind: 'prefix',
@@ -582,104 +580,6 @@ function restoreMaskedSecrets(incoming, previous) {
582
580
  return incoming
583
581
  }
584
582
 
585
- /**
586
- * Resolve the dsh-mnemon extension SDK.
587
- *
588
- * A bare `import('dsh-mnemon/extension-sdk')` is not enough here. Installed
589
- * from npm the package sits at `<profile>/node_modules/@summerscar/...`, so
590
- * Node's own ancestor walk would find dsh-mnemon beside it — but a *linked*
591
- * checkout lives outside the profile with no `node_modules` of its own, and
592
- * then the walk finds nothing.
593
- *
594
- * Both install shapes are therefore covered, cheapest first:
595
- * 1. From this module's own location, which is right for an npm install.
596
- * 2. From `DSH_PROFILE_DIR`, which is right for a linked checkout and is set
597
- * in every shell and Host process a profile launches.
598
- * 3. From `DSH_HOME/profiles/web`, for a Host booted without that variable.
599
- *
600
- * Returns `undefined` when dsh-mnemon is not installed at all, which is a
601
- * supported configuration: the backup keeps working without its page.
602
- */
603
- async function loadExtensionSdk() {
604
- // 1. Ask Node, anchored at this file. Resolves for an npm install.
605
- try {
606
- const resolver = createRequire(import.meta.url)
607
- const resolved = resolver.resolve('dsh-mnemon/lib/extension-sdk.js')
608
- return await import(pathToFileURL(resolved).href)
609
- } catch {
610
- // Not resolvable from here: fall through to the profile-anchored paths.
611
- }
612
-
613
- const candidates = []
614
- const profileDir = process.env.DSH_PROFILE_DIR?.trim()
615
- if (profileDir !== undefined && profileDir !== '') {
616
- candidates.push(join(profileDir, 'node_modules', 'dsh-mnemon', 'lib', 'extension-sdk.js'))
617
- }
618
- // The Host that loads this plugin lives in the installation's own tree.
619
- candidates.push(join(process.env.DSH_HOME ?? '', 'profiles', 'web', 'node_modules', 'dsh-mnemon', 'lib', 'extension-sdk.js'))
620
- for (const candidate of candidates) {
621
- if (candidate === '' || !existsSync(candidate)) continue
622
- return import(pathToFileURL(candidate).href)
623
- }
624
- return undefined
625
- }
626
-
627
- /**
628
- * Install the sync Source into dsh-mnemon's composable-memory service.
629
- *
630
- * Returns a disposer, and does nothing at all when the Host runs without that
631
- * service (a profile with dsh-mnemon absent).
632
- */
633
- function registerSyncSource(ctx, dataRoot) {
634
- if (typeof ctx.get !== 'function') return () => {}
635
- const service = ctx.get('mnemonMemory')
636
- if (service === undefined || service === null || typeof service.installContributions !== 'function') {
637
- return () => {}
638
- }
639
- let disposed = false
640
- let uninstall
641
- void (async () => {
642
- try {
643
- const sdk = await loadExtensionSdk()
644
- if (sdk === undefined || typeof sdk.defineMemorySource !== 'function') {
645
- console.warn('mnemon-sync: dsh-mnemon extension SDK not found; the sync tab stays hidden')
646
- return
647
- }
648
- const { createSyncSource, syncSourceManifest } = await import('./source.js')
649
- if (disposed) return
650
- // The manifest goes through the SDK validator so a contract drift in
651
- // dsh-mnemon fails loudly here instead of at composition time.
652
- const definition = sdk.defineMemorySource(createSyncSource({ ...syncSourceManifest }))
653
- uninstall = service.installContributions({
654
- plugin: {
655
- apiVersion: 'dsh-mnemon/plugin/v1',
656
- packageName: '@summerscar/dsh-mnemon-sync',
657
- label: { en: 'Sync backup', 'zh-CN': '同步备份' },
658
- description: {
659
- en: 'Archive memory spaces to S3 or WebDAV.',
660
- 'zh-CN': '把记忆空间备份到 S3 或 WebDAV。',
661
- },
662
- roles: ['source'],
663
- provides: [{ id: 'source' }],
664
- },
665
- sources: [definition],
666
- }, { instanceId: 'mnemon-sync' })
667
- if (disposed) {
668
- uninstall?.()
669
- uninstall = undefined
670
- }
671
- } catch (error) {
672
- // A Host that cannot accept the Source must not take the backup down.
673
- console.warn('mnemon-sync: could not register the sync source', error)
674
- }
675
- })()
676
- return () => {
677
- disposed = true
678
- uninstall?.()
679
- uninstall = undefined
680
- }
681
- }
682
-
683
583
  /** Read a staging tree into the flat `{ path, bytes }` entries the ZIP writer takes. */
684
584
  function collect(root, prefix = '') {
685
585
  const entries = []
package/locale/en.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "meta": {
3
- "title": "Memory space sync backup",
4
- "description": "Archive the whole Mnemon data directory — every memory space database — to S3-compatible storage or WebDAV, with minute/hour/day/clock scheduling and retention."
3
+ "title": "Sync backup",
4
+ "description": "Back up and restore dsh-mnemon memory components (memory spaces, project documents, runtime memory — pick any subset) to S3-compatible storage or WebDAV, with scheduled backups, an archive list and selective restore."
5
5
  }
6
6
  }
package/locale/zh.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "meta": {
3
- "title": "记忆空间同步备份",
4
- "description": "把整个 Mnemon data 目录(含全部记忆空间数据库)打包备份到 S3 兼容存储或 WebDAV,支持按分钟/小时/天/固定时刻调度与保留份数。"
3
+ "title": "同步备份",
4
+ "description": "备份与恢复 dsh-mnemon 的记忆组件(记忆空间、项目档案、运行时记忆,可任选)到 S3 兼容存储或 WebDAV,支持定时备份、备份列表与选择性恢复。"
5
5
  }
6
6
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@summerscar/dsh-mnemon-sync",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Back up and restore dsh-mnemon memory components (memory spaces, project documents, runtime memory) to S3-compatible storage or WebDAV.",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -8,7 +8,8 @@
8
8
  ".": "./index.js",
9
9
  "./client": "./client.js",
10
10
  "./locale/*.json": "./locale/*.json",
11
- "./icon": "./icon.svg"
11
+ "./icon": "./icon.svg",
12
+ "./source": "./source.js"
12
13
  },
13
14
  "files": [
14
15
  "index.js",
package/source.js CHANGED
@@ -1,34 +1,98 @@
1
1
  /**
2
2
  * Mnemon Sync — the Source that owns this page.
3
3
  *
4
+ * This module is a standard Cordis plugin entry, written the same way the
5
+ * shipped dsh-mnemon Source packages are (see dsh-mnemon-source-runtime):
6
+ * `inject = ['mnemonMemory']` and `installMemory(ctx, ...)` inside `apply`.
7
+ * The Loader waits for the service, calls apply when it appears, and disposes
8
+ * the registration when it goes away.
9
+ *
10
+ * One concession to being distributable: the SDK is loaded with `createRequire`
11
+ * instead of a static `import`. A static import resolves fine for an npm
12
+ * install (the package sits under `<profile>/node_modules/`, with dsh-mnemon
13
+ * as a sibling ancestor) but fails for a `link:` checkout that lives outside
14
+ * the profile and has no `node_modules` of its own. `createRequire` with an
15
+ * explicit `DSH_PROFILE_DIR` fallback covers both shapes.
16
+ *
4
17
  * Why a Source is required at all: dsh-mnemon renders a page tab only when its
5
- * `sourceTypeId` appears in the live source catalog, and the catalog lists
18
+ * `sourceTypeId` appears in the live source catalog, and that catalog lists
6
19
  * real, running Source implementations. A UI-only contribution can borrow
7
20
  * `memory-spaces` and be shown, but the tab bar highlights *every* tab sharing
8
21
  * the selected page's type prefix, so borrowing means clicking one lights up
9
22
  * the other. Declaring our own type is the only way to get an independent tab.
10
23
  *
11
24
  * This Source is deliberately honest about what it holds. It is not a memory
12
- * backend: it stores no insights, so it contributes an empty projection and
13
- * declares no read routes or write actions. Its role is to be the identity the
14
- * sync page hangs from, which is why every capability is false and the facts
15
- * report only what the backup configuration currently is.
16
- *
17
- * The alternative — faking routes that answer with nothing — would pollute the
18
- * model's memory view with an empty source and could make a composition look
19
- * incomplete. An empty, clearly-labelled source is the smaller lie.
25
+ * backend: it stores no insights, contributes an empty projection and declares
26
+ * no read routes or write actions. Its role is to be the identity the sync
27
+ * page hangs from.
20
28
  */
21
29
 
22
30
  import { createHash } from 'node:crypto'
31
+ import { existsSync } from 'node:fs'
32
+ import { createRequire } from 'node:module'
23
33
  import { join } from 'node:path'
24
34
 
25
35
  import { resolveDataRoot } from './index.js'
26
36
  import { inspectHome } from './snapshot.js'
27
37
  import { readSettings } from './store.js'
28
38
 
39
+ const require = createRequire(import.meta.url)
40
+
29
41
  export const SYNC_SOURCE_TYPE_ID = 'mnemon-sync'
30
42
  export const SYNC_PACKAGE_NAME = '@summerscar/dsh-mnemon-sync'
31
43
 
44
+ export const name = SYNC_PACKAGE_NAME
45
+ // The one dependency the Source needs. The Loader holds this Entry until
46
+ // dsh-mnemon publishes mnemonMemory, then mounts it.
47
+ export const inject = ['mnemonMemory']
48
+
49
+ /**
50
+ * Resolve the extension SDK synchronously so `apply` can stay synchronous.
51
+ * Tries normal package resolution first, then the profile-anchored absolute
52
+ * path for linked checkouts.
53
+ */
54
+ function loadSdk() {
55
+ try {
56
+ return require('dsh-mnemon/extension-sdk')
57
+ } catch {
58
+ // Fall through: linked checkout has no ancestor node_modules.
59
+ }
60
+ const profileDir = process.env.DSH_PROFILE_DIR?.trim()
61
+ const candidates = []
62
+ if (profileDir !== undefined && profileDir !== '') {
63
+ candidates.push(join(profileDir, 'node_modules', 'dsh-mnemon', 'lib', 'extension-sdk.js'))
64
+ }
65
+ candidates.push(join(process.env.DSH_HOME ?? '', 'profiles', 'web', 'node_modules', 'dsh-mnemon', 'lib', 'extension-sdk.js'))
66
+ for (const candidate of candidates) {
67
+ if (candidate === '' || !existsSync(candidate)) continue
68
+ return require(candidate)
69
+ }
70
+ return undefined
71
+ }
72
+
73
+ const sdk = loadSdk()
74
+
75
+ /** The plugin descriptor, validated by the SDK when available. */
76
+ export const memoryPlugin = sdk === undefined ? {
77
+ packageName: SYNC_PACKAGE_NAME,
78
+ label: { en: 'Sync backup', 'zh-CN': '同步备份' },
79
+ description: {
80
+ en: 'Back up and restore dsh-mnemon memory components.',
81
+ 'zh-CN': '备份与恢复 dsh-mnemon 的记忆组件。',
82
+ },
83
+ roles: ['source'],
84
+ provides: [{ id: 'source' }],
85
+ } : sdk.defineMemoryPlugin({
86
+ packageName: SYNC_PACKAGE_NAME,
87
+ label: { en: 'Sync backup', 'zh-CN': '同步备份' },
88
+ description: {
89
+ en: 'Back up and restore dsh-mnemon memory components.',
90
+ 'zh-CN': '备份与恢复 dsh-mnemon 的记忆组件。',
91
+ },
92
+ roles: ['source'],
93
+ provides: [{ id: 'source' }],
94
+ })
95
+
32
96
  /** The manifest Core validates before this Source may take part in a View. */
33
97
  export const syncSourceManifest = {
34
98
  apiVersion: 'dsh-mnemon/v1',
@@ -43,30 +107,21 @@ export const syncSourceManifest = {
43
107
  consistency: 'exact-snapshot',
44
108
  routes: [],
45
109
  actions: [],
46
- // A management descriptor is not decoration. dsh-mnemon builds its Source
47
- // catalog from installed Sources that declare one, and the workbench decides
48
- // a layer is "running" by whether its type is in that catalog — so a Source
49
- // without `management` is installed but reported as "on yet not running".
50
- // Fields stay empty: this page does its own editing.
110
+ // A management descriptor is required for managementCatalog to list this
111
+ // Source; without it the Source is installed but reported "on yet not running".
51
112
  management: {
52
113
  label: 'Sync backup',
53
- description: 'Archive memory spaces to S3 or WebDAV.',
114
+ description: 'Back up and restore dsh-mnemon memory components.',
54
115
  },
55
116
  }
56
117
 
57
118
  /**
58
119
  * Build the Source definition.
59
120
  *
60
- * `facts()` must return the Host's *facts envelope* — `sourceInstanceKey`,
121
+ * `facts()` returns the Host's facts envelope — `sourceInstanceKey`,
61
122
  * `sourceTypeId`, `role`, `availability`, `revision`, `capabilities`,
62
- * `routeIds`, `actionIds` (plus optional `hints`). dsh-mnemon rejects anything
63
- * else: `normalizeFacts()` compares `facts.sourceInstanceKey` against the
64
- * registry key and throws `memory Source facts instance mismatch` when it is
65
- * missing, which is exactly what a `{ summary, details }` shape produced.
66
- *
67
- * `hints` is the only free-form channel that survives normalization, so the
68
- * backup configuration a human may want to see travels there. `project()`
69
- * returns `{ fragments: [] }`: there is no memory content here to contribute.
123
+ * `routeIds`, `actionIds` (plus optional `hints`). `project()` brings nothing
124
+ * into a prompt, because there is no memory content here to bring.
70
125
  */
71
126
  export function createSyncSource(manifest = syncSourceManifest) {
72
127
  return {
@@ -81,21 +136,18 @@ export function createSyncSource(manifest = syncSourceManifest) {
81
136
  return {
82
137
  facts() {
83
138
  const settings = safeSettings(home)
84
- const inspected = safeInspect(dataRoot)
139
+ const inspected = safeInspect(home)
85
140
  return {
86
141
  sourceInstanceKey: context.sourceInstanceKey,
87
142
  sourceTypeId: manifest.typeId,
88
143
  role: manifest.role,
89
- // This Source is a maintenance identity, not a memory backend: it
90
- // is always ready and holds nothing a View could select.
91
144
  availability: 'ready',
92
145
  revision: configurationRevision(settings, inspected),
93
146
  capabilities: [],
94
147
  routeIds: [],
95
148
  actionIds: [],
96
- // Facts are JSON only and must stay small: metadata the Host may
97
- // surface, never memory content. Only `hints` survives
98
- // normalization, so anything worth showing goes here.
149
+ // Only `hints` survives normalization, so the backup configuration
150
+ // a human may want to see travels there.
99
151
  hints: {
100
152
  summary: settings.enabled
101
153
  ? `Sync backup every ${String(settings.autoSyncMinutes)} min to ${describeTarget(settings.target)}`
@@ -114,7 +166,6 @@ export function createSyncSource(manifest = syncSourceManifest) {
114
166
  }
115
167
  },
116
168
  project() {
117
- // Nothing to inject: this Source holds no memories to contribute.
118
169
  return { fragments: [] }
119
170
  },
120
171
  dispose() {},
@@ -123,13 +174,23 @@ export function createSyncSource(manifest = syncSourceManifest) {
123
174
  }
124
175
  }
125
176
 
177
+ /** The standard Cordis entry point, identical in shape to the shipped Sources. */
178
+ export function apply(ctx, config = {}) {
179
+ if (sdk === undefined) {
180
+ // dsh-mnemon is not installed at all; the backup engine in index.js still
181
+ // works, only the page is unavailable. Do not fake a registration.
182
+ console.warn('mnemon-sync: dsh-mnemon extension SDK not found; the sync tab stays hidden')
183
+ return
184
+ }
185
+ sdk.installMemory(ctx, {
186
+ plugin: memoryPlugin,
187
+ sources: [sdk.defineMemorySource(createSyncSource({ ...syncSourceManifest }))],
188
+ }, config?.instanceId === undefined ? undefined : { instanceId: config.instanceId })
189
+ }
190
+
126
191
  /**
127
- * A stable revision for the current backup configuration.
128
- *
129
- * The Host requires a non-empty string and compares it across a turn, so this
130
- * must change when the configuration changes and stay equal otherwise. It is
131
- * derived from the facts themselves rather than a clock, which keeps
132
- * composition deterministic.
192
+ * A stable revision for the current backup configuration, derived from the
193
+ * facts themselves rather than a clock so composition stays deterministic.
133
194
  */
134
195
  function configurationRevision(settings, inspected) {
135
196
  const material = JSON.stringify({