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 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
- const detail = await read({
83
- name: memory.name,
84
- scope: group.scope,
85
- cwd: cwd(group)
86
- });
87
- setEditing({
88
- group,
89
- memory,
90
- body: "body" in detail ? detail.body : ""
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
- const detail = await read({
234
- name: memory.name,
235
- scope: group.scope,
236
- cwd: cwd(group)
237
- });
238
- const bodyText = "body" in detail ? detail.body : "";
239
- await act(() => write({
240
- cwd: cwd(group),
241
- scope: group.scope,
242
- name: memory.name,
243
- title: memory.title,
244
- description: memory.description,
245
- type: memory.type,
246
- body: bodyText,
247
- pinned: !memory.pinned
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
- return (await fetch(`${BASE}/memory.read?${params}`)).json();
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_\-]{20,}\b/g
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: /\bBearer\s+[A-Za-z0-9_\-.=/+]{20,}\b/g
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
- * 入库前剥离注入协议标签字样(防闭合逃逸伪造):任何文本中的 <memory_context>
244
- * 开闭标签转义为 &lt;…&gt;,使注入段永远是"一层壳",无法被内容嵌套穿透。
251
+ * 入库前剥离注入协议标签字样(防闭合逃逸伪造)。匹配放宽到容忍空白/大小写/
252
+ * 属性变体(LLM 分词器对这些变体的容忍度远高于词法精确匹配——审查确认
253
+ * 单空格即可穿透精确形式)。转义后壳永远只有一层,内容无法闭壳。
245
254
  */
246
255
  function stripMemoryTags(text) {
247
- return text.replace(/<\/?memory_context>/g, (match) => match.replace(/[<>]/g, (ch) => ch === "<" ? "&lt;" : "&gt;"));
256
+ return text.replace(/<\s*\/?\s*memory_context[^>]*>/gi, (match) => match.replace(/[<>]/g, (ch) => ch === "<" ? "&lt;" : "&gt;"));
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) break;
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.trim().length > 0 ? raw : null;
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
- ...body.title !== void 0 && body.title.trim().length > 0 ? { title: body.title.trim() } : {},
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(ctx, store, rootDir, { maxBytes: config.maxBytes });
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-auto-memory",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
4
4
  "description": "Claude Code style auto-memory plugin for DeepSeek Harness: typed memory files + MEMORY.md index auto-injected into the system prompt",
5
5
  "type": "module",
6
6
  "license": "MIT",