dsh-auto-memory 0.5.0 → 0.5.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.
- package/README.md +5 -1
- package/README.zh.md +5 -1
- package/lib/client.js +63 -28
- package/lib/index.js +35 -20
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -199,9 +199,13 @@ Override in your profile's `cordis.patch.yml` (config replaces wholesale):
|
|
|
199
199
|
```yaml
|
|
200
200
|
- id: auto-memory
|
|
201
201
|
config:
|
|
202
|
-
maxBytes: 4096 # injection budget
|
|
202
|
+
maxBytes: 4096 # injection budget (index + guidance + wrapper)
|
|
203
203
|
memoryDir: D:/memories # default: $DSH_HOME/memory
|
|
204
204
|
enableUserScope: true # false: user layer off on every path
|
|
205
|
+
autoSummarize: false # true: on root-session end, a background LLM files durable new facts
|
|
206
|
+
autoSummarizeMaxMemories: 5 # cap per consolidation
|
|
207
|
+
autoSummarizeMaxTokens: 2048 # consolidation output cap
|
|
208
|
+
staleAfterDays: 0 # >0: soft-hide zero-read memories older than N days (files kept)
|
|
205
209
|
```
|
|
206
210
|
|
|
207
211
|
## How it works (60 seconds)
|
package/README.zh.md
CHANGED
|
@@ -168,9 +168,13 @@ dsh --profile demo # 重启 profile 生效
|
|
|
168
168
|
```yaml
|
|
169
169
|
- id: auto-memory
|
|
170
170
|
config:
|
|
171
|
-
maxBytes: 4096 # 注入预算
|
|
171
|
+
maxBytes: 4096 # 注入预算(索引+指导文本+协议壳)
|
|
172
172
|
memoryDir: D:/memories # 默认: $DSH_HOME/memory
|
|
173
173
|
enableUserScope: true # false: 用户层在所有路径禁用
|
|
174
|
+
autoSummarize: false # true: 根会话结束时后台 LLM 提取持久事实写入
|
|
175
|
+
autoSummarizeMaxMemories: 5 # 单次固化上限
|
|
176
|
+
autoSummarizeMaxTokens: 2048 # 固化输出 token 上限
|
|
177
|
+
staleAfterDays: 0 # >0: 软隐藏超 N 天零引用记忆(文件保留)
|
|
174
178
|
```
|
|
175
179
|
|
|
176
180
|
## 工作原理(60 秒)
|
package/lib/client.js
CHANGED
|
@@ -77,18 +77,23 @@ window.__ModuleLoader__.load({
|
|
|
77
77
|
}
|
|
78
78
|
};
|
|
79
79
|
const cwd = (group) => group.scope === "project" && group.current === true ? currentCwd() : void 0;
|
|
80
|
-
/** 打开编辑器:先拉取正文(列表响应不含 body)
|
|
80
|
+
/** 打开编辑器:先拉取正文(列表响应不含 body);失败中止,绝不以空正文打开。 */
|
|
81
81
|
const openEditor = async (group, memory) => {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
82
|
+
try {
|
|
83
|
+
const detail = await read({
|
|
84
|
+
name: memory.name,
|
|
85
|
+
scope: group.scope,
|
|
86
|
+
cwd: cwd(group)
|
|
87
|
+
});
|
|
88
|
+
setEditing({
|
|
89
|
+
group,
|
|
90
|
+
memory,
|
|
91
|
+
body: detail.body
|
|
92
|
+
});
|
|
93
|
+
} catch (error) {
|
|
94
|
+
console.warn("[auto-memory] read failed, editor aborted", error);
|
|
95
|
+
await load();
|
|
96
|
+
}
|
|
92
97
|
};
|
|
93
98
|
if (error) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
94
99
|
style: { padding: "24px" },
|
|
@@ -169,6 +174,28 @@ window.__ModuleLoader__.load({
|
|
|
169
174
|
background: hot ? "#d97706" : "rgba(96,165,250,.9)"
|
|
170
175
|
} })
|
|
171
176
|
}),
|
|
177
|
+
group.indexText != null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("details", {
|
|
178
|
+
style: { margin: "6px 0" },
|
|
179
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("summary", {
|
|
180
|
+
style: {
|
|
181
|
+
cursor: "pointer",
|
|
182
|
+
fontSize: "12px",
|
|
183
|
+
opacity: .8
|
|
184
|
+
},
|
|
185
|
+
children: t("injectedPreview")
|
|
186
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", {
|
|
187
|
+
style: {
|
|
188
|
+
margin: "6px 0 0",
|
|
189
|
+
padding: "8px",
|
|
190
|
+
background: "rgba(127,127,127,0.12)",
|
|
191
|
+
borderRadius: "6px",
|
|
192
|
+
fontSize: "12px",
|
|
193
|
+
whiteSpace: "pre-wrap",
|
|
194
|
+
wordBreak: "break-word"
|
|
195
|
+
},
|
|
196
|
+
children: group.indexText
|
|
197
|
+
})]
|
|
198
|
+
}),
|
|
172
199
|
group.memories.map((memory) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
173
200
|
style: ROW_STYLE,
|
|
174
201
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
@@ -230,22 +257,26 @@ window.__ModuleLoader__.load({
|
|
|
230
257
|
disabled: busy,
|
|
231
258
|
onClick: () => {
|
|
232
259
|
(async () => {
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
260
|
+
try {
|
|
261
|
+
const detail = await read({
|
|
262
|
+
name: memory.name,
|
|
263
|
+
scope: group.scope,
|
|
264
|
+
cwd: cwd(group)
|
|
265
|
+
});
|
|
266
|
+
await act(() => write({
|
|
267
|
+
cwd: cwd(group),
|
|
268
|
+
scope: group.scope,
|
|
269
|
+
name: memory.name,
|
|
270
|
+
title: memory.title,
|
|
271
|
+
description: memory.description,
|
|
272
|
+
type: memory.type,
|
|
273
|
+
body: detail.body,
|
|
274
|
+
pinned: !memory.pinned
|
|
275
|
+
}));
|
|
276
|
+
} catch (error) {
|
|
277
|
+
console.warn("[auto-memory] read failed, pin aborted", error);
|
|
278
|
+
await load();
|
|
279
|
+
}
|
|
249
280
|
})();
|
|
250
281
|
},
|
|
251
282
|
children: memory.pinned ? t("unpinAction") : t("pinAction")
|
|
@@ -407,6 +438,7 @@ window.__ModuleLoader__.load({
|
|
|
407
438
|
pinned: "置顶",
|
|
408
439
|
reads: "次读取",
|
|
409
440
|
updated: "更新于",
|
|
441
|
+
injectedPreview: "下一次会话将注入的索引(点开预览)",
|
|
410
442
|
indexUsage: "索引占用",
|
|
411
443
|
budget: "注入预算",
|
|
412
444
|
of: "/",
|
|
@@ -440,6 +472,7 @@ window.__ModuleLoader__.load({
|
|
|
440
472
|
pinned: "pinned",
|
|
441
473
|
reads: "reads",
|
|
442
474
|
updated: "updated",
|
|
475
|
+
injectedPreview: "Index injected into the next session (click to preview)",
|
|
443
476
|
indexUsage: "Index usage",
|
|
444
477
|
budget: "injection budget",
|
|
445
478
|
of: "/",
|
|
@@ -487,7 +520,9 @@ window.__ModuleLoader__.load({
|
|
|
487
520
|
scope
|
|
488
521
|
});
|
|
489
522
|
if (cwd !== void 0) params.set("cwd", cwd);
|
|
490
|
-
|
|
523
|
+
const response = await fetch(`${BASE}/memory.read?${params}`);
|
|
524
|
+
if (!response.ok) throw new Error(`memory.read HTTP ${String(response.status)}`);
|
|
525
|
+
return await response.json();
|
|
491
526
|
},
|
|
492
527
|
write: (payload) => fetch(`${BASE}/memory.write`, {
|
|
493
528
|
method: "POST",
|
package/lib/index.js
CHANGED
|
@@ -207,7 +207,7 @@ const SECRET_PATTERNS = [
|
|
|
207
207
|
},
|
|
208
208
|
{
|
|
209
209
|
kind: "github-token",
|
|
210
|
-
re: /\bgh[pousr]_[A-Za-z0-9]{20,}\b/g
|
|
210
|
+
re: /\bgh[pousr]_[A-Za-z0-9]{20,}\b|\bgithub_pat_[A-Za-z0-9_]{20,}\b/g
|
|
211
211
|
},
|
|
212
212
|
{
|
|
213
213
|
kind: "npm-token",
|
|
@@ -215,11 +215,19 @@ const SECRET_PATTERNS = [
|
|
|
215
215
|
},
|
|
216
216
|
{
|
|
217
217
|
kind: "openai-key",
|
|
218
|
-
re: /\bsk-[A-Za-z0-9_
|
|
218
|
+
re: /\bsk-(?=[A-Za-z0-9_-]*[0-9])[A-Za-z0-9_-]{20,}\b/g
|
|
219
219
|
},
|
|
220
220
|
{
|
|
221
221
|
kind: "bearer",
|
|
222
|
-
re: /\
|
|
222
|
+
re: /\bbearer\s+(?:eyJ[A-Za-z0-9_-]{10,}|[A-Za-z0-9_\-]{16,}[=+/][A-Za-z0-9_\-=/+]{10,})/gi
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
kind: "slack-token",
|
|
226
|
+
re: /\bxox[bposa]-[0-9A-Za-z-]{10,}\b/g
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
kind: "google-key",
|
|
230
|
+
re: /\bAIza[0-9A-Za-z_\-]{30,}\b/g
|
|
223
231
|
},
|
|
224
232
|
{
|
|
225
233
|
kind: "phone",
|
|
@@ -240,11 +248,12 @@ function redactSecrets(text) {
|
|
|
240
248
|
return out;
|
|
241
249
|
}
|
|
242
250
|
/**
|
|
243
|
-
* 入库前剥离注入协议标签字样(防闭合逃逸伪造)
|
|
244
|
-
*
|
|
251
|
+
* 入库前剥离注入协议标签字样(防闭合逃逸伪造)。匹配放宽到容忍空白/大小写/
|
|
252
|
+
* 属性变体(LLM 分词器对这些变体的容忍度远高于词法精确匹配——审查确认
|
|
253
|
+
* 单空格即可穿透精确形式)。转义后壳永远只有一层,内容无法闭壳。
|
|
245
254
|
*/
|
|
246
255
|
function stripMemoryTags(text) {
|
|
247
|
-
return text.replace(
|
|
256
|
+
return text.replace(/<\s*\/?\s*memory_context[^>]*>/gi, (match) => match.replace(/[<>]/g, (ch) => ch === "<" ? "<" : ">"));
|
|
248
257
|
}
|
|
249
258
|
/** 写入内容统一清洗(脱敏 + 协议标签防护)。 */
|
|
250
259
|
function sanitizeContent(text) {
|
|
@@ -355,9 +364,13 @@ var MemoryStore = class {
|
|
|
355
364
|
await this.withLockRecovery(join(dir, INDEX_FILENAME), async () => {
|
|
356
365
|
const existing = parseMemory(await promises.readFile(file, "utf8").catch(() => ""), scope);
|
|
357
366
|
const pinned = record.pinned ?? existing?.pinned;
|
|
367
|
+
const importance = record.importance ?? existing?.importance;
|
|
368
|
+
const title = record.title ?? existing?.title;
|
|
358
369
|
written = {
|
|
359
370
|
...record,
|
|
360
371
|
...pinned === true ? { pinned: true } : {},
|
|
372
|
+
...importance !== void 0 ? { importance } : {},
|
|
373
|
+
...title !== void 0 ? { title: sanitizeContent(title) } : {},
|
|
361
374
|
description: sanitizeContent(record.description),
|
|
362
375
|
body: sanitizeContent(record.body),
|
|
363
376
|
...mergeLifecycleMeta(existing, Date.now()),
|
|
@@ -646,11 +659,11 @@ function registerMemoryTools(ctx, store, enableUserScope) {
|
|
|
646
659
|
const scope = guardScope(explicit ?? existing?.scope ?? "project");
|
|
647
660
|
await store.write({
|
|
648
661
|
name,
|
|
649
|
-
title: args.title !== void 0 && args.title.trim().length > 0 ? args.title.trim() : void 0,
|
|
662
|
+
title: args.title !== void 0 && args.title.trim().length > 0 ? args.title.trim().slice(0, 80) : void 0,
|
|
650
663
|
...args.pinned !== void 0 ? { pinned: args.pinned } : {},
|
|
651
|
-
description: args.description.trim(),
|
|
664
|
+
description: args.description.trim().slice(0, 160),
|
|
652
665
|
type: args.type,
|
|
653
|
-
body: args.body
|
|
666
|
+
body: args.body.slice(0, 2e3)
|
|
654
667
|
}, scope, cwd);
|
|
655
668
|
return {
|
|
656
669
|
name,
|
|
@@ -1133,7 +1146,7 @@ function renderMemoryIndexText(store, config, cwd) {
|
|
|
1133
1146
|
let size = 0;
|
|
1134
1147
|
for (const line of lines) {
|
|
1135
1148
|
const lineSize = Buffer.byteLength(line + "\n", "utf8");
|
|
1136
|
-
if (size + lineSize > budget)
|
|
1149
|
+
if (size + lineSize > budget) continue;
|
|
1137
1150
|
kept.push(line);
|
|
1138
1151
|
size += lineSize;
|
|
1139
1152
|
}
|
|
@@ -1364,18 +1377,18 @@ async function readGroup(dir, scope, slug) {
|
|
|
1364
1377
|
};
|
|
1365
1378
|
let entries;
|
|
1366
1379
|
try {
|
|
1367
|
-
entries = await promises.readdir(dir);
|
|
1380
|
+
entries = await promises.readdir(dir, { withFileTypes: true });
|
|
1368
1381
|
} catch {
|
|
1369
1382
|
return group;
|
|
1370
1383
|
}
|
|
1371
1384
|
for (const entry of entries) {
|
|
1372
|
-
if (!entry.endsWith(".md")) continue;
|
|
1373
|
-
const file = join(dir, entry);
|
|
1385
|
+
if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
|
|
1386
|
+
const file = join(dir, entry.name);
|
|
1374
1387
|
const raw = await promises.readFile(file, "utf8").catch(() => null);
|
|
1375
1388
|
if (raw === null) continue;
|
|
1376
|
-
if (entry.toLowerCase() === "MEMORY.md".toLowerCase()) {
|
|
1389
|
+
if (entry.name.toLowerCase() === "MEMORY.md".toLowerCase()) {
|
|
1377
1390
|
group.indexBytes = Buffer.byteLength(raw, "utf8");
|
|
1378
|
-
group.indexText = raw.
|
|
1391
|
+
group.indexText = raw.length > 0 ? raw.slice(0, 8192) : null;
|
|
1379
1392
|
continue;
|
|
1380
1393
|
}
|
|
1381
1394
|
const record = parseMemory(raw, scope);
|
|
@@ -1474,13 +1487,15 @@ function registerMemoryApi(ctx, store, rootDir, config) {
|
|
|
1474
1487
|
const cwd = body.scope === "project" ? body.cwd : void 0;
|
|
1475
1488
|
if (body.scope === "project" && (cwd === void 0 || cwd.length === 0)) return json({ error: "project scope requires cwd" }, 400);
|
|
1476
1489
|
const name = normalizeName(body.name);
|
|
1490
|
+
if (typeof body.description !== "string" || body.description.trim().length === 0) return json({ error: "description must be a non-empty string" }, 400);
|
|
1491
|
+
if (body.type !== "user" && body.type !== "feedback" && body.type !== "project" && body.type !== "reference") return json({ error: "invalid type" }, 400);
|
|
1477
1492
|
await store.write({
|
|
1478
1493
|
name,
|
|
1479
|
-
|
|
1494
|
+
title: typeof body.title === "string" && body.title.trim().length > 0 ? body.title.trim().slice(0, 80) : void 0,
|
|
1480
1495
|
...body.pinned !== void 0 ? { pinned: body.pinned } : {},
|
|
1481
|
-
description: body.description,
|
|
1496
|
+
description: body.description.trim().slice(0, 160),
|
|
1482
1497
|
type: body.type,
|
|
1483
|
-
body: body.body
|
|
1498
|
+
body: typeof body.body === "string" ? body.body.slice(0, 2e3) : ""
|
|
1484
1499
|
}, body.scope, cwd);
|
|
1485
1500
|
return json({
|
|
1486
1501
|
ok: true,
|
|
@@ -1533,8 +1548,8 @@ function apply(ctx, config) {
|
|
|
1533
1548
|
const rootDir = config.memoryDir !== void 0 && config.memoryDir.length > 0 ? resolve(config.memoryDir) : join(resolveDshHome(), "memory");
|
|
1534
1549
|
const store = new MemoryStore(rootDir, { staleAfterDays: config.staleAfterDays });
|
|
1535
1550
|
registerMemoryTools(ctx, store, config.enableUserScope);
|
|
1536
|
-
ctx.inject(["connection"], () => {
|
|
1537
|
-
registerMemoryApi(
|
|
1551
|
+
ctx.inject(["connection"], (fiberCtx) => {
|
|
1552
|
+
registerMemoryApi(fiberCtx, store, rootDir, { maxBytes: config.maxBytes });
|
|
1538
1553
|
});
|
|
1539
1554
|
registerConsolidation(ctx, store, {
|
|
1540
1555
|
autoSummarize: config.autoSummarize,
|
package/package.json
CHANGED