@dcrays/mbh-memory 0.1.1-beta.5 → 0.1.1-beta.7

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,5 +1,5 @@
1
1
  # @dcrays/mbh-memory
2
2
 
3
- 书灵墨宝的 DSH 云端记忆接入插件。加载后注册 `get_memory` 与 `set_memory`,把模型工具映射到配置的 HTTP API;插件本身不持久化记忆。
3
+ 书灵墨宝的 DSH 长期记忆插件。加载后注册 `get_memory` 与 `set_memory`:未配置 `baseUrl` 时原子写入 `$DSH_HOME/memory/users/<account-hash>/MEMORY.md`,配置后使用原有云端 HTTP API。
4
4
 
5
- 单一发布包同时适用于生产与测试桌面。API 地址由 Cordis 插件配置下发,不读取 `.env`。
5
+ 单一发布包同时适用于生产与测试桌面。API 地址由 Cordis 插件配置下发,不读取 `.env`;本地与云端数据不会自动互相覆盖。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dcrays/mbh-memory",
3
- "version": "0.1.1-beta.5",
3
+ "version": "0.1.1-beta.7",
4
4
  "description": "DeepSeek Harness memory plugin for Mobook",
5
5
  "type": "module",
6
6
  "main": "plugin/index.js",
@@ -30,6 +30,8 @@
30
30
  },
31
31
  "peerDependencies": {
32
32
  "@deepseek-ai/cordis": "^4.0.2",
33
+ "@deepseek-ai/dsh-atomic-write": "0.1.5-rc.2",
34
+ "@deepseek-ai/dsh-home-paths": "0.1.5-rc.2",
33
35
  "@deepseek-ai/dsh-tools": "0.1.5-rc.2",
34
36
  "@deepseek-ai/schemastery": "^3.18.1"
35
37
  },
package/plugin/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import type { Context } from '@deepseek-ai/cordis';
2
2
  import { Service } from '@deepseek-ai/cordis';
3
3
  import z from '@deepseek-ai/schemastery';
4
- import { type MemoryApiValue } from '../src/index.js';
4
+ import { type MemoryApiValue, type MemoryOperations } from '../src/index.js';
5
5
  export declare const name = "mbh-memory";
6
6
  export declare const inject: string[];
7
7
  export interface Config {
@@ -13,6 +13,9 @@ export interface Config {
13
13
  maxResponseBytes: number;
14
14
  }
15
15
  export declare const Config: z<Config>;
16
+ export declare function localMemoryAccountScope(metadata: unknown): string;
17
+ export declare function resolveLocalMemoryFile(): string;
18
+ export declare function createMemoryOperations(config: Config): MemoryOperations | undefined;
16
19
  declare module '@deepseek-ai/cordis' {
17
20
  interface Context {
18
21
  mbhMemory: MbhMemoryService;
@@ -24,6 +27,7 @@ export declare class MbhMemoryService extends Service {
24
27
  constructor(ctx: Context, config: Config);
25
28
  isEnabled(): boolean;
26
29
  isConfigured(): boolean;
30
+ backend(): 'disabled' | 'local' | 'cloud';
27
31
  getMemory(signal?: AbortSignal): Promise<MemoryApiValue>;
28
32
  setMemory(memory: string, signal?: AbortSignal): Promise<MemoryApiValue>;
29
33
  }
package/plugin/index.js CHANGED
@@ -1,7 +1,10 @@
1
1
  import { createRequire as __mobookCreateRequire } from 'node:module'; const require = __mobookCreateRequire(import.meta.url);
2
2
 
3
3
  // dist/plugin/index.js
4
+ import { createHash as createHash2 } from "node:crypto";
5
+ import { readFileSync } from "node:fs";
4
6
  import { Service } from "@deepseek-ai/cordis";
7
+ import { dshHomePath } from "@deepseek-ai/dsh-home-paths";
5
8
  import z from "@deepseek-ai/schemastery";
6
9
 
7
10
  // dist/src/api-client.js
@@ -133,6 +136,121 @@ var MemoryApiClient = class {
133
136
  }
134
137
  };
135
138
 
139
+ // dist/src/local-memory.js
140
+ import { createHash } from "node:crypto";
141
+ import { mkdir, readFile } from "node:fs/promises";
142
+ import { dirname } from "node:path";
143
+ import { withFileLock, writeFileAtomic } from "@deepseek-ai/dsh-atomic-write";
144
+ var HEADER = `# Long-term Memory
145
+
146
+ <!-- Managed by mbh-memory. This Markdown file is the canonical local memory source. -->
147
+ `;
148
+ var LocalMemoryError = class extends Error {
149
+ code;
150
+ constructor(code, message, options) {
151
+ super(message, options);
152
+ this.code = code;
153
+ this.name = "LocalMemoryError";
154
+ }
155
+ };
156
+ function normalizeMemory(memory) {
157
+ return memory.replaceAll("\r\n", "\n").trim();
158
+ }
159
+ function entryId(memory) {
160
+ return createHash("sha256").update(memory).digest("hex");
161
+ }
162
+ function entry(memory, now) {
163
+ const id = entryId(memory);
164
+ return {
165
+ id,
166
+ markdown: `
167
+ ## ${now.toISOString()}
168
+
169
+ <!-- mbh-memory:id=${id} -->
170
+
171
+ ${memory}
172
+ `
173
+ };
174
+ }
175
+ function isMissing(error) {
176
+ return error?.code === "ENOENT";
177
+ }
178
+ var LocalMemoryStore = class {
179
+ config;
180
+ now;
181
+ constructor(config) {
182
+ this.config = config;
183
+ if (!config.file.trim())
184
+ throw new LocalMemoryError("INVALID_CONFIG", "mbh-memory local file is required");
185
+ if (!Number.isSafeInteger(config.maxMemoryChars) || config.maxMemoryChars <= 0) {
186
+ throw new LocalMemoryError("INVALID_CONFIG", "mbh-memory maxMemoryChars must be a positive integer");
187
+ }
188
+ if (!Number.isSafeInteger(config.maxFileBytes) || config.maxFileBytes <= 0) {
189
+ throw new LocalMemoryError("INVALID_CONFIG", "mbh-memory maxFileBytes must be a positive integer");
190
+ }
191
+ this.now = config.now ?? (() => /* @__PURE__ */ new Date());
192
+ }
193
+ async getMemory(signal) {
194
+ signal?.throwIfAborted();
195
+ let markdown;
196
+ try {
197
+ markdown = await readFile(this.config.file, "utf8");
198
+ } catch (error) {
199
+ if (isMissing(error))
200
+ return { backend: "local", memory: "" };
201
+ throw new LocalMemoryError("STORAGE_ERROR", "mbh-memory local memory could not be read", { cause: error });
202
+ }
203
+ signal?.throwIfAborted();
204
+ if (Buffer.byteLength(markdown) > this.config.maxFileBytes) {
205
+ throw new LocalMemoryError("STORAGE_LIMIT", `mbh-memory local memory exceeds ${this.config.maxFileBytes} bytes`);
206
+ }
207
+ return { backend: "local", memory: markdown };
208
+ }
209
+ async setMemory(memory, signal) {
210
+ const normalized = normalizeMemory(memory);
211
+ if (!normalized)
212
+ throw new LocalMemoryError("INVALID_MEMORY", "memory must not be empty");
213
+ if (normalized.length > this.config.maxMemoryChars) {
214
+ throw new LocalMemoryError("INVALID_MEMORY", `memory exceeds ${this.config.maxMemoryChars} characters`);
215
+ }
216
+ signal?.throwIfAborted();
217
+ await mkdir(dirname(this.config.file), { recursive: true, mode: 448 }).catch((error) => {
218
+ throw new LocalMemoryError("STORAGE_ERROR", "mbh-memory local memory directory could not be prepared", {
219
+ cause: error
220
+ });
221
+ });
222
+ try {
223
+ return await withFileLock(this.config.file, async () => {
224
+ signal?.throwIfAborted();
225
+ let current = HEADER;
226
+ try {
227
+ current = await readFile(this.config.file, "utf8");
228
+ } catch (error) {
229
+ if (!isMissing(error))
230
+ throw error;
231
+ }
232
+ const nextEntry = entry(normalized, this.now());
233
+ if (current.includes(`<!-- mbh-memory:id=${nextEntry.id} -->`)) {
234
+ return { backend: "local", saved: true, duplicate: true };
235
+ }
236
+ const prefix = current.trimEnd() || HEADER.trimEnd();
237
+ const next = `${prefix}
238
+ ${nextEntry.markdown}`;
239
+ if (Buffer.byteLength(next) > this.config.maxFileBytes) {
240
+ throw new LocalMemoryError("STORAGE_LIMIT", `mbh-memory local memory would exceed ${this.config.maxFileBytes} bytes`);
241
+ }
242
+ signal?.throwIfAborted();
243
+ await writeFileAtomic(this.config.file, next, { mode: 384, dirMode: 448 });
244
+ return { backend: "local", saved: true, duplicate: false };
245
+ }, { waitMs: 5e3 });
246
+ } catch (error) {
247
+ if (error instanceof LocalMemoryError || signal?.aborted)
248
+ throw error;
249
+ throw new LocalMemoryError("STORAGE_ERROR", "mbh-memory local memory could not be written", { cause: error });
250
+ }
251
+ }
252
+ };
253
+
136
254
  // dist/src/tools.js
137
255
  import { defineTool } from "@deepseek-ai/dsh-tools";
138
256
  function render(value) {
@@ -141,7 +259,7 @@ function render(value) {
141
259
  function createMemoryTools(memory) {
142
260
  const getMemory = defineTool({
143
261
  name: "get_memory",
144
- description: "Read the user's long-term memory from the configured Mobook memory API. Use it only when prior preferences, facts, or experience are relevant to the current request.",
262
+ description: "Read the user's long-term memory from Mobook's configured memory storage. Use it only when prior preferences, facts, or experience are relevant to the current request.",
145
263
  parameters: {},
146
264
  output: {
147
265
  schema: { type: "json" },
@@ -153,7 +271,7 @@ function createMemoryTools(memory) {
153
271
  });
154
272
  const setMemory = defineTool({
155
273
  name: "set_memory",
156
- description: "Write one durable memory through the configured Mobook memory API. Use this when the user explicitly asks you to remember something useful for future conversations. Never store passwords, access tokens, API keys, or other secrets.",
274
+ description: "Write one durable memory through the configured Mobook memory storage. Use this when the user explicitly asks you to remember something useful for future conversations. Never store passwords, access tokens, API keys, or other secrets.",
157
275
  parameters: {
158
276
  memory: {
159
277
  type: "string",
@@ -186,19 +304,58 @@ var Config = z.object({
186
304
  maxMemoryChars: z.natural().min(1).default(2e4),
187
305
  maxResponseBytes: z.natural().min(1).default(128 * 1024)
188
306
  });
307
+ function record(value) {
308
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
309
+ }
310
+ function identifier(value) {
311
+ if (typeof value === "number" && Number.isSafeInteger(value))
312
+ return String(value);
313
+ if (typeof value !== "string")
314
+ return void 0;
315
+ return value.trim() || void 0;
316
+ }
317
+ function localMemoryAccountScope(metadata) {
318
+ const account = record(record(metadata)?.account);
319
+ const userId = identifier(account?.userId);
320
+ if (!userId || userId === "0")
321
+ return "anonymous";
322
+ const identity = JSON.stringify([userId, identifier(account?.appId) ?? "", identifier(account?.domainId) ?? ""]);
323
+ return createHash2("sha256").update(identity).digest("hex").slice(0, 32);
324
+ }
325
+ function resolveLocalMemoryFile() {
326
+ let metadata;
327
+ try {
328
+ metadata = JSON.parse(readFileSync(dshHomePath("mobook.json"), "utf8"));
329
+ } catch {
330
+ metadata = void 0;
331
+ }
332
+ return dshHomePath("memory", "users", localMemoryAccountScope(metadata), "MEMORY.md");
333
+ }
334
+ function createMemoryOperations(config) {
335
+ if (!config.enabled)
336
+ return void 0;
337
+ if (config.baseUrl.trim()) {
338
+ return new MemoryApiClient({
339
+ baseUrl: config.baseUrl.trim(),
340
+ ...config.authorizationToken.trim() ? { authorizationToken: config.authorizationToken.trim() } : {},
341
+ timeoutMs: config.timeoutMs,
342
+ maxMemoryChars: config.maxMemoryChars,
343
+ maxResponseBytes: config.maxResponseBytes
344
+ });
345
+ }
346
+ return new LocalMemoryStore({
347
+ file: resolveLocalMemoryFile(),
348
+ maxMemoryChars: config.maxMemoryChars,
349
+ maxFileBytes: config.maxResponseBytes
350
+ });
351
+ }
189
352
  var MbhMemoryService = class extends Service {
190
353
  config;
191
354
  operations;
192
355
  constructor(ctx, config) {
193
356
  super(ctx, MBH_MEMORY_SERVICE_KEY);
194
357
  this.config = config;
195
- this.operations = config.enabled && config.baseUrl.trim() ? new MemoryApiClient({
196
- baseUrl: config.baseUrl.trim(),
197
- ...config.authorizationToken.trim() ? { authorizationToken: config.authorizationToken.trim() } : {},
198
- timeoutMs: config.timeoutMs,
199
- maxMemoryChars: config.maxMemoryChars,
200
- maxResponseBytes: config.maxResponseBytes
201
- }) : void 0;
358
+ this.operations = createMemoryOperations(config);
202
359
  }
203
360
  isEnabled() {
204
361
  return this.config.enabled;
@@ -206,14 +363,19 @@ var MbhMemoryService = class extends Service {
206
363
  isConfigured() {
207
364
  return this.operations !== void 0;
208
365
  }
366
+ backend() {
367
+ if (!this.operations)
368
+ return "disabled";
369
+ return this.config.baseUrl.trim() ? "cloud" : "local";
370
+ }
209
371
  getMemory(signal) {
210
372
  if (!this.operations)
211
- return Promise.reject(new Error("mbh-memory API is not configured"));
373
+ return Promise.reject(new Error("mbh-memory storage is disabled"));
212
374
  return this.operations.getMemory(signal);
213
375
  }
214
376
  setMemory(memory, signal) {
215
377
  if (!this.operations)
216
- return Promise.reject(new Error("mbh-memory API is not configured"));
378
+ return Promise.reject(new Error("mbh-memory storage is disabled"));
217
379
  return this.operations.setMemory(memory, signal);
218
380
  }
219
381
  };
@@ -223,12 +385,10 @@ function apply(ctx, config) {
223
385
  const logger = ctx.logger(name);
224
386
  if (!service.isEnabled())
225
387
  return () => void 0;
226
- if (!service.isConfigured()) {
227
- logger.info("%s plugin loaded without an API baseUrl; memory tools are unavailable", name);
388
+ if (!service.isConfigured())
228
389
  return () => void 0;
229
- }
230
390
  const unregister = createMemoryTools(service).map((tool) => ctx.tools.register(tool));
231
- logger.info("%s plugin loaded with get_memory and set_memory tools", name);
391
+ logger.info("%s plugin loaded with get_memory and set_memory tools using %s storage", name, service.backend());
232
392
  return () => {
233
393
  for (const dispose of unregister.reverse())
234
394
  dispose();
@@ -239,6 +399,9 @@ export {
239
399
  Config,
240
400
  MbhMemoryService,
241
401
  apply,
402
+ createMemoryOperations,
242
403
  inject,
243
- name
404
+ localMemoryAccountScope,
405
+ name,
406
+ resolveLocalMemoryFile
244
407
  };
package/src/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export declare const MBH_MEMORY_PLUGIN_NAME = "mbh-memory";
2
2
  export declare const MBH_MEMORY_SERVICE_KEY = "mbhMemory";
3
3
  export * from './api-client.js';
4
+ export * from './local-memory.js';
4
5
  export * from './tools.js';
package/src/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  export const MBH_MEMORY_PLUGIN_NAME = 'mbh-memory';
2
2
  export const MBH_MEMORY_SERVICE_KEY = 'mbhMemory';
3
3
  export * from './api-client.js';
4
+ export * from './local-memory.js';
4
5
  export * from './tools.js';
@@ -0,0 +1,18 @@
1
+ import type { MemoryApiValue, MemoryOperations } from './api-client.js';
2
+ export interface LocalMemoryStoreConfig {
3
+ file: string;
4
+ maxMemoryChars: number;
5
+ maxFileBytes: number;
6
+ now?: () => Date;
7
+ }
8
+ export declare class LocalMemoryError extends Error {
9
+ readonly code: 'INVALID_CONFIG' | 'INVALID_MEMORY' | 'STORAGE_LIMIT' | 'STORAGE_ERROR';
10
+ constructor(code: 'INVALID_CONFIG' | 'INVALID_MEMORY' | 'STORAGE_LIMIT' | 'STORAGE_ERROR', message: string, options?: ErrorOptions);
11
+ }
12
+ export declare class LocalMemoryStore implements MemoryOperations {
13
+ private readonly config;
14
+ private readonly now;
15
+ constructor(config: LocalMemoryStoreConfig);
16
+ getMemory(signal?: AbortSignal): Promise<MemoryApiValue>;
17
+ setMemory(memory: string, signal?: AbortSignal): Promise<MemoryApiValue>;
18
+ }
@@ -0,0 +1,109 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { mkdir, readFile } from 'node:fs/promises';
3
+ import { dirname } from 'node:path';
4
+ import { withFileLock, writeFileAtomic } from '@deepseek-ai/dsh-atomic-write';
5
+ const HEADER = `# Long-term Memory
6
+
7
+ <!-- Managed by mbh-memory. This Markdown file is the canonical local memory source. -->
8
+ `;
9
+ export class LocalMemoryError extends Error {
10
+ code;
11
+ constructor(code, message, options) {
12
+ super(message, options);
13
+ this.code = code;
14
+ this.name = 'LocalMemoryError';
15
+ }
16
+ }
17
+ function normalizeMemory(memory) {
18
+ return memory.replaceAll('\r\n', '\n').trim();
19
+ }
20
+ function entryId(memory) {
21
+ return createHash('sha256').update(memory).digest('hex');
22
+ }
23
+ function entry(memory, now) {
24
+ const id = entryId(memory);
25
+ return {
26
+ id,
27
+ markdown: `\n## ${now.toISOString()}\n\n<!-- mbh-memory:id=${id} -->\n\n${memory}\n`
28
+ };
29
+ }
30
+ function isMissing(error) {
31
+ return error?.code === 'ENOENT';
32
+ }
33
+ export class LocalMemoryStore {
34
+ config;
35
+ now;
36
+ constructor(config) {
37
+ this.config = config;
38
+ if (!config.file.trim())
39
+ throw new LocalMemoryError('INVALID_CONFIG', 'mbh-memory local file is required');
40
+ if (!Number.isSafeInteger(config.maxMemoryChars) || config.maxMemoryChars <= 0) {
41
+ throw new LocalMemoryError('INVALID_CONFIG', 'mbh-memory maxMemoryChars must be a positive integer');
42
+ }
43
+ if (!Number.isSafeInteger(config.maxFileBytes) || config.maxFileBytes <= 0) {
44
+ throw new LocalMemoryError('INVALID_CONFIG', 'mbh-memory maxFileBytes must be a positive integer');
45
+ }
46
+ this.now = config.now ?? (() => new Date());
47
+ }
48
+ async getMemory(signal) {
49
+ signal?.throwIfAborted();
50
+ let markdown;
51
+ try {
52
+ markdown = await readFile(this.config.file, 'utf8');
53
+ }
54
+ catch (error) {
55
+ if (isMissing(error))
56
+ return { backend: 'local', memory: '' };
57
+ throw new LocalMemoryError('STORAGE_ERROR', 'mbh-memory local memory could not be read', { cause: error });
58
+ }
59
+ signal?.throwIfAborted();
60
+ if (Buffer.byteLength(markdown) > this.config.maxFileBytes) {
61
+ throw new LocalMemoryError('STORAGE_LIMIT', `mbh-memory local memory exceeds ${this.config.maxFileBytes} bytes`);
62
+ }
63
+ return { backend: 'local', memory: markdown };
64
+ }
65
+ async setMemory(memory, signal) {
66
+ const normalized = normalizeMemory(memory);
67
+ if (!normalized)
68
+ throw new LocalMemoryError('INVALID_MEMORY', 'memory must not be empty');
69
+ if (normalized.length > this.config.maxMemoryChars) {
70
+ throw new LocalMemoryError('INVALID_MEMORY', `memory exceeds ${this.config.maxMemoryChars} characters`);
71
+ }
72
+ signal?.throwIfAborted();
73
+ await mkdir(dirname(this.config.file), { recursive: true, mode: 0o700 }).catch((error) => {
74
+ throw new LocalMemoryError('STORAGE_ERROR', 'mbh-memory local memory directory could not be prepared', {
75
+ cause: error
76
+ });
77
+ });
78
+ try {
79
+ return await withFileLock(this.config.file, async () => {
80
+ signal?.throwIfAborted();
81
+ let current = HEADER;
82
+ try {
83
+ current = await readFile(this.config.file, 'utf8');
84
+ }
85
+ catch (error) {
86
+ if (!isMissing(error))
87
+ throw error;
88
+ }
89
+ const nextEntry = entry(normalized, this.now());
90
+ if (current.includes(`<!-- mbh-memory:id=${nextEntry.id} -->`)) {
91
+ return { backend: 'local', saved: true, duplicate: true };
92
+ }
93
+ const prefix = current.trimEnd() || HEADER.trimEnd();
94
+ const next = `${prefix}\n${nextEntry.markdown}`;
95
+ if (Buffer.byteLength(next) > this.config.maxFileBytes) {
96
+ throw new LocalMemoryError('STORAGE_LIMIT', `mbh-memory local memory would exceed ${this.config.maxFileBytes} bytes`);
97
+ }
98
+ signal?.throwIfAborted();
99
+ await writeFileAtomic(this.config.file, next, { mode: 0o600, dirMode: 0o700 });
100
+ return { backend: 'local', saved: true, duplicate: false };
101
+ }, { waitMs: 5_000 });
102
+ }
103
+ catch (error) {
104
+ if (error instanceof LocalMemoryError || signal?.aborted)
105
+ throw error;
106
+ throw new LocalMemoryError('STORAGE_ERROR', 'mbh-memory local memory could not be written', { cause: error });
107
+ }
108
+ }
109
+ }
package/src/tools.js CHANGED
@@ -5,7 +5,7 @@ function render(value) {
5
5
  export function createMemoryTools(memory) {
6
6
  const getMemory = defineTool({
7
7
  name: 'get_memory',
8
- description: "Read the user's long-term memory from the configured Mobook memory API. Use it only when prior preferences, facts, or experience are relevant to the current request.",
8
+ description: "Read the user's long-term memory from Mobook's configured memory storage. Use it only when prior preferences, facts, or experience are relevant to the current request.",
9
9
  parameters: {},
10
10
  output: {
11
11
  schema: { type: 'json' },
@@ -17,7 +17,7 @@ export function createMemoryTools(memory) {
17
17
  });
18
18
  const setMemory = defineTool({
19
19
  name: 'set_memory',
20
- description: 'Write one durable memory through the configured Mobook memory API. Use this when the user explicitly asks you to remember something useful for future conversations. Never store passwords, access tokens, API keys, or other secrets.',
20
+ description: 'Write one durable memory through the configured Mobook memory storage. Use this when the user explicitly asks you to remember something useful for future conversations. Never store passwords, access tokens, API keys, or other secrets.',
21
21
  parameters: {
22
22
  memory: {
23
23
  type: 'string',