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 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) console.warn("[auto-memory] action failed", response.status);
74
- await load();
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
- 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
- });
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.jsx)("p", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
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
- 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
- }));
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
- return (await fetch(`${BASE}/memory.read?${params}`)).json();
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
- * 关键设计决策(依据源码调研与对抗性审查,见 docs/api-reports.md):
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_\-]{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",
@@ -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 与 body——所有写路径(工具/固化/面板)都经 store.write,单点拦截。
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
- * 入库前剥离注入协议标签字样(防闭合逃逸伪造):任何文本中的 <memory_context>
244
- * 开闭标签转义为 &lt;…&gt;,使注入段永远是"一层壳",无法被内容嵌套穿透。
293
+ * 入库前剥离注入协议标签字样(防闭合逃逸伪造)。匹配放宽到容忍空白/大小写/
294
+ * 属性变体(LLM 分词器对这些变体的容忍度远高于词法精确匹配——审查确认
295
+ * 单空格即可穿透精确形式)。转义后壳永远只有一层,内容无法闭壳。
245
296
  */
246
297
  function stripMemoryTags(text) {
247
- return text.replace(/<\/?memory_context>/g, (match) => match.replace(/[<>]/g, (ch) => ch === "<" ? "&lt;" : "&gt;"));
298
+ return text.replace(/<\s*\/?\s*memory_context[^>]*>/gi, (match) => match.replace(/[<>]/g, (ch) => ch === "<" ? "&lt;" : "&gt;"));
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) => new Set(text.toLowerCase().split(/[^a-z0-9一-鿿]+/).filter((w) => w.length > 0));
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) break;
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.trim().length > 0 ? raw : null;
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
- ...body.title !== void 0 && body.title.trim().length > 0 ? { title: body.title.trim() } : {},
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(ctx, store, rootDir, { maxBytes: config.maxBytes });
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-auto-memory",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
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",