@modusensus/dsh-mneme 0.1.3 → 0.1.5

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
@@ -3,7 +3,7 @@
3
3
  [![npm version](https://img.shields.io/npm/v/@modusensus/dsh-mneme?color=blue&label=npm)](https://www.npmjs.com/package/@modusensus/dsh-mneme)
4
4
  [![license](https://img.shields.io/badge/license-MIT-green)](LICENSE)
5
5
  [![dsh-plugin](https://img.shields.io/badge/dsh-plugin-awesome-orange)](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin)
6
- [![tests](https://img.shields.io/badge/tests-108%20passed-success)](https://github.com/modusensus/dsh-mneme)
6
+ [![tests](https://img.shields.io/badge/tests-129%20passed-success)](https://github.com/modusensus/dsh-mneme)
7
7
 
8
8
  > 给 DeepSeek Harness 的跨会话记忆插件:让 Agent 记住你、记住项目、自动整理记忆。**Mneme**(Μνήμη)——希腊记忆女神 Mnemosyne 之名,掌管记忆与梦境,正如 autoDream 在后台巩固记忆。
9
9
 
@@ -47,6 +47,16 @@
47
47
 
48
48
  侧边栏"记忆"按钮 → 模态面板:按类型浏览、全文搜索、查看详情。
49
49
 
50
+ ### 用户设置(画像 / 规则)与自定义指令 ⚙️
51
+
52
+ 侧边栏"设置"按钮 → 设置面板:
53
+
54
+ - **用户画像**:一段自由文本描述用户自己(角色、背景、偏好),**每轮注入**到系统提示,让 Agent 始终遵循
55
+ - **规则**:Agent 必须遵守的行为规则列表(如"回答先给结论"),同样每轮注入
56
+ - **自定义指令**:注册斜杠命令(`/名称`),触发时把用户定义的指令内容交给 Agent。命令持久化到 SQLite,启动时自动注册到 DSH 命令表,增删实时生效
57
+
58
+ > 画像与规则通过独立的 `[用户设置]` 注入区块(优先级高于记忆库),即使记忆为空也会注入。
59
+
50
60
  ## 📦 安装
51
61
 
52
62
  ### 前置条件
@@ -150,7 +160,7 @@ src/
150
160
  lib/
151
161
  ├── client.js # Web 面板(手写 ModuleLoader bundle)
152
162
  └── *.js # src 的同步分发产物
153
- test/ # 108 个 node:test 测试
163
+ test/ # 129 个 node:test 测试
154
164
  ```
155
165
 
156
166
  ## 🧪 开发
@@ -158,7 +168,7 @@ test/ # 108 个 node:test 测试
158
168
  ```bash
159
169
  cd dsh-mneme
160
170
  npm install # 安装 peer 依赖(以 devDependencies 形式,用于本地测试)
161
- npm test # 运行 108 个测试(--test-isolation=none 用于受限沙箱,禁止子进程 spawn)
171
+ npm test # 运行 129 个测试(--test-isolation=none 用于受限沙箱,禁止子进程 spawn)
162
172
  npm run sync # 把 src/ 同步到 lib/(发布时由 prepack 钩子自动执行)
163
173
  ```
164
174
 
package/lib/api.js CHANGED
@@ -5,19 +5,41 @@ function sendJson(res, status, payload) {
5
5
  res.end(JSON.stringify(payload));
6
6
  }
7
7
 
8
- export function createApi(ctx, service) {
8
+ /** Collect the request body as text (tolerant of empty/invalid bodies). */
9
+ function readBody(req) {
10
+ return new Promise((resolve) => {
11
+ let body = "";
12
+ req.on("data", (chunk) => { body += chunk; });
13
+ req.on("end", () => resolve(body));
14
+ req.on("error", () => resolve(""));
15
+ });
16
+ }
17
+
18
+ function parseBody(text) {
19
+ try {
20
+ return JSON.parse(text || "{}");
21
+ } catch {
22
+ return {};
23
+ }
24
+ }
25
+
26
+ export function createApi(ctx, service, settings, commands, embedder) {
9
27
  const disposers = [];
10
28
 
29
+ const register = (route) => {
30
+ disposers.push(ctx.webServer.register(route));
31
+ };
32
+
11
33
  // /api/dsh-mneme prefix fallback → 404 JSON for unknown sub-paths
12
- disposers.push(ctx.webServer.register({
34
+ register({
13
35
  kind: "prefix",
14
36
  path: "/api/dsh-mneme",
15
37
  handler(req, res) {
16
38
  sendJson(res, 404, { error: "not-found" });
17
39
  }
18
- }));
40
+ });
19
41
 
20
- disposers.push(ctx.webServer.register({
42
+ register({
21
43
  kind: "exact",
22
44
  path: "/api/dsh-mneme/list",
23
45
  handler(req, res) {
@@ -32,9 +54,9 @@ export function createApi(ctx, service) {
32
54
  sendJson(res, 500, { error: "internal" });
33
55
  }
34
56
  }
35
- }));
57
+ });
36
58
 
37
- disposers.push(ctx.webServer.register({
59
+ register({
38
60
  kind: "exact",
39
61
  path: "/api/dsh-mneme/search",
40
62
  handler(req, res) {
@@ -42,16 +64,180 @@ export function createApi(ctx, service) {
42
64
  const url = new URL(req.url, "http://localhost");
43
65
  const q = url.searchParams.get("q") ?? "";
44
66
  const limit = Number(url.searchParams.get("limit") ?? 20);
45
- const items = service.toApiList(service.search(q, { limit }));
46
- sendJson(res, 200, { items });
67
+ // mode: auto (default) | keyword | vector
68
+ const mode = url.searchParams.get("mode") ?? "auto";
69
+ const query = q.trim();
70
+ if (!query) {
71
+ sendJson(res, 200, { items: [], mode: "keyword" });
72
+ return;
73
+ }
74
+ // Keyword results (existing behavior) always computed; used as a
75
+ // fallback and as the primary ranking when vector is unavailable.
76
+ const keyword = service.toApiList(service.search(query, { limit }));
77
+ if (mode === "keyword" || !embedder) {
78
+ sendJson(res, 200, { items: keyword, mode: "keyword" });
79
+ return;
80
+ }
81
+ const cfg = settings.getVectorConfig();
82
+ if (mode === "vector" && !cfg?.enabled) {
83
+ sendJson(res, 200, { items: keyword, mode: "keyword", error: "vector-disabled" });
84
+ return;
85
+ }
86
+ // Try vector search; on any failure fall back to keyword results.
87
+ return embedder.embed(query).then(async (vector) => {
88
+ let items = keyword;
89
+ let used = "keyword";
90
+ if (vector) {
91
+ const scored = service.toApiList(service.searchVector(vector, { limit }));
92
+ // Merge: keyword exact hits first (they are the user's literal
93
+ // words), then vector results fill the remaining slots, deduped.
94
+ const seen = new Set(keyword.map((m) => m.id));
95
+ const merged = [...keyword];
96
+ for (const m of scored) {
97
+ if (merged.length >= limit) break;
98
+ if (!seen.has(m.id)) {
99
+ seen.add(m.id);
100
+ merged.push(m);
101
+ }
102
+ }
103
+ items = merged;
104
+ used = "vector";
105
+ }
106
+ sendJson(res, 200, { items, mode: used });
107
+ }).catch(() => {
108
+ sendJson(res, 200, { items: keyword, mode: "keyword" });
109
+ });
110
+ } catch {
111
+ sendJson(res, 500, { error: "internal" });
112
+ }
113
+ }
114
+ });
115
+
116
+ // --- user profile ---
117
+ register({
118
+ kind: "exact",
119
+ path: "/api/dsh-mneme/profile",
120
+ handler(req, res) {
121
+ try {
122
+ if (req.method === "PUT" || req.method === "POST") {
123
+ return readBody(req).then((text) => {
124
+ const body = parseBody(text);
125
+ settings.setProfile(typeof body.profile === "string" ? body.profile : "");
126
+ sendJson(res, 200, { profile: settings.getProfile() });
127
+ });
128
+ }
129
+ sendJson(res, 200, { profile: settings.getProfile() });
130
+ } catch {
131
+ sendJson(res, 500, { error: "internal" });
132
+ }
133
+ }
134
+ });
135
+
136
+ // --- rules ---
137
+ register({
138
+ kind: "exact",
139
+ path: "/api/dsh-mneme/rules",
140
+ handler(req, res) {
141
+ try {
142
+ if (req.method === "PUT" || req.method === "POST") {
143
+ return readBody(req).then((text) => {
144
+ const body = parseBody(text);
145
+ settings.setRules(Array.isArray(body.rules) ? body.rules : []);
146
+ sendJson(res, 200, { rules: settings.getRules() });
147
+ });
148
+ }
149
+ sendJson(res, 200, { rules: settings.getRules() });
150
+ } catch {
151
+ sendJson(res, 500, { error: "internal" });
152
+ }
153
+ }
154
+ });
155
+
156
+ // --- vector search config ---
157
+ register({
158
+ kind: "exact",
159
+ path: "/api/dsh-mneme/vector-config",
160
+ handler(req, res) {
161
+ try {
162
+ if (req.method === "PUT" || req.method === "POST") {
163
+ return readBody(req).then((text) => {
164
+ const body = parseBody(text);
165
+ const cfg = settings.setVectorConfig({
166
+ enabled: body.enabled,
167
+ baseUrl: body.baseUrl,
168
+ apiKey: body.apiKey,
169
+ model: body.model
170
+ });
171
+ sendJson(res, 200, { config: cfg });
172
+ });
173
+ }
174
+ sendJson(res, 200, { config: settings.getVectorConfig() ?? { enabled: false, baseUrl: "", apiKey: "", model: "" } });
175
+ } catch {
176
+ sendJson(res, 500, { error: "internal" });
177
+ }
178
+ }
179
+ });
180
+
181
+ // --- vector re-index (backfill embeddings for rows missing them) ---
182
+ register({
183
+ kind: "exact",
184
+ path: "/api/dsh-mneme/vector-reindex",
185
+ handler(req, res) {
186
+ try {
187
+ if (!embedder) {
188
+ sendJson(res, 200, { indexed: 0, skipped: 0, error: "vector-unavailable" });
189
+ return;
190
+ }
191
+ const url = new URL(req.url, "http://localhost");
192
+ const limit = Number(url.searchParams.get("limit") ?? 100);
193
+ embedder.reindexMissing(limit).then((result) => {
194
+ sendJson(res, 200, result);
195
+ }).catch(() => {
196
+ sendJson(res, 200, { indexed: 0, skipped: 0, error: "vector-failed" });
197
+ });
198
+ } catch {
199
+ sendJson(res, 500, { error: "internal" });
200
+ }
201
+ }
202
+ });
203
+
204
+ // --- custom commands ---
205
+ register({
206
+ kind: "exact",
207
+ path: "/api/dsh-mneme/commands",
208
+ handler(req, res) {
209
+ try {
210
+ if (req.method === "POST") {
211
+ return readBody(req).then((text) => {
212
+ const body = parseBody(text);
213
+ try {
214
+ const command = commands.add({
215
+ name: body.name,
216
+ description: body.description,
217
+ instruction: body.instruction
218
+ });
219
+ sendJson(res, 200, { command });
220
+ } catch (error) {
221
+ sendJson(res, 400, { error: error.message });
222
+ }
223
+ });
224
+ }
225
+ if (req.method === "DELETE") {
226
+ const url = new URL(req.url, "http://localhost");
227
+ const id = url.searchParams.get("id");
228
+ const removed = id ? commands.remove(id) : false;
229
+ sendJson(res, 200, { removed });
230
+ return;
231
+ }
232
+ sendJson(res, 200, { commands: commands.list() });
47
233
  } catch {
48
234
  sendJson(res, 500, { error: "internal" });
49
235
  }
50
236
  }
51
- }));
237
+ });
52
238
 
53
239
  return {
54
- routes: 3,
240
+ routes: 6,
55
241
  dispose: () => {
56
242
  for (const dispose of disposers) dispose();
57
243
  }