dsh-mnemon 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/README.en.md +31 -2
  2. package/README.md +31 -2
  3. package/SECURITY.md +89 -0
  4. package/docs/en/README.md +1 -0
  5. package/docs/en/configuration.md +6 -1
  6. package/docs/en/getting-started.md +19 -3
  7. package/docs/en/interfaces.md +12 -0
  8. package/docs/en/operations.md +6 -2
  9. package/docs/zh-CN/README.md +1 -0
  10. package/docs/zh-CN/configuration.md +6 -1
  11. package/docs/zh-CN/getting-started.md +19 -3
  12. package/docs/zh-CN/interfaces.md +12 -0
  13. package/docs/zh-CN/operations.md +6 -2
  14. package/lib/client.js +2164 -624
  15. package/lib/client.js.map +1 -1
  16. package/lib/index.js +3608 -2457
  17. package/lib/types/activity.d.ts +32 -0
  18. package/lib/types/activity.d.ts.map +1 -0
  19. package/lib/types/activity.js +93 -0
  20. package/lib/types/activity.js.map +1 -0
  21. package/lib/types/client/MnemonPackSection.d.ts +11 -0
  22. package/lib/types/client/MnemonPackSection.d.ts.map +1 -0
  23. package/lib/types/client/MnemonPackSection.js +136 -0
  24. package/lib/types/client/MnemonPackSection.js.map +1 -0
  25. package/lib/types/client/MnemonSaveAction.d.ts +13 -0
  26. package/lib/types/client/MnemonSaveAction.d.ts.map +1 -0
  27. package/lib/types/client/MnemonSaveAction.js +141 -0
  28. package/lib/types/client/MnemonSaveAction.js.map +1 -0
  29. package/lib/types/client/MnemonSettingsCard.d.ts +11 -3
  30. package/lib/types/client/MnemonSettingsCard.d.ts.map +1 -1
  31. package/lib/types/client/MnemonSettingsCard.js +112 -48
  32. package/lib/types/client/MnemonSettingsCard.js.map +1 -1
  33. package/lib/types/client/MnemonToolviews.d.ts +19 -0
  34. package/lib/types/client/MnemonToolviews.d.ts.map +1 -0
  35. package/lib/types/client/MnemonToolviews.js +172 -0
  36. package/lib/types/client/MnemonToolviews.js.map +1 -0
  37. package/lib/types/client/MnemonTurnTail.d.ts +18 -0
  38. package/lib/types/client/MnemonTurnTail.d.ts.map +1 -0
  39. package/lib/types/client/MnemonTurnTail.js +44 -0
  40. package/lib/types/client/MnemonTurnTail.js.map +1 -0
  41. package/lib/types/client/MnemonView.d.ts.map +1 -1
  42. package/lib/types/client/MnemonView.js +19 -8
  43. package/lib/types/client/MnemonView.js.map +1 -1
  44. package/lib/types/client/anchor.d.ts +28 -0
  45. package/lib/types/client/anchor.d.ts.map +1 -0
  46. package/lib/types/client/anchor.js +46 -0
  47. package/lib/types/client/anchor.js.map +1 -0
  48. package/lib/types/client/api.d.ts +14 -1
  49. package/lib/types/client/api.d.ts.map +1 -1
  50. package/lib/types/client/api.js +56 -3
  51. package/lib/types/client/api.js.map +1 -1
  52. package/lib/types/client/dsh-compat.d.ts +21 -0
  53. package/lib/types/client/dsh-compat.d.ts.map +1 -0
  54. package/lib/types/client/dsh-compat.js +2 -0
  55. package/lib/types/client/dsh-compat.js.map +1 -0
  56. package/lib/types/client/index.d.ts.map +1 -1
  57. package/lib/types/client/index.js +131 -16
  58. package/lib/types/client/index.js.map +1 -1
  59. package/lib/types/client/locales.d.ts +252 -4
  60. package/lib/types/client/locales.d.ts.map +1 -1
  61. package/lib/types/client/locales.js +268 -20
  62. package/lib/types/client/locales.js.map +1 -1
  63. package/lib/types/client/settings.d.ts +10 -5
  64. package/lib/types/client/settings.d.ts.map +1 -1
  65. package/lib/types/client/settings.js +32 -11
  66. package/lib/types/client/settings.js.map +1 -1
  67. package/lib/types/config.d.ts +37 -1
  68. package/lib/types/config.d.ts.map +1 -1
  69. package/lib/types/config.js +78 -6
  70. package/lib/types/config.js.map +1 -1
  71. package/lib/types/contracts.d.ts +17 -38
  72. package/lib/types/contracts.d.ts.map +1 -1
  73. package/lib/types/guidance.d.ts +2 -1
  74. package/lib/types/guidance.d.ts.map +1 -1
  75. package/lib/types/guidance.js +6 -2
  76. package/lib/types/guidance.js.map +1 -1
  77. package/lib/types/index.d.ts +4 -2
  78. package/lib/types/index.d.ts.map +1 -1
  79. package/lib/types/index.js +27 -18
  80. package/lib/types/index.js.map +1 -1
  81. package/lib/types/lifecycle.d.ts +14 -1
  82. package/lib/types/lifecycle.d.ts.map +1 -1
  83. package/lib/types/lifecycle.js +81 -12
  84. package/lib/types/lifecycle.js.map +1 -1
  85. package/lib/types/live-runtime.d.ts +42 -0
  86. package/lib/types/live-runtime.d.ts.map +1 -0
  87. package/lib/types/live-runtime.js +76 -0
  88. package/lib/types/live-runtime.js.map +1 -0
  89. package/lib/types/memory-bodies.d.ts +2 -0
  90. package/lib/types/memory-bodies.d.ts.map +1 -1
  91. package/lib/types/memory-bodies.js +29 -10
  92. package/lib/types/memory-bodies.js.map +1 -1
  93. package/lib/types/pack.d.ts +74 -0
  94. package/lib/types/pack.d.ts.map +1 -0
  95. package/lib/types/pack.js +709 -0
  96. package/lib/types/pack.js.map +1 -0
  97. package/lib/types/rpc.d.ts +5 -1
  98. package/lib/types/rpc.d.ts.map +1 -1
  99. package/lib/types/rpc.js +47 -5
  100. package/lib/types/rpc.js.map +1 -1
  101. package/lib/types/runner.d.ts +2 -0
  102. package/lib/types/runner.d.ts.map +1 -1
  103. package/lib/types/runner.js +5 -0
  104. package/lib/types/runner.js.map +1 -1
  105. package/lib/types/settings.d.ts +1 -0
  106. package/lib/types/settings.d.ts.map +1 -1
  107. package/lib/types/settings.js +45 -16
  108. package/lib/types/settings.js.map +1 -1
  109. package/lib/types/tools.js +4 -2
  110. package/lib/types/tools.js.map +1 -1
  111. package/package.json +23 -12
@@ -0,0 +1,709 @@
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import { closeSync, copyFileSync, existsSync, fstatSync, lstatSync, mkdirSync, openSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync, } from 'node:fs';
3
+ import { basename, dirname, join, resolve, sep } from 'node:path';
4
+ import { homedir } from 'node:os';
5
+ import { strFromU8, strToU8, unzipSync, zipSync } from 'fflate';
6
+ import { DOCUMENTS_ACTIVE_LIMIT_BYTES, DOCUMENTS_VERSION } from "./documents.js";
7
+ import { RUNTIME_ENTRY_DELIMITER, RUNTIME_MEMORY_LIMITS, RUNTIME_MEMORY_VERSION } from "./runtime-memory.js";
8
+ export const MNEMON_PACK_FORMAT = 'mnemonpack';
9
+ export const MNEMON_PACK_VERSION = 1;
10
+ export const MNEMON_PACK_MIME = 'application/zip';
11
+ export const MNEMON_PACK_MAX_ARCHIVE_BYTES = 48 * 1024 * 1024;
12
+ export const MNEMON_PACK_MAX_EXPANDED_BYTES = 256 * 1024 * 1024;
13
+ const MAX_FILE_BYTES = 128 * 1024 * 1024;
14
+ const MAX_FILES = 4096;
15
+ const LOCK_TIMEOUT_MS = 5_000;
16
+ const LOCK_STALE_MS = 30_000;
17
+ const LOCK_RETRY_MS = 20;
18
+ const COMPONENT_DIRECTORIES = { runtime: 'runtime', documents: 'documents', 'memory-spaces': 'data' };
19
+ const COMPONENT_ORDER = ['runtime', 'documents', 'memory-spaces'];
20
+ const BODY_ID = /^[a-zA-Z0-9][a-zA-Z0-9_-]*$/;
21
+ const SQLITE_HEADER = Buffer.from('SQLite format 3\0', 'binary');
22
+ function record(value) {
23
+ return typeof value === 'object' && value !== null && !Array.isArray(value) ? value : undefined;
24
+ }
25
+ function utf8(bytes, label) {
26
+ try {
27
+ return new TextDecoder('utf-8', { fatal: true }).decode(bytes);
28
+ }
29
+ catch {
30
+ throw new Error(`${label} is not valid UTF-8`);
31
+ }
32
+ }
33
+ function json(bytes, label) {
34
+ try {
35
+ return JSON.parse(utf8(bytes, label));
36
+ }
37
+ catch (error) {
38
+ if (error instanceof SyntaxError)
39
+ throw new Error(`${label} is not valid JSON`);
40
+ throw error;
41
+ }
42
+ }
43
+ function sha256(value) {
44
+ return createHash('sha256').update(value).digest('hex');
45
+ }
46
+ function safeArchivePath(path) {
47
+ if (path === '' || path.length > 512 || path.includes('\0') || path.includes('\\') || path.startsWith('/') || /^[a-zA-Z]:/.test(path)) {
48
+ throw new Error(`unsafe Pack entry path: ${JSON.stringify(path)}`);
49
+ }
50
+ const parts = path.split('/');
51
+ if (parts.some(part => part === '' || part === '.' || part === '..'))
52
+ throw new Error(`unsafe Pack entry path: ${JSON.stringify(path)}`);
53
+ }
54
+ function payloadComponent(path) {
55
+ if (path.startsWith('payload/runtime/'))
56
+ return 'runtime';
57
+ if (path.startsWith('payload/documents/'))
58
+ return 'documents';
59
+ if (path.startsWith('payload/data/'))
60
+ return 'memory-spaces';
61
+ return undefined;
62
+ }
63
+ function allowedPayloadPath(path) {
64
+ if (path === 'payload/runtime/memories.json' || path === 'payload/runtime/USER.md' || path === 'payload/runtime/MEMORY.md')
65
+ return true;
66
+ if (path === 'payload/documents/index.json')
67
+ return true;
68
+ if (/^payload\/documents\/(active|archived)\/[a-zA-Z0-9._-]+\.md$/u.test(path))
69
+ return true;
70
+ if (path === 'payload/data/.dsh-memory-bodies.json')
71
+ return true;
72
+ return /^payload\/data\/[a-zA-Z0-9][a-zA-Z0-9_-]*\/mnemon\.db$/u.test(path);
73
+ }
74
+ function componentsForScope(scope) {
75
+ if (scope === 'full')
76
+ return [...COMPONENT_ORDER];
77
+ if (!COMPONENT_ORDER.includes(scope))
78
+ throw new Error('Mnemon Pack scope must be full, runtime, documents, or memory-spaces');
79
+ return [scope];
80
+ }
81
+ function parseManifest(value) {
82
+ const manifest = record(value);
83
+ if (manifest?.format !== MNEMON_PACK_FORMAT || manifest.version !== MNEMON_PACK_VERSION)
84
+ throw new Error('unsupported Mnemon Pack format or version');
85
+ if (manifest.scope !== 'full' && !COMPONENT_ORDER.includes(manifest.scope))
86
+ throw new Error('Mnemon Pack scope is invalid');
87
+ if (typeof manifest.exportedAt !== 'string')
88
+ throw new Error('Mnemon Pack exportedAt is invalid');
89
+ if (!Array.isArray(manifest.components))
90
+ throw new Error('Mnemon Pack components are invalid');
91
+ const components = manifest.components.map(String);
92
+ if (components.length === 0 || new Set(components).size !== components.length || components.some(component => !COMPONENT_ORDER.includes(component))) {
93
+ throw new Error('Mnemon Pack components are invalid');
94
+ }
95
+ const expected = componentsForScope(manifest.scope);
96
+ if (components.length !== expected.length || expected.some(component => !components.includes(component)))
97
+ throw new Error('Mnemon Pack scope does not match its components');
98
+ const source = record(manifest.source);
99
+ if (source?.plugin !== 'dsh-mnemon' || typeof source.pluginVersion !== 'string')
100
+ throw new Error('Mnemon Pack source is invalid');
101
+ if (!Array.isArray(manifest.summary))
102
+ throw new Error('Mnemon Pack summary is invalid');
103
+ const summary = manifest.summary.map((entry) => {
104
+ const item = record(entry);
105
+ const component = item?.component;
106
+ if (!COMPONENT_ORDER.includes(component) || !Number.isSafeInteger(item?.files) || !Number.isSafeInteger(item?.bytes) || !Number.isSafeInteger(item?.items)
107
+ || Number(item?.files) < 0 || Number(item?.bytes) < 0 || Number(item?.items) < 0) {
108
+ throw new Error('Mnemon Pack summary entry is invalid');
109
+ }
110
+ return { component, files: Number(item.files), bytes: Number(item.bytes), items: Number(item.items) };
111
+ });
112
+ return {
113
+ format: MNEMON_PACK_FORMAT,
114
+ version: MNEMON_PACK_VERSION,
115
+ scope: manifest.scope,
116
+ exportedAt: manifest.exportedAt,
117
+ source: { plugin: 'dsh-mnemon', pluginVersion: source.pluginVersion },
118
+ components,
119
+ summary,
120
+ };
121
+ }
122
+ function decodeArchive(base64) {
123
+ if (typeof base64 !== 'string' || base64 === '' || base64.length > Math.ceil(MNEMON_PACK_MAX_ARCHIVE_BYTES / 3) * 4 + 4)
124
+ throw new Error('Mnemon Pack archive is empty or too large');
125
+ if (!/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/u.test(base64))
126
+ throw new Error('Mnemon Pack payload is not valid base64');
127
+ const bytes = Buffer.from(base64, 'base64');
128
+ if (bytes.length === 0 || bytes.length > MNEMON_PACK_MAX_ARCHIVE_BYTES)
129
+ throw new Error('Mnemon Pack archive is empty or too large');
130
+ return bytes;
131
+ }
132
+ function parseArchive(base64) {
133
+ const archive = decodeArchive(base64);
134
+ let count = 0;
135
+ let expandedBytes = 0;
136
+ const files = unzipSync(archive, {
137
+ filter(info) {
138
+ safeArchivePath(info.name);
139
+ if (info.name.endsWith('/'))
140
+ return false;
141
+ count += 1;
142
+ expandedBytes += info.originalSize;
143
+ if (count > MAX_FILES)
144
+ throw new Error(`Mnemon Pack contains more than ${MAX_FILES} files`);
145
+ if (info.originalSize > MAX_FILE_BYTES || expandedBytes > MNEMON_PACK_MAX_EXPANDED_BYTES)
146
+ throw new Error('Mnemon Pack expanded data exceeds the safety limit');
147
+ return true;
148
+ },
149
+ });
150
+ const names = Object.keys(files);
151
+ if (!names.includes('manifest.json') || !names.includes('checksums.json'))
152
+ throw new Error('Mnemon Pack is missing manifest.json or checksums.json');
153
+ for (const path of names) {
154
+ safeArchivePath(path);
155
+ if (path !== 'manifest.json' && path !== 'checksums.json' && !allowedPayloadPath(path))
156
+ throw new Error(`unsupported Mnemon Pack entry: ${path}`);
157
+ }
158
+ const manifest = parseManifest(json(files['manifest.json'], 'manifest.json'));
159
+ const checksumValue = record(json(files['checksums.json'], 'checksums.json'));
160
+ const checksumFiles = record(checksumValue?.files);
161
+ if (checksumValue?.algorithm !== 'sha256' || checksumFiles === undefined)
162
+ throw new Error('Mnemon Pack checksums are invalid');
163
+ const payloadNames = names.filter(path => path.startsWith('payload/')).sort();
164
+ if (Object.keys(checksumFiles).length !== payloadNames.length)
165
+ throw new Error('Mnemon Pack checksum inventory does not match the payload');
166
+ for (const path of payloadNames) {
167
+ const component = payloadComponent(path);
168
+ if (component === undefined || !manifest.components.includes(component))
169
+ throw new Error(`Mnemon Pack payload is outside its declared components: ${path}`);
170
+ const expected = checksumFiles[path];
171
+ if (typeof expected !== 'string' || expected !== sha256(files[path]))
172
+ throw new Error(`Mnemon Pack checksum mismatch: ${path}`);
173
+ }
174
+ validatePackPayload(files, manifest.components);
175
+ const actualSummary = summaryFor(manifest.components, files);
176
+ return { archiveBytes: archive.length, expandedBytes, files, manifest: { ...manifest, summary: actualSummary } };
177
+ }
178
+ function parseRuntime(value) {
179
+ const source = record(value);
180
+ if (source?.version !== RUNTIME_MEMORY_VERSION || !Array.isArray(source.entries))
181
+ throw new Error('runtime memories.json is invalid');
182
+ const entries = source.entries.map((raw) => {
183
+ const entry = record(raw);
184
+ if (typeof entry?.content !== 'string' || (entry.target !== 'memory' && entry.target !== 'user') || !['critical', 'normal', 'low'].includes(String(entry.importance))) {
185
+ throw new Error('runtime memories.json contains an invalid entry');
186
+ }
187
+ if (typeof entry.created_at !== 'string' || typeof entry.updated_at !== 'string')
188
+ throw new Error('runtime memories.json contains invalid timestamps');
189
+ const content = entry.content.trim().replace(/\s+/gu, ' ');
190
+ if (content === '' || content.includes('§') || Buffer.byteLength(content, 'utf8') > 8 * 1024)
191
+ throw new Error('runtime memories.json contains invalid content');
192
+ return { content, target: entry.target, importance: entry.importance, created_at: entry.created_at, updated_at: entry.updated_at };
193
+ });
194
+ for (const target of ['user', 'memory']) {
195
+ const used = runtimeBytes(entries, target);
196
+ if (used > RUNTIME_MEMORY_LIMITS[target])
197
+ throw new Error(`runtime ${target} memory exceeds its ${RUNTIME_MEMORY_LIMITS[target]} byte limit`);
198
+ }
199
+ return { version: 1, entries };
200
+ }
201
+ function runtimeBytes(entries, target) {
202
+ return Buffer.byteLength(entries.filter(entry => entry.target === target).map(entry => entry.content).join(RUNTIME_ENTRY_DELIMITER), 'utf8');
203
+ }
204
+ function runtimeProjection(entries, target) {
205
+ const content = entries.filter(entry => entry.target === target).map(entry => entry.content).join(RUNTIME_ENTRY_DELIMITER);
206
+ return content === '' ? '' : `${content}\n`;
207
+ }
208
+ function parseDocumentIndex(value) {
209
+ const index = record(value);
210
+ if (index?.version !== DOCUMENTS_VERSION || !Array.isArray(index.documents))
211
+ throw new Error('Documents index.json is invalid');
212
+ const ids = new Set();
213
+ const documents = index.documents.map((raw) => {
214
+ const item = record(raw);
215
+ if (typeof item?.id !== 'string' || item.id.trim() === '' || ids.has(item.id))
216
+ throw new Error('Documents index contains an invalid or duplicate id');
217
+ if (typeof item.title !== 'string' || typeof item.description !== 'string' || (item.status !== 'active' && item.status !== 'archived'))
218
+ throw new Error('Documents index contains an invalid record');
219
+ if (typeof item.filename !== 'string' || basename(item.filename) !== item.filename || !/^[a-zA-Z0-9._-]+\.md$/u.test(item.filename))
220
+ throw new Error('Documents index contains an unsafe filename');
221
+ if (typeof item.createdAt !== 'string' || typeof item.updatedAt !== 'string' || typeof item.lastAccessedAt !== 'string' || !Number.isSafeInteger(item.revision) || typeof item.contentHash !== 'string' || !Number.isSafeInteger(item.sizeBytes))
222
+ throw new Error('Documents index contains invalid metadata');
223
+ if (!Array.isArray(item.sourcePaths) || !Array.isArray(item.sessionIds) || !Array.isArray(item.memoryBodyIds))
224
+ throw new Error('Documents index contains invalid lists');
225
+ ids.add(item.id);
226
+ return {
227
+ id: item.id, title: item.title, description: item.description, status: item.status, filename: item.filename,
228
+ relativePath: `documents/${item.status}/${item.filename}`,
229
+ sourcePaths: item.sourcePaths.filter((entry) => typeof entry === 'string'),
230
+ sessionIds: item.sessionIds.filter((entry) => typeof entry === 'string'),
231
+ createdAt: item.createdAt, updatedAt: item.updatedAt, lastAccessedAt: item.lastAccessedAt,
232
+ revision: Number(item.revision), contentHash: item.contentHash, sizeBytes: Number(item.sizeBytes),
233
+ ...(typeof item.archivedAt === 'string' ? { archivedAt: item.archivedAt } : {}),
234
+ ...(typeof item.archiveSummary === 'string' ? { archiveSummary: item.archiveSummary } : {}),
235
+ memoryBodyIds: item.memoryBodyIds.filter((entry) => typeof entry === 'string'),
236
+ };
237
+ });
238
+ return { version: 1, documents };
239
+ }
240
+ function parseRegistry(value) {
241
+ const registry = record(value);
242
+ if (registry?.version !== 1 || !Array.isArray(registry.bodies))
243
+ throw new Error('Memory Space registry is invalid');
244
+ const ids = new Set();
245
+ const bodies = registry.bodies.map((raw) => {
246
+ const body = record(raw);
247
+ if (typeof body?.id !== 'string' || !BODY_ID.test(body.id) || ids.has(body.id))
248
+ throw new Error('Memory Space registry contains an invalid or duplicate id');
249
+ if (typeof body.name !== 'string' || body.name.trim() === '' || body.name.length > 100 || typeof body.description !== 'string' || body.description.length > 1000)
250
+ throw new Error('Memory Space registry contains invalid metadata');
251
+ if (typeof body.createdAt !== 'string' || typeof body.updatedAt !== 'string')
252
+ throw new Error('Memory Space registry contains invalid timestamps');
253
+ ids.add(body.id);
254
+ return { id: body.id, name: body.name, description: body.description, active: body.active === true, createdAt: body.createdAt, updatedAt: body.updatedAt };
255
+ });
256
+ return { version: 1, bodies };
257
+ }
258
+ function validDatabase(bytes, path) {
259
+ if (bytes.length < 100 || !Buffer.from(bytes.subarray(0, SQLITE_HEADER.length)).equals(SQLITE_HEADER))
260
+ throw new Error(`${path} is not a SQLite mnemon.db`);
261
+ }
262
+ function validatePackPayload(files, components) {
263
+ if (components.includes('runtime'))
264
+ parseRuntime(json(files['payload/runtime/memories.json'] ?? new Uint8Array(), 'payload/runtime/memories.json'));
265
+ if (components.includes('documents')) {
266
+ const index = parseDocumentIndex(json(files['payload/documents/index.json'] ?? new Uint8Array(), 'payload/documents/index.json'));
267
+ for (const document of index.documents) {
268
+ const path = `payload/${document.relativePath}`;
269
+ const bytes = files[path];
270
+ if (bytes === undefined)
271
+ throw new Error(`Documents payload is missing ${document.relativePath}`);
272
+ if (bytes.length !== document.sizeBytes)
273
+ throw new Error(`Documents payload size does not match ${document.relativePath}`);
274
+ const markdown = utf8(bytes, path);
275
+ const frontmatterEnd = markdown.startsWith('---\n') ? markdown.indexOf('\n---\n', 4) : -1;
276
+ if (frontmatterEnd < 0 || !markdown.slice(4, frontmatterEnd).split('\n').includes(`id: ${JSON.stringify(document.id)}`))
277
+ throw new Error(`Documents payload identity does not match ${document.relativePath}`);
278
+ const body = markdown.slice(frontmatterEnd + 5).trim();
279
+ if (sha256(body) !== document.contentHash)
280
+ throw new Error(`Documents payload content hash does not match ${document.relativePath}`);
281
+ }
282
+ }
283
+ if (components.includes('memory-spaces')) {
284
+ const registry = parseRegistry(json(files['payload/data/.dsh-memory-bodies.json'] ?? new Uint8Array(), 'payload/data/.dsh-memory-bodies.json'));
285
+ for (const body of registry.bodies) {
286
+ const path = `payload/data/${body.id}/mnemon.db`;
287
+ const database = files[path];
288
+ if (database === undefined)
289
+ throw new Error(`Memory Space payload is missing ${body.id}/mnemon.db`);
290
+ validDatabase(database, path);
291
+ }
292
+ const databases = Object.keys(files).filter(path => /^payload\/data\/[^/]+\/mnemon\.db$/u.test(path));
293
+ if (databases.length !== registry.bodies.length)
294
+ throw new Error('Memory Space registry does not match the database payload');
295
+ }
296
+ }
297
+ function sleepSync(milliseconds) {
298
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, milliseconds);
299
+ }
300
+ function acquireLock(path) {
301
+ mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
302
+ const deadline = Date.now() + LOCK_TIMEOUT_MS;
303
+ let descriptor;
304
+ while (descriptor === undefined) {
305
+ try {
306
+ descriptor = openSync(path, 'wx', 0o600);
307
+ }
308
+ catch (error) {
309
+ if (error.code !== 'EEXIST')
310
+ throw error;
311
+ try {
312
+ if (Date.now() - statSync(path).mtimeMs > LOCK_STALE_MS)
313
+ rmSync(path, { force: true });
314
+ }
315
+ catch { }
316
+ if (Date.now() >= deadline)
317
+ throw new Error(`timed out waiting for Pack component lock: ${path}`);
318
+ sleepSync(LOCK_RETRY_MS);
319
+ }
320
+ }
321
+ const identity = fstatSync(descriptor);
322
+ return () => {
323
+ closeSync(descriptor);
324
+ try {
325
+ const current = lstatSync(path);
326
+ if (current.dev === identity.dev && current.ino === identity.ino)
327
+ rmSync(path, { force: true });
328
+ }
329
+ catch { }
330
+ };
331
+ }
332
+ function withLocks(root, components, operation) {
333
+ const paths = [join(root, '.dsh-pack.lock')];
334
+ if (components.includes('runtime'))
335
+ paths.push(join(root, 'runtime', '.memories.lock'));
336
+ if (components.includes('documents'))
337
+ paths.push(join(root, 'documents', '.index.lock'));
338
+ const releases = [];
339
+ try {
340
+ for (const path of paths)
341
+ releases.push(acquireLock(path));
342
+ return operation();
343
+ }
344
+ finally {
345
+ for (const release of releases.reverse())
346
+ release();
347
+ }
348
+ }
349
+ function assertRegularFile(path) {
350
+ const stat = lstatSync(path);
351
+ if (!stat.isFile() || stat.isSymbolicLink())
352
+ throw new Error(`Pack source is not a regular file: ${path}`);
353
+ if (stat.size > MAX_FILE_BYTES)
354
+ throw new Error(`Pack source file exceeds the safety limit: ${path}`);
355
+ }
356
+ function sourceBytes(path) {
357
+ assertRegularFile(path);
358
+ return readFileSync(path);
359
+ }
360
+ function emptyRuntime() {
361
+ return { version: 1, entries: [] };
362
+ }
363
+ function readCurrentRuntime(root) {
364
+ const path = join(root, 'runtime', 'memories.json');
365
+ return existsSync(path) ? parseRuntime(JSON.parse(readFileSync(path, 'utf8'))) : emptyRuntime();
366
+ }
367
+ function writeRuntime(directory, file) {
368
+ mkdirSync(directory, { recursive: true, mode: 0o700 });
369
+ writeFileSync(join(directory, 'memories.json'), `${JSON.stringify(file, null, 2)}\n`, { mode: 0o600 });
370
+ writeFileSync(join(directory, 'USER.md'), runtimeProjection(file.entries, 'user'), { mode: 0o600 });
371
+ writeFileSync(join(directory, 'MEMORY.md'), runtimeProjection(file.entries, 'memory'), { mode: 0o600 });
372
+ }
373
+ function readCurrentDocuments(root) {
374
+ const indexPath = join(root, 'documents', 'index.json');
375
+ const index = existsSync(indexPath) ? parseDocumentIndex(JSON.parse(readFileSync(indexPath, 'utf8'))) : { version: 1, documents: [] };
376
+ const files = new Map();
377
+ for (const document of index.documents) {
378
+ const path = join(root, document.relativePath);
379
+ if (!existsSync(path))
380
+ throw new Error(`current Documents index is missing ${document.relativePath}`);
381
+ files.set(document.id, sourceBytes(path));
382
+ }
383
+ return { index, files };
384
+ }
385
+ function writeDocuments(directory, index, files) {
386
+ mkdirSync(join(directory, 'active'), { recursive: true, mode: 0o700 });
387
+ mkdirSync(join(directory, 'archived'), { recursive: true, mode: 0o700 });
388
+ for (const document of index.documents) {
389
+ const bytes = files.get(document.id);
390
+ if (bytes === undefined)
391
+ throw new Error(`Documents staging is missing ${document.id}`);
392
+ writeFileSync(join(directory, document.status, document.filename), bytes, { mode: 0o600 });
393
+ }
394
+ writeFileSync(join(directory, 'index.json'), `${JSON.stringify(index, null, 2)}\n`, { mode: 0o600 });
395
+ }
396
+ function archiveDocuments(pack) {
397
+ const index = parseDocumentIndex(json(pack.files['payload/documents/index.json'], 'payload/documents/index.json'));
398
+ return { index, files: new Map(index.documents.map(document => [document.id, pack.files[`payload/${document.relativePath}`]])) };
399
+ }
400
+ function remapDocument(document, bytes, id) {
401
+ const base = document.filename.replace(/\.md$/u, '').slice(0, 80) || 'document';
402
+ const filename = `${base}-${id.slice(0, 8)}.md`;
403
+ const markdown = utf8(bytes, document.filename).replace(/^id:\s*.*$/mu, `id: ${JSON.stringify(id)}`);
404
+ const output = strToU8(markdown);
405
+ return {
406
+ document: { ...document, id, filename, relativePath: `documents/${document.status}/${filename}`, sizeBytes: output.length },
407
+ bytes: output,
408
+ };
409
+ }
410
+ function readCurrentRegistry(root) {
411
+ const data = join(root, 'data');
412
+ if (!existsSync(data))
413
+ return { registry: { version: 1, bodies: [] }, databases: new Map() };
414
+ const discovered = readdirSync(data, { withFileTypes: true }).filter(entry => entry.isDirectory() && BODY_ID.test(entry.name) && existsSync(join(data, entry.name, 'mnemon.db'))).map(entry => entry.name);
415
+ const registryPath = join(data, '.dsh-memory-bodies.json');
416
+ const existing = existsSync(registryPath) ? parseRegistry(JSON.parse(readFileSync(registryPath, 'utf8'))) : { version: 1, bodies: [] };
417
+ const byId = new Map(existing.bodies.map(body => [body.id, body]));
418
+ const timestamp = new Date().toISOString();
419
+ const bodies = discovered.map(id => byId.get(id) ?? { id, name: id, description: 'Existing Mnemon Store discovered on disk.', active: false, createdAt: timestamp, updatedAt: timestamp });
420
+ const databases = new Map();
421
+ for (const body of bodies) {
422
+ const path = join(data, body.id, 'mnemon.db');
423
+ const wal = `${path}-wal`;
424
+ if (existsSync(wal) && statSync(wal).size > 0)
425
+ throw new Error(`Memory Space ${body.id} is busy (mnemon.db-wal is not checkpointed); retry after writes settle`);
426
+ const bytes = sourceBytes(path);
427
+ validDatabase(bytes, path);
428
+ databases.set(body.id, bytes);
429
+ }
430
+ return { registry: { version: 1, bodies }, databases };
431
+ }
432
+ function archiveRegistry(pack) {
433
+ const registry = parseRegistry(json(pack.files['payload/data/.dsh-memory-bodies.json'], 'payload/data/.dsh-memory-bodies.json'));
434
+ return { registry, databases: new Map(registry.bodies.map(body => [body.id, pack.files[`payload/data/${body.id}/mnemon.db`]])) };
435
+ }
436
+ function writeRegistry(directory, registry, databases) {
437
+ mkdirSync(directory, { recursive: true, mode: 0o700 });
438
+ for (const body of registry.bodies) {
439
+ const bytes = databases.get(body.id);
440
+ if (bytes === undefined)
441
+ throw new Error(`Memory Space staging is missing ${body.id}/mnemon.db`);
442
+ const bodyDirectory = join(directory, body.id);
443
+ mkdirSync(bodyDirectory, { recursive: true, mode: 0o700 });
444
+ writeFileSync(join(bodyDirectory, 'mnemon.db'), bytes, { mode: 0o600 });
445
+ }
446
+ writeFileSync(join(directory, '.dsh-memory-bodies.json'), `${JSON.stringify(registry, null, 2)}\n`, { mode: 0o600 });
447
+ }
448
+ function componentItems(component, files) {
449
+ if (component === 'runtime')
450
+ return parseRuntime(json(files['payload/runtime/memories.json'], 'payload/runtime/memories.json')).entries.length;
451
+ if (component === 'documents')
452
+ return parseDocumentIndex(json(files['payload/documents/index.json'], 'payload/documents/index.json')).documents.length;
453
+ return parseRegistry(json(files['payload/data/.dsh-memory-bodies.json'], 'payload/data/.dsh-memory-bodies.json')).bodies.length;
454
+ }
455
+ function summaryFor(components, files) {
456
+ return components.map(component => {
457
+ const prefix = `payload/${COMPONENT_DIRECTORIES[component]}/`;
458
+ const entries = Object.entries(files).filter(([path]) => path.startsWith(prefix));
459
+ return { component, files: entries.length, bytes: entries.reduce((sum, [, value]) => sum + value.length, 0), items: componentItems(component, files) };
460
+ });
461
+ }
462
+ function collectExport(root, components) {
463
+ const files = {};
464
+ if (components.includes('runtime')) {
465
+ const runtime = readCurrentRuntime(root);
466
+ files['payload/runtime/memories.json'] = strToU8(`${JSON.stringify(runtime, null, 2)}\n`);
467
+ files['payload/runtime/USER.md'] = strToU8(runtimeProjection(runtime.entries, 'user'));
468
+ files['payload/runtime/MEMORY.md'] = strToU8(runtimeProjection(runtime.entries, 'memory'));
469
+ }
470
+ if (components.includes('documents')) {
471
+ const current = readCurrentDocuments(root);
472
+ files['payload/documents/index.json'] = strToU8(`${JSON.stringify(current.index, null, 2)}\n`);
473
+ for (const document of current.index.documents)
474
+ files[`payload/${document.relativePath}`] = current.files.get(document.id);
475
+ }
476
+ if (components.includes('memory-spaces')) {
477
+ const current = readCurrentRegistry(root);
478
+ files['payload/data/.dsh-memory-bodies.json'] = strToU8(`${JSON.stringify(current.registry, null, 2)}\n`);
479
+ for (const body of current.registry.bodies)
480
+ files[`payload/data/${body.id}/mnemon.db`] = current.databases.get(body.id);
481
+ }
482
+ return files;
483
+ }
484
+ function mergeRuntime(root, pack) {
485
+ const current = readCurrentRuntime(root);
486
+ const incoming = parseRuntime(json(pack.files['payload/runtime/memories.json'], 'payload/runtime/memories.json'));
487
+ const keys = new Set(current.entries.map(entry => `${entry.target}\0${entry.content}`));
488
+ const entries = [...current.entries];
489
+ for (const entry of incoming.entries) {
490
+ const key = `${entry.target}\0${entry.content}`;
491
+ if (!keys.has(key)) {
492
+ keys.add(key);
493
+ entries.push(entry);
494
+ }
495
+ }
496
+ return parseRuntime({ version: 1, entries });
497
+ }
498
+ function mergeDocuments(root, pack) {
499
+ const current = readCurrentDocuments(root);
500
+ const incoming = archiveDocuments(pack);
501
+ const ids = new Map(current.index.documents.map(document => [document.id, document]));
502
+ for (const source of incoming.index.documents) {
503
+ const existing = ids.get(source.id);
504
+ if (existing !== undefined && existing.contentHash === source.contentHash)
505
+ continue;
506
+ let document = source;
507
+ let bytes = incoming.files.get(source.id);
508
+ if (existing !== undefined) {
509
+ const remapped = remapDocument(source, bytes, randomUUID());
510
+ document = remapped.document;
511
+ bytes = remapped.bytes;
512
+ }
513
+ current.index.documents.push(document);
514
+ current.files.set(document.id, bytes);
515
+ ids.set(document.id, document);
516
+ }
517
+ const activeBytes = current.index.documents.filter(document => document.status === 'active').reduce((sum, document) => sum + document.sizeBytes, 0);
518
+ if (activeBytes > DOCUMENTS_ACTIVE_LIMIT_BYTES)
519
+ throw new Error(`merged Documents exceed the ${DOCUMENTS_ACTIVE_LIMIT_BYTES} byte active limit`);
520
+ return current;
521
+ }
522
+ function mergeRegistry(root, pack) {
523
+ const current = readCurrentRegistry(root);
524
+ const incoming = archiveRegistry(pack);
525
+ const ids = new Set(current.registry.bodies.map(body => body.id));
526
+ for (const source of incoming.registry.bodies) {
527
+ let body = source;
528
+ const sourceDb = incoming.databases.get(source.id);
529
+ if (ids.has(source.id)) {
530
+ const currentDb = current.databases.get(source.id);
531
+ if (sha256(currentDb) === sha256(sourceDb))
532
+ continue;
533
+ const id = randomUUID();
534
+ body = { ...source, id, updatedAt: new Date().toISOString() };
535
+ }
536
+ ids.add(body.id);
537
+ current.registry.bodies.push(body);
538
+ current.databases.set(body.id, sourceDb);
539
+ }
540
+ return current;
541
+ }
542
+ function stageImport(root, pack, components, mode) {
543
+ const staging = join(root, `.dsh-pack-stage-${randomUUID()}`);
544
+ mkdirSync(staging, { recursive: true, mode: 0o700 });
545
+ try {
546
+ if (components.includes('runtime')) {
547
+ const runtime = mode === 'merge' ? mergeRuntime(root, pack) : parseRuntime(json(pack.files['payload/runtime/memories.json'], 'payload/runtime/memories.json'));
548
+ writeRuntime(join(staging, 'runtime'), runtime);
549
+ }
550
+ if (components.includes('documents')) {
551
+ const documents = mode === 'merge' ? mergeDocuments(root, pack) : archiveDocuments(pack);
552
+ writeDocuments(join(staging, 'documents'), documents.index, documents.files);
553
+ }
554
+ if (components.includes('memory-spaces')) {
555
+ const memory = mode === 'merge' ? mergeRegistry(root, pack) : archiveRegistry(pack);
556
+ writeRegistry(join(staging, 'data'), memory.registry, memory.databases);
557
+ }
558
+ return staging;
559
+ }
560
+ catch (error) {
561
+ rmSync(staging, { recursive: true, force: true });
562
+ throw error;
563
+ }
564
+ }
565
+ function commitStaging(root, staging, components) {
566
+ const backup = join(root, `.dsh-pack-backup-${randomUUID()}`);
567
+ mkdirSync(backup, { recursive: true, mode: 0o700 });
568
+ const committed = [];
569
+ const replacementLocks = components.flatMap(component => component === 'runtime'
570
+ ? [join(staging, 'runtime', '.memories.lock')]
571
+ : component === 'documents' ? [join(staging, 'documents', '.index.lock')] : []);
572
+ try {
573
+ for (const lock of replacementLocks)
574
+ writeFileSync(lock, 'pack-import\n', { mode: 0o600 });
575
+ for (const component of components) {
576
+ const directory = COMPONENT_DIRECTORIES[component];
577
+ const target = join(root, directory);
578
+ const previous = join(backup, directory);
579
+ const hadPrevious = existsSync(target);
580
+ if (hadPrevious)
581
+ renameSync(target, previous);
582
+ try {
583
+ renameSync(join(staging, directory), target);
584
+ }
585
+ catch (error) {
586
+ if (hadPrevious)
587
+ renameSync(previous, target);
588
+ throw error;
589
+ }
590
+ committed.push({ directory, hadPrevious });
591
+ }
592
+ if (components.includes('runtime'))
593
+ rmSync(join(root, 'runtime', '.memories.lock'), { force: true });
594
+ if (components.includes('documents'))
595
+ rmSync(join(root, 'documents', '.index.lock'), { force: true });
596
+ }
597
+ catch (error) {
598
+ for (const entry of committed.reverse()) {
599
+ const target = join(root, entry.directory);
600
+ rmSync(target, { recursive: true, force: true });
601
+ if (entry.hadPrevious)
602
+ renameSync(join(backup, entry.directory), target);
603
+ }
604
+ throw error;
605
+ }
606
+ finally {
607
+ rmSync(staging, { recursive: true, force: true });
608
+ rmSync(backup, { recursive: true, force: true });
609
+ }
610
+ }
611
+ function occupied(root, component) {
612
+ const directory = join(root, COMPONENT_DIRECTORIES[component]);
613
+ if (!existsSync(directory))
614
+ return false;
615
+ try {
616
+ return readdirSync(directory).some(name => !name.startsWith('.'));
617
+ }
618
+ catch {
619
+ return true;
620
+ }
621
+ }
622
+ function safeName(value) {
623
+ if (value === undefined)
624
+ return undefined;
625
+ const name = basename(value.trim()).replace(/[^a-zA-Z0-9._-]+/gu, '-');
626
+ return name === '' ? undefined : name.slice(0, 160);
627
+ }
628
+ /** Native, checksummed import/export for the one currently effective Mnemon root. */
629
+ export class MnemonPackManager {
630
+ runner;
631
+ config;
632
+ afterImport;
633
+ now;
634
+ root;
635
+ constructor(runner, config, afterImport = () => { }, now = () => new Date()) {
636
+ this.runner = runner;
637
+ this.config = config;
638
+ this.afterImport = afterImport;
639
+ this.now = now;
640
+ this.root = resolve(runner.effectiveDataDir());
641
+ }
642
+ target() {
643
+ return { root: this.root, scope: this.config.storageScope };
644
+ }
645
+ async exportPack(scope) {
646
+ const components = componentsForScope(scope);
647
+ return this.runner.withExclusive(async () => {
648
+ await new Promise(resolveReady => setImmediate(resolveReady));
649
+ return withLocks(this.root, components, () => {
650
+ const payload = collectExport(this.root, components);
651
+ const exportedAt = this.now().toISOString();
652
+ const summary = summaryFor(components, payload);
653
+ const manifest = {
654
+ format: MNEMON_PACK_FORMAT, version: MNEMON_PACK_VERSION, scope, exportedAt,
655
+ source: { plugin: 'dsh-mnemon', pluginVersion: '0.1.0' }, components, summary,
656
+ };
657
+ const checksums = { algorithm: 'sha256', files: Object.fromEntries(Object.entries(payload).map(([path, bytes]) => [path, sha256(bytes)])) };
658
+ const entries = {
659
+ 'manifest.json': strToU8(`${JSON.stringify(manifest, null, 2)}\n`),
660
+ 'checksums.json': strToU8(`${JSON.stringify(checksums, null, 2)}\n`),
661
+ ...payload,
662
+ };
663
+ const archive = zipSync(entries, { level: 6, mtime: new Date('1980-01-01T00:00:00.000Z') });
664
+ if (archive.length > MNEMON_PACK_MAX_ARCHIVE_BYTES)
665
+ throw new Error('exported Mnemon Pack exceeds the transport safety limit');
666
+ const stamp = exportedAt.replace(/[:.]/gu, '-').replace('T', '_').replace('Z', '');
667
+ return {
668
+ fileName: `mnemon-backup-${stamp}.zip`, mimeType: MNEMON_PACK_MIME, bytes: archive.length,
669
+ base64: Buffer.from(archive).toString('base64'), targetRoot: this.root, manifest,
670
+ };
671
+ });
672
+ });
673
+ }
674
+ inspectPack(base64, fileName) {
675
+ const pack = parseArchive(base64);
676
+ const sanitizedName = safeName(fileName);
677
+ return {
678
+ ...(sanitizedName === undefined ? {} : { fileName: sanitizedName }),
679
+ archiveBytes: pack.archiveBytes, expandedBytes: pack.expandedBytes, targetRoot: this.root, targetScope: this.config.storageScope,
680
+ manifest: pack.manifest,
681
+ occupied: Object.fromEntries(COMPONENT_ORDER.map(component => [component, occupied(this.root, component)])),
682
+ };
683
+ }
684
+ async importPack(base64, options) {
685
+ const pack = parseArchive(base64);
686
+ if (options.mode !== 'merge' && options.mode !== 'replace')
687
+ throw new Error('Pack import mode must be merge or replace');
688
+ if (options.components !== undefined && (new Set(options.components).size !== options.components.length || options.components.some(component => !COMPONENT_ORDER.includes(component)))) {
689
+ throw new Error('requested import components are invalid');
690
+ }
691
+ const components = options.components === undefined ? pack.manifest.components : COMPONENT_ORDER.filter(component => options.components.includes(component));
692
+ if (components.length === 0 || components.some(component => !pack.manifest.components.includes(component)))
693
+ throw new Error('requested import components are not present in this Pack');
694
+ return this.runner.withExclusive(async () => {
695
+ await new Promise(resolveReady => setImmediate(resolveReady));
696
+ mkdirSync(this.root, { recursive: true, mode: 0o700 });
697
+ return withLocks(this.root, components, () => {
698
+ const staging = stageImport(this.root, pack, components, options.mode);
699
+ commitStaging(this.root, staging, components);
700
+ this.afterImport(components);
701
+ return {
702
+ imported: true, mode: options.mode, targetRoot: this.root, components,
703
+ summary: pack.manifest.summary.filter(summary => components.includes(summary.component)),
704
+ };
705
+ });
706
+ });
707
+ }
708
+ }
709
+ //# sourceMappingURL=pack.js.map