dsh-auto-memory 0.5.0 → 0.5.2
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 -2
- package/README.zh.md +5 -2
- package/lib/client.js +81 -33
- package/lib/index.js +91 -24
- 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)
|
|
@@ -216,7 +220,6 @@ Override in your profile's `cordis.patch.yml` (config replaces wholesale):
|
|
|
216
220
|
resume); everything flows through standard `tool/call` / `tool/result`.
|
|
217
221
|
|
|
218
222
|
Deep dives: [design decisions](docs/design.md) ·
|
|
219
|
-
[dsh source-level research](docs/api-reports.md) ·
|
|
220
223
|
[postmortem: shipping a PR to awesome-dsh-plugin](docs/postmortem-pr-5696.md)
|
|
221
224
|
|
|
222
225
|
## Roadmap
|
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 秒)
|
|
@@ -183,7 +187,6 @@ dsh --profile demo # 重启 profile 生效
|
|
|
183
187
|
一切走标准 `tool/call` / `tool/result`。
|
|
184
188
|
|
|
185
189
|
深度内容:[设计决策](docs/design.md) ·
|
|
186
|
-
[dsh 源码级调研](docs/api-reports.md) ·
|
|
187
190
|
[复盘:向 awesome-dsh-plugin 提 PR](docs/postmortem-pr-5696.md)
|
|
188
191
|
|
|
189
192
|
## 路线图
|
package/lib/client.js
CHANGED
|
@@ -51,6 +51,7 @@ window.__ModuleLoader__.load({
|
|
|
51
51
|
const [groups, setGroups] = (0, react.useState)(null);
|
|
52
52
|
const [maxBytes, setMaxBytes] = (0, react.useState)(4096);
|
|
53
53
|
const [error, setError] = (0, react.useState)(false);
|
|
54
|
+
const [actionError, setActionError] = (0, react.useState)(null);
|
|
54
55
|
const [editing, setEditing] = (0, react.useState)(null);
|
|
55
56
|
const [busy, setBusy] = (0, react.useState)(false);
|
|
56
57
|
const load = (0, react.useCallback)(async () => {
|
|
@@ -68,27 +69,35 @@ window.__ModuleLoader__.load({
|
|
|
68
69
|
}, [load]);
|
|
69
70
|
const act = async (fn) => {
|
|
70
71
|
setBusy(true);
|
|
72
|
+
setActionError(null);
|
|
71
73
|
try {
|
|
72
74
|
const response = await fn();
|
|
73
|
-
if (!response.ok)
|
|
74
|
-
|
|
75
|
+
if (!response.ok) setActionError(response.status === 404 ? t("deleteFailed") : t("saveFailed"));
|
|
76
|
+
} catch {
|
|
77
|
+
setActionError(t("saveFailed"));
|
|
75
78
|
} finally {
|
|
76
79
|
setBusy(false);
|
|
80
|
+
await load();
|
|
77
81
|
}
|
|
78
82
|
};
|
|
79
83
|
const cwd = (group) => group.scope === "project" && group.current === true ? currentCwd() : void 0;
|
|
80
|
-
/** 打开编辑器:先拉取正文(列表响应不含 body)
|
|
84
|
+
/** 打开编辑器:先拉取正文(列表响应不含 body);失败中止,绝不以空正文打开。 */
|
|
81
85
|
const openEditor = async (group, memory) => {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
86
|
+
try {
|
|
87
|
+
const detail = await read({
|
|
88
|
+
name: memory.name,
|
|
89
|
+
scope: group.scope,
|
|
90
|
+
cwd: cwd(group)
|
|
91
|
+
});
|
|
92
|
+
setEditing({
|
|
93
|
+
group,
|
|
94
|
+
memory,
|
|
95
|
+
body: detail.body
|
|
96
|
+
});
|
|
97
|
+
} catch (error) {
|
|
98
|
+
console.warn("[auto-memory] read failed, editor aborted", error);
|
|
99
|
+
await load();
|
|
100
|
+
}
|
|
92
101
|
};
|
|
93
102
|
if (error) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
94
103
|
style: { padding: "24px" },
|
|
@@ -115,13 +124,19 @@ window.__ModuleLoader__.load({
|
|
|
115
124
|
style: { opacity: .75 },
|
|
116
125
|
children: t("intro")
|
|
117
126
|
}),
|
|
118
|
-
/* @__PURE__ */ (0, react_jsx_runtime.
|
|
127
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
119
128
|
onClick: () => {
|
|
120
129
|
load();
|
|
121
130
|
},
|
|
122
131
|
disabled: busy,
|
|
123
132
|
children: t("refresh")
|
|
124
|
-
})
|
|
133
|
+
}), actionError !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
134
|
+
style: {
|
|
135
|
+
marginLeft: "12px",
|
|
136
|
+
color: "#d97706"
|
|
137
|
+
},
|
|
138
|
+
children: actionError
|
|
139
|
+
})] }),
|
|
125
140
|
total === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("empty") }),
|
|
126
141
|
groups.map((group) => {
|
|
127
142
|
const usage = group.indexBytes / maxBytes;
|
|
@@ -169,6 +184,28 @@ window.__ModuleLoader__.load({
|
|
|
169
184
|
background: hot ? "#d97706" : "rgba(96,165,250,.9)"
|
|
170
185
|
} })
|
|
171
186
|
}),
|
|
187
|
+
group.indexText != null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("details", {
|
|
188
|
+
style: { margin: "6px 0" },
|
|
189
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("summary", {
|
|
190
|
+
style: {
|
|
191
|
+
cursor: "pointer",
|
|
192
|
+
fontSize: "12px",
|
|
193
|
+
opacity: .8
|
|
194
|
+
},
|
|
195
|
+
children: t("injectedPreview")
|
|
196
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", {
|
|
197
|
+
style: {
|
|
198
|
+
margin: "6px 0 0",
|
|
199
|
+
padding: "8px",
|
|
200
|
+
background: "rgba(127,127,127,0.12)",
|
|
201
|
+
borderRadius: "6px",
|
|
202
|
+
fontSize: "12px",
|
|
203
|
+
whiteSpace: "pre-wrap",
|
|
204
|
+
wordBreak: "break-word"
|
|
205
|
+
},
|
|
206
|
+
children: group.indexText
|
|
207
|
+
})]
|
|
208
|
+
}),
|
|
172
209
|
group.memories.map((memory) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
173
210
|
style: ROW_STYLE,
|
|
174
211
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
@@ -230,22 +267,26 @@ window.__ModuleLoader__.load({
|
|
|
230
267
|
disabled: busy,
|
|
231
268
|
onClick: () => {
|
|
232
269
|
(async () => {
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
270
|
+
try {
|
|
271
|
+
const detail = await read({
|
|
272
|
+
name: memory.name,
|
|
273
|
+
scope: group.scope,
|
|
274
|
+
cwd: cwd(group)
|
|
275
|
+
});
|
|
276
|
+
await act(() => write({
|
|
277
|
+
cwd: cwd(group),
|
|
278
|
+
scope: group.scope,
|
|
279
|
+
name: memory.name,
|
|
280
|
+
title: memory.title,
|
|
281
|
+
description: memory.description,
|
|
282
|
+
type: memory.type,
|
|
283
|
+
body: detail.body,
|
|
284
|
+
pinned: !memory.pinned
|
|
285
|
+
}));
|
|
286
|
+
} catch (error) {
|
|
287
|
+
console.warn("[auto-memory] read failed, pin aborted", error);
|
|
288
|
+
await load();
|
|
289
|
+
}
|
|
249
290
|
})();
|
|
250
291
|
},
|
|
251
292
|
children: memory.pinned ? t("unpinAction") : t("pinAction")
|
|
@@ -272,6 +313,7 @@ window.__ModuleLoader__.load({
|
|
|
272
313
|
t,
|
|
273
314
|
initial: editing.memory,
|
|
274
315
|
initialBody: editing.body,
|
|
316
|
+
busy,
|
|
275
317
|
onCancel: () => {
|
|
276
318
|
setEditing(null);
|
|
277
319
|
},
|
|
@@ -288,7 +330,7 @@ window.__ModuleLoader__.load({
|
|
|
288
330
|
});
|
|
289
331
|
}
|
|
290
332
|
/** 单条记忆编辑器(标题/摘要/正文,保存即重建索引、下次注入生效)。 */
|
|
291
|
-
function MemoryEditor({ t, initial, initialBody, onSave, onCancel }) {
|
|
333
|
+
function MemoryEditor({ t, initial, initialBody, busy, onSave, onCancel }) {
|
|
292
334
|
const [title, setTitle] = (0, react.useState)(initial.title ?? "");
|
|
293
335
|
const [description, setDescription] = (0, react.useState)(initial.description);
|
|
294
336
|
const [body, setBody] = (0, react.useState)(initialBody);
|
|
@@ -352,6 +394,7 @@ window.__ModuleLoader__.load({
|
|
|
352
394
|
style: { margin: "8px 0 0" },
|
|
353
395
|
children: [
|
|
354
396
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
397
|
+
disabled: busy,
|
|
355
398
|
onClick: () => {
|
|
356
399
|
onSave({
|
|
357
400
|
name: initial.name,
|
|
@@ -366,6 +409,7 @@ window.__ModuleLoader__.load({
|
|
|
366
409
|
}),
|
|
367
410
|
" ",
|
|
368
411
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
412
|
+
disabled: busy,
|
|
369
413
|
onClick: onCancel,
|
|
370
414
|
children: t("cancel")
|
|
371
415
|
})
|
|
@@ -407,6 +451,7 @@ window.__ModuleLoader__.load({
|
|
|
407
451
|
pinned: "置顶",
|
|
408
452
|
reads: "次读取",
|
|
409
453
|
updated: "更新于",
|
|
454
|
+
injectedPreview: "下一次会话将注入的索引(点开预览)",
|
|
410
455
|
indexUsage: "索引占用",
|
|
411
456
|
budget: "注入预算",
|
|
412
457
|
of: "/",
|
|
@@ -440,6 +485,7 @@ window.__ModuleLoader__.load({
|
|
|
440
485
|
pinned: "pinned",
|
|
441
486
|
reads: "reads",
|
|
442
487
|
updated: "updated",
|
|
488
|
+
injectedPreview: "Index injected into the next session (click to preview)",
|
|
443
489
|
indexUsage: "Index usage",
|
|
444
490
|
budget: "injection budget",
|
|
445
491
|
of: "/",
|
|
@@ -487,7 +533,9 @@ window.__ModuleLoader__.load({
|
|
|
487
533
|
scope
|
|
488
534
|
});
|
|
489
535
|
if (cwd !== void 0) params.set("cwd", cwd);
|
|
490
|
-
|
|
536
|
+
const response = await fetch(`${BASE}/memory.read?${params}`);
|
|
537
|
+
if (!response.ok) throw new Error(`memory.read HTTP ${String(response.status)}`);
|
|
538
|
+
return await response.json();
|
|
491
539
|
},
|
|
492
540
|
write: (payload) => fetch(`${BASE}/memory.write`, {
|
|
493
541
|
method: "POST",
|
package/lib/index.js
CHANGED
|
@@ -14,7 +14,7 @@ const USER_SCOPE_DIR = "_user";
|
|
|
14
14
|
/**
|
|
15
15
|
* 记忆存储层:类型化记忆文件 CRUD + MEMORY.md 索引维护。
|
|
16
16
|
*
|
|
17
|
-
* 关键设计决策(依据源码调研与对抗性审查,见
|
|
17
|
+
* 关键设计决策(依据源码调研与对抗性审查,见 internal research notes):
|
|
18
18
|
* 1. **必须用 node:fs 而非 ctx.fs**——默认部署的 fs-sandbox 可写根不含 $DSH_HOME,
|
|
19
19
|
* 走 ctx.fs 会抛 FS_SANDBOX_DENIED;官方先例 skill-filesystem 访问 $DSH_HOME
|
|
20
20
|
* 下的文件同样直接用 node:fs。
|
|
@@ -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",
|
|
@@ -231,20 +239,63 @@ const SECRET_PATTERNS = [
|
|
|
231
239
|
}
|
|
232
240
|
];
|
|
233
241
|
/**
|
|
242
|
+
* GB11643 身份证校验位验证:18 位号码仅当末位与加权模 11 校验相符才算真身份证。
|
|
243
|
+
* 纯长度匹配会把订单号/雪花 ID 等任意 18 位数字一并误杀(#7)。
|
|
244
|
+
*/
|
|
245
|
+
function isValidIdCard(candidate) {
|
|
246
|
+
if (!/^\d{17}[\dXx]$/.test(candidate)) return false;
|
|
247
|
+
const WEIGHTS = [
|
|
248
|
+
7,
|
|
249
|
+
9,
|
|
250
|
+
10,
|
|
251
|
+
5,
|
|
252
|
+
8,
|
|
253
|
+
4,
|
|
254
|
+
2,
|
|
255
|
+
1,
|
|
256
|
+
6,
|
|
257
|
+
3,
|
|
258
|
+
7,
|
|
259
|
+
9,
|
|
260
|
+
10,
|
|
261
|
+
5,
|
|
262
|
+
8,
|
|
263
|
+
4,
|
|
264
|
+
2
|
|
265
|
+
];
|
|
266
|
+
const CHECKS = [
|
|
267
|
+
"1",
|
|
268
|
+
"0",
|
|
269
|
+
"X",
|
|
270
|
+
"9",
|
|
271
|
+
"8",
|
|
272
|
+
"7",
|
|
273
|
+
"6",
|
|
274
|
+
"5",
|
|
275
|
+
"4",
|
|
276
|
+
"3",
|
|
277
|
+
"2"
|
|
278
|
+
];
|
|
279
|
+
let sum = 0;
|
|
280
|
+
for (let i = 0; i < 17; i++) sum += Number(candidate[i]) * WEIGHTS[i];
|
|
281
|
+
return candidate[17].toUpperCase() === CHECKS[sum % 11];
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
234
284
|
* 写入前脱敏(纯函数,可测):把高置信密钥/手机号/身份证替换为 [REDACTED:kind]。
|
|
235
|
-
* 应用于 description
|
|
285
|
+
* 应用于 description/body——所有写路径(工具/固化/面板)都经 store.write,单点拦截。
|
|
236
286
|
*/
|
|
237
287
|
function redactSecrets(text) {
|
|
238
288
|
let out = text;
|
|
239
|
-
for (const { kind, re } of SECRET_PATTERNS) out = out.replace(re, `[REDACTED:${kind}]`);
|
|
289
|
+
for (const { kind, re } of SECRET_PATTERNS) out = out.replace(re, (match) => kind === "id-card" && !isValidIdCard(match) ? match : `[REDACTED:${kind}]`);
|
|
240
290
|
return out;
|
|
241
291
|
}
|
|
242
292
|
/**
|
|
243
|
-
* 入库前剥离注入协议标签字样(防闭合逃逸伪造)
|
|
244
|
-
*
|
|
293
|
+
* 入库前剥离注入协议标签字样(防闭合逃逸伪造)。匹配放宽到容忍空白/大小写/
|
|
294
|
+
* 属性变体(LLM 分词器对这些变体的容忍度远高于词法精确匹配——审查确认
|
|
295
|
+
* 单空格即可穿透精确形式)。转义后壳永远只有一层,内容无法闭壳。
|
|
245
296
|
*/
|
|
246
297
|
function stripMemoryTags(text) {
|
|
247
|
-
return text.replace(
|
|
298
|
+
return text.replace(/<\s*\/?\s*memory_context[^>]*>/gi, (match) => match.replace(/[<>]/g, (ch) => ch === "<" ? "<" : ">"));
|
|
248
299
|
}
|
|
249
300
|
/** 写入内容统一清洗(脱敏 + 协议标签防护)。 */
|
|
250
301
|
function sanitizeContent(text) {
|
|
@@ -271,9 +322,19 @@ function memoryScore(record, nowMs) {
|
|
|
271
322
|
/**
|
|
272
323
|
* 描述相似度(Jaccard 词集,纯函数,可测):固化防回声的查重依据——
|
|
273
324
|
* "既有记忆的复述不是新信息",相似度过高的候选跳过而非堆积。
|
|
325
|
+
* CJK 感知:连续中文段按字符二元组(bigram)切分(检索领域标准做法)——
|
|
326
|
+
* 整段单 token 会让中文近重复仅得 ~0.4,防回声对中文完全失效(#5)。
|
|
274
327
|
*/
|
|
275
328
|
function descriptionSimilarity(a, b) {
|
|
276
|
-
const words = (text) =>
|
|
329
|
+
const words = (text) => {
|
|
330
|
+
const tokens = text.toLowerCase().split(/[^a-z0-9一-鿿]+/).filter((w) => w.length > 0);
|
|
331
|
+
const out = /* @__PURE__ */ new Set();
|
|
332
|
+
for (const token of tokens) if (/^[一-鿿]+$/.test(token)) {
|
|
333
|
+
if (token.length === 1) out.add(token);
|
|
334
|
+
else for (let i = 0; i + 1 < token.length; i++) out.add(token.slice(i, i + 2));
|
|
335
|
+
} else out.add(token);
|
|
336
|
+
return out;
|
|
337
|
+
};
|
|
277
338
|
const wa = words(a);
|
|
278
339
|
const wb = words(b);
|
|
279
340
|
if (wa.size === 0 || wb.size === 0) return 0;
|
|
@@ -355,9 +416,13 @@ var MemoryStore = class {
|
|
|
355
416
|
await this.withLockRecovery(join(dir, INDEX_FILENAME), async () => {
|
|
356
417
|
const existing = parseMemory(await promises.readFile(file, "utf8").catch(() => ""), scope);
|
|
357
418
|
const pinned = record.pinned ?? existing?.pinned;
|
|
419
|
+
const importance = record.importance ?? existing?.importance;
|
|
420
|
+
const title = record.title ?? existing?.title;
|
|
358
421
|
written = {
|
|
359
422
|
...record,
|
|
360
423
|
...pinned === true ? { pinned: true } : {},
|
|
424
|
+
...importance !== void 0 ? { importance } : {},
|
|
425
|
+
...title !== void 0 ? { title: sanitizeContent(title) } : {},
|
|
361
426
|
description: sanitizeContent(record.description),
|
|
362
427
|
body: sanitizeContent(record.body),
|
|
363
428
|
...mergeLifecycleMeta(existing, Date.now()),
|
|
@@ -646,11 +711,11 @@ function registerMemoryTools(ctx, store, enableUserScope) {
|
|
|
646
711
|
const scope = guardScope(explicit ?? existing?.scope ?? "project");
|
|
647
712
|
await store.write({
|
|
648
713
|
name,
|
|
649
|
-
title: args.title !== void 0 && args.title.trim().length > 0 ? args.title.trim() : void 0,
|
|
714
|
+
title: args.title !== void 0 && args.title.trim().length > 0 ? args.title.trim().slice(0, 80) : void 0,
|
|
650
715
|
...args.pinned !== void 0 ? { pinned: args.pinned } : {},
|
|
651
|
-
description: args.description.trim(),
|
|
716
|
+
description: args.description.trim().slice(0, 160),
|
|
652
717
|
type: args.type,
|
|
653
|
-
body: args.body
|
|
718
|
+
body: args.body.slice(0, 2e3)
|
|
654
719
|
}, scope, cwd);
|
|
655
720
|
return {
|
|
656
721
|
name,
|
|
@@ -1133,7 +1198,7 @@ function renderMemoryIndexText(store, config, cwd) {
|
|
|
1133
1198
|
let size = 0;
|
|
1134
1199
|
for (const line of lines) {
|
|
1135
1200
|
const lineSize = Buffer.byteLength(line + "\n", "utf8");
|
|
1136
|
-
if (size + lineSize > budget)
|
|
1201
|
+
if (size + lineSize > budget) continue;
|
|
1137
1202
|
kept.push(line);
|
|
1138
1203
|
size += lineSize;
|
|
1139
1204
|
}
|
|
@@ -1364,18 +1429,18 @@ async function readGroup(dir, scope, slug) {
|
|
|
1364
1429
|
};
|
|
1365
1430
|
let entries;
|
|
1366
1431
|
try {
|
|
1367
|
-
entries = await promises.readdir(dir);
|
|
1432
|
+
entries = await promises.readdir(dir, { withFileTypes: true });
|
|
1368
1433
|
} catch {
|
|
1369
1434
|
return group;
|
|
1370
1435
|
}
|
|
1371
1436
|
for (const entry of entries) {
|
|
1372
|
-
if (!entry.endsWith(".md")) continue;
|
|
1373
|
-
const file = join(dir, entry);
|
|
1437
|
+
if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
|
|
1438
|
+
const file = join(dir, entry.name);
|
|
1374
1439
|
const raw = await promises.readFile(file, "utf8").catch(() => null);
|
|
1375
1440
|
if (raw === null) continue;
|
|
1376
|
-
if (entry.toLowerCase() === "MEMORY.md".toLowerCase()) {
|
|
1441
|
+
if (entry.name.toLowerCase() === "MEMORY.md".toLowerCase()) {
|
|
1377
1442
|
group.indexBytes = Buffer.byteLength(raw, "utf8");
|
|
1378
|
-
group.indexText = raw.
|
|
1443
|
+
group.indexText = raw.length > 0 ? raw.slice(0, 8192) : null;
|
|
1379
1444
|
continue;
|
|
1380
1445
|
}
|
|
1381
1446
|
const record = parseMemory(raw, scope);
|
|
@@ -1474,13 +1539,15 @@ function registerMemoryApi(ctx, store, rootDir, config) {
|
|
|
1474
1539
|
const cwd = body.scope === "project" ? body.cwd : void 0;
|
|
1475
1540
|
if (body.scope === "project" && (cwd === void 0 || cwd.length === 0)) return json({ error: "project scope requires cwd" }, 400);
|
|
1476
1541
|
const name = normalizeName(body.name);
|
|
1542
|
+
if (typeof body.description !== "string" || body.description.trim().length === 0) return json({ error: "description must be a non-empty string" }, 400);
|
|
1543
|
+
if (body.type !== "user" && body.type !== "feedback" && body.type !== "project" && body.type !== "reference") return json({ error: "invalid type" }, 400);
|
|
1477
1544
|
await store.write({
|
|
1478
1545
|
name,
|
|
1479
|
-
|
|
1546
|
+
title: typeof body.title === "string" && body.title.trim().length > 0 ? body.title.trim().slice(0, 80) : void 0,
|
|
1480
1547
|
...body.pinned !== void 0 ? { pinned: body.pinned } : {},
|
|
1481
|
-
description: body.description,
|
|
1548
|
+
description: body.description.trim().slice(0, 160),
|
|
1482
1549
|
type: body.type,
|
|
1483
|
-
body: body.body
|
|
1550
|
+
body: typeof body.body === "string" ? body.body.slice(0, 2e3) : ""
|
|
1484
1551
|
}, body.scope, cwd);
|
|
1485
1552
|
return json({
|
|
1486
1553
|
ok: true,
|
|
@@ -1533,8 +1600,8 @@ function apply(ctx, config) {
|
|
|
1533
1600
|
const rootDir = config.memoryDir !== void 0 && config.memoryDir.length > 0 ? resolve(config.memoryDir) : join(resolveDshHome(), "memory");
|
|
1534
1601
|
const store = new MemoryStore(rootDir, { staleAfterDays: config.staleAfterDays });
|
|
1535
1602
|
registerMemoryTools(ctx, store, config.enableUserScope);
|
|
1536
|
-
ctx.inject(["connection"], () => {
|
|
1537
|
-
registerMemoryApi(
|
|
1603
|
+
ctx.inject(["connection"], (fiberCtx) => {
|
|
1604
|
+
registerMemoryApi(fiberCtx, store, rootDir, { maxBytes: config.maxBytes });
|
|
1538
1605
|
});
|
|
1539
1606
|
registerConsolidation(ctx, store, {
|
|
1540
1607
|
autoSummarize: config.autoSummarize,
|
package/package.json
CHANGED