dsh-memory-palace 1.8.4-alpha.1 → 1.8.4-alpha.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
@@ -60,8 +60,8 @@
60
60
 
61
61
  | 工具 | 层级 | 作用 |
62
62
  |---|---|---|
63
- | `memory_note` | 项目级 | 把约定/偏好写入当前项目全部目标 `MEMORY.md`(去重) |
64
- | `memory_note_user` | 用户级 | 把跨项目偏好写入 `~/.deepseek-harness/MEMORY.md`(去重) |
63
+ | `memory_note` | 项目级 | 章节化写入当前项目全部目标 `MEMORY.md`(同 `memory_write` 格式;`section` 可选,默认「项目约定」;同章节去重) |
64
+ | `memory_note_user` | 用户级 | 章节化写入 `~/.deepseek-harness/MEMORY.md`(与项目级同构;`section` 可选,默认「用户偏好」;同章节去重) |
65
65
  | `memory_read` | 按需 | 读取记忆。`scope` 默认 `memory`(用户级 + 项目级);可选 `project`(仅项目级,与 `memory_write` 的写侧 scope 同名)/ `today` / `yesterday` / `daily`(近三天日志)/ `all`(长期记忆 + 日志) |
66
66
  | `memory_delete` | 用户级/项目级/每日级 | 按内容删除记忆条目(两阶段确认:先预览匹配位置与内容,用户确认后再删;删除动作经 harness 原生确认弹窗硬闸门,真人点允许才真正执行) |
67
67
  | `memory_write` | 项目级/用户级 | 章节化写入:章节存在则末尾追加,不存在则新建章节(章节化格式规范) |
@@ -120,6 +120,9 @@ dsh plugin --profile web remove dsh-memory-palace
120
120
  ## 版本历史
121
121
 
122
122
  - **1.8.4**
123
+ - **1.8.4-alpha.2**
124
+ - 🐛用户自定义指令前缀多行指令粘连修复
125
+ - 💥移除插件用户记忆过时工具
123
126
  - **1.8.4-alpha.1**
124
127
  - 💥用户自定义指令落盘位置迁移
125
128
  - 💪memory_reorganize 指令支持中文
@@ -5,7 +5,10 @@
5
5
  // - 全部返回 { ok, reason, ... },永不抛错——LLM 产出的 ops 不可信,机器侧兜底;
6
6
  // - 只拆两级标题(`## `),`#` 文件标题与前言归 head,`###` 及更深归当前章节内容;
7
7
  // - text.mjs 的 normLine 不动;budgetClip 已在按 D2 修复重写(兑现「保留全部
8
- // 结构行」声明),锚点同步更新——本文件自持归一化,不依赖其内部实现。
8
+ // 结构行」声明),锚点同步更新——章节匹配与条目归一化自持,不依赖其内部实现。
9
+ // - sectionHasEntry 是本文件唯一的外部依赖点:条目去重口径必须与 text.mjs 的 normLine
10
+ // 完全一致(多工具共用),故显式 import 而非复制一份。
11
+ import { normLine } from "./text.mjs";
9
12
 
10
13
  // 章节标题归一化:剥 `#` 前缀、合空白、转小写。标题匹配全部走它,"带不带 ## 前缀"都鲁棒。
11
14
  const normTitle = (s) =>
@@ -86,6 +89,19 @@ export function locateSection(md, section) {
86
89
  // 处理:按行拆开,逐行剥行首列表符(- / *)与井号(#),去空行后以「;」连接为单行——
87
90
  // 内容零丢失、结构损坏向量消除。返回 { text, lines }(lines = 非空行数),调用方可在
88
91
  // message 里明示「已合并单行」。memory_write 与子代理 log_write_ops 共用,一处修两条路径闭环。
92
+ // 章节内是否已存在「同归一化」条目(精确去重,防手滑重复)。
93
+ // memory_write 与 memory_note 系列共用本判据——两个写入面口径必须一致,
94
+ // 否则同一个事实从不同工具写进去会各存一份。
95
+ export function sectionHasEntry(md, section, entry) {
96
+ const loc = locateSection(md, section);
97
+ if (!loc) return false;
98
+ const want = normLine(entry);
99
+ for (let i = loc.start + 1; i < loc.end; i++) {
100
+ if (normLine(loc.lines[i]) === want) return true;
101
+ }
102
+ return false;
103
+ }
104
+
89
105
  export function flattenEntry(entry) {
90
106
  const parts = String(entry ?? "")
91
107
  .split(/\r?\n/)
@@ -9,8 +9,8 @@ import { defineTool } from "@deepseek-ai/dsh-tools";
9
9
  import { writeFile, rename } from "node:fs/promises";
10
10
  import { join, dirname } from "node:path";
11
11
  import { readFileSync } from "node:fs";
12
- import { expandHome, toHomeShort, readMdSync, nowStamp, normLine } from "../common/text.mjs";
13
- import { appendToSectionText, upsertSectionText, replaceSectionText, markEntryDeletedText, locateSection } from "../common/sections.mjs";
12
+ import { expandHome, toHomeShort, readMdSync, nowStamp } from "../common/text.mjs";
13
+ import { appendToSectionText, upsertSectionText, replaceSectionText, markEntryDeletedText, locateSection, sectionHasEntry } from "../common/sections.mjs";
14
14
  import { planModeOf } from "../common/planmode.mjs";
15
15
 
16
16
  const REORG_MARK = "memory-palace:last-reorg:";
@@ -54,16 +54,8 @@ export function checkReorgGate(memFile, cfg) {
54
54
  };
55
55
  }
56
56
 
57
- // 同 normLine 精确去重:目标章节内已存在同归一化条目 → skip(不算去重管线,仅防手滑重复)。
58
- function sectionHasEntry(md, section, entry) {
59
- const loc = locateSection(md, section);
60
- if (!loc) return false;
61
- const want = normLine(entry);
62
- for (let i = loc.start + 1; i < loc.end; i++) {
63
- if (normLine(loc.lines[i]) === want) return true;
64
- }
65
- return false;
66
- }
57
+ // 同 normLine 精确去重由 common/sections.mjs 的 sectionHasEntry 提供
58
+ // (memory_write 与 memory_note 系列必须共用同一判据)。
67
59
 
68
60
  /**
69
61
  * 注册记忆工具(MEMORY.md 章节化写入 / 整章节替换 / 全量重整)+ pre-execute 闸门。
package/lib/index.js CHANGED
@@ -602,11 +602,13 @@ export function apply(ctx, config) {
602
602
  "- 用户级 MEMORY.md(跨项目):偏好与硬约束。可写——memory_note_user,或 memory_write / memory_update_section 的 scope='user'。\n" +
603
603
  "- 项目级 MEMORY.md(本项目):约定、决策、踩坑。可写——memory_note,或 memory_write / memory_update_section 的 scope='project'。\n" +
604
604
  "- 每日日志 memory/YYYY-MM-DD.md:记忆子代理每轮自动维护。你只读——不写、不改文件头、不重排。\n" +
605
+ "- 章节:按语义自己选章节名(章节不存在会自动新建),不要都塞进默认章节;memory_note / memory_note_user 省略 section 时落「项目约定」/「用户偏好」,仅当语义确实合适才省略。\n" +
605
606
  "当前项目已存在 WorkBuddy/CodeBuddy 记忆目录,本插件直接读写这些目录(不再单独创建 .deepseek-harness/memory/)。"
606
607
  : "[记忆文件]\n" +
607
608
  "- 用户级 ~/.deepseek-harness/MEMORY.md(跨项目):偏好与硬约束。可写——memory_note_user,或 memory_write / memory_update_section 的 scope='user'。\n" +
608
609
  "- 项目级 .deepseek-harness/MEMORY.md(项目根下):约定、决策、踩坑。可写——memory_note,或 memory_write / memory_update_section 的 scope='project'。\n" +
609
- "- 每日日志 .deepseek-harness/memory/YYYY-MM-DD.md(项目根下):记忆子代理每轮自动维护。你只读——不写、不改文件头、不重排。";
610
+ "- 每日日志 .deepseek-harness/memory/YYYY-MM-DD.md(项目根下):记忆子代理每轮自动维护。你只读——不写、不改文件头、不重排。\n" +
611
+ "- 章节:按语义自己选章节名(章节不存在会自动新建),不要都塞进默认章节;memory_note / memory_note_user 省略 section 时落「项目约定」/「用户偏好」,仅当语义确实合适才省略。";
610
612
 
611
613
  // 段二:读取 —— 与 [记忆文件] 同一套「标签:内容」行语法(每行都能竖着扫)。
612
614
  // ⚠ 重构只改排版,不动语义:工具名、条件、分支、举例必须逐字保留。
@@ -640,10 +642,11 @@ export function apply(ctx, config) {
640
642
  // 真源是 <profile>/.memory-palace/user-commands.md(用户手改或设置页编辑);
641
643
  // 内容恒定(改文件才变)→ 放 system 零缓存成本,不违反本节的分流原则。
642
644
  // readSync 内部按 mtimeMs 缓存,文件未变时不读盘。
645
+ // 前缀独占一行:多行指令若与前缀同行,首行会被当成前缀的一部分、后续行看起来游离在块外。
643
646
  const custom = userCommands.readSync().trim();
644
647
  // 本 section 已无任何动态块(日志迁出 E 投影),直接返回常量指令。
645
648
  // 返回值仍非空串(即便自定义指令为空也含 intro),避免 harness 对空 section 的处理歧义。
646
- return custom ? `${introFull}\n\n[用户自定义指令] ${custom}` : introFull;
649
+ return custom ? `${introFull}\n\n[用户自定义指令]\n${custom}` : introFull;
647
650
  },
648
651
  });
649
652
 
package/lib/tools.mjs CHANGED
@@ -1,10 +1,14 @@
1
1
  // memory-palace 记忆工具:memory_note / memory_note_user / memory_read / memory_delete
2
2
  // + 删除硬确认闸门(tools/pre-execute → harness 原生确认弹窗)。
3
+ // memory_note 系列与 memory_write 共用章节化写入(upsertSectionText + sectionHasEntry),
4
+ // 不再追加裸行——两个层级的条目格式与去重口径必须一致。
3
5
  // 经 registerTools 注入 ctx/config/paths/运行时状态;在 index.mjs 装配时调用一次。
4
6
  import { defineTool } from "@deepseek-ai/dsh-tools";
5
- import { join } from "node:path";
7
+ import { mkdir, writeFile } from "node:fs/promises";
8
+ import { join, dirname } from "node:path";
6
9
  import { todayISO, isoDaysAgo, expandHome, toHomeShort, readMdSync, budgetClip, stripDeletedLines } from "./common/text.mjs";
7
- import { appendLineDedup, findMatches, removeLineByMatch, recentLogDates } from "./common/records.mjs";
10
+ import { findMatches, removeLineByMatch, recentLogDates } from "./common/records.mjs";
11
+ import { upsertSectionText, sectionHasEntry } from "./common/sections.mjs";
8
12
  import { planModeOf } from "./common/planmode.mjs";
9
13
 
10
14
  /**
@@ -77,6 +81,8 @@ export function registerTools({ ctx, getConfig, paths, state }) {
77
81
  "key decision, or root-cause fix — or when the user explicitly asks you to remember it. " +
78
82
  "Do NOT log step-by-step work or one-off process (that belongs in the daily log). " +
79
83
  "Format: ONE concise line, conclusion first, objective third-person. " +
84
+ "It is appended under `## <section>` (created when absent) — the same chaptered format as " +
85
+ "memory_write; pass `section` to choose the chapter, default 「项目约定」. " +
80
86
  "CONTENT RULES: no source coordinates (`file.go:123`), no one-off material (error dumps, command output, " +
81
87
  "throwaway URLs); paths = reusable entry points only; URLs = must-reuse base addresses only. " +
82
88
  "For cross-project user-level memory, use memory_note_user instead.",
@@ -86,6 +92,10 @@ export function registerTools({ ctx, getConfig, paths, state }) {
86
92
  required: true,
87
93
  description: "The convention / preference / fact to remember, in one concise line.",
88
94
  },
95
+ section: {
96
+ type: "string",
97
+ description: "目标章节标题(不含 `## ` 前缀)。省略时落在「项目约定」章节;语义不符请显式指定。",
98
+ },
89
99
  },
90
100
  output: {
91
101
  schema: {
@@ -103,17 +113,32 @@ export function registerTools({ ctx, getConfig, paths, state }) {
103
113
  const cwd = sessionCwd(exec);
104
114
  const dirs = paths.writeDirs(cwd);
105
115
  if (!dirs.length) return { ok: false, message: "No active workspace." };
116
+ const entry = String(args.content ?? "").trim();
117
+ if (!entry) return { ok: false, message: "content 不能为空。" };
118
+ // 章节化:与 memory_write 同一套格式(`## 章节` + `- 条目`),默认落「项目约定」。
119
+ const section = String(args.section ?? "").trim() || "项目约定";
106
120
  try {
107
- const line = `- ${args.content}`;
108
121
  let wrote = 0;
109
122
  let skipped = 0;
110
123
  for (const dir of dirs) {
111
124
  const mem = paths.memoryFileOf(dir, cwd);
112
- if (await appendLineDedup(mem, line)) wrote++;
113
- else skipped++;
125
+ const md = readMdSync(mem);
126
+ if (sectionHasEntry(md, section, entry)) {
127
+ skipped++;
128
+ continue;
129
+ }
130
+ const r = upsertSectionText(md, section, entry);
131
+ if (!r.ok) {
132
+ skipped++;
133
+ continue;
134
+ }
135
+ // 目录可能尚不存在(首次写入 / 新建 dsh 目录)——章节化写入用 writeFile,须自行建目录。
136
+ await mkdir(dirname(mem), { recursive: true });
137
+ await writeFile(mem, r.text, "utf8");
138
+ wrote++;
114
139
  }
115
140
  const dup = skipped > 0 ? ` (${skipped} already had it)` : "";
116
- return { ok: true, message: `Saved to ${wrote} workspace MEMORY.md file(s)${dup}.` };
141
+ return { ok: true, message: `Saved to ${wrote} workspace MEMORY.md file(s) under 「${section}」${dup}.` };
117
142
  } catch (e) {
118
143
  return { ok: false, message: `Failed: ${e}` };
119
144
  }
@@ -132,6 +157,8 @@ export function registerTools({ ctx, getConfig, paths, state }) {
132
157
  "a personal preference / constraint, or a reusable fact / decision — or when the user explicitly " +
133
158
  "asks you to remember something that is not tied to this project. Not for one-off process. " +
134
159
  "Format: ONE concise line, conclusion first. " +
160
+ "It is appended under `## <section>` (created when absent) — the same chaptered format as " +
161
+ "memory_write; pass `section` to choose the chapter, default 「用户偏好」. " +
135
162
  "CONTENT RULES: no source coordinates (`file.go:123`), no one-off material (error dumps, command output, " +
136
163
  "throwaway URLs); paths = reusable entry points only; URLs = must-reuse base addresses only. " +
137
164
  "For current-project conventions, use memory_note instead.",
@@ -141,6 +168,10 @@ export function registerTools({ ctx, getConfig, paths, state }) {
141
168
  required: true,
142
169
  description: "The user-level preference / fact to remember, in one concise line.",
143
170
  },
171
+ section: {
172
+ type: "string",
173
+ description: "目标章节标题(不含 `## ` 前缀)。省略时落在「用户偏好」章节;语义不符请显式指定。",
174
+ },
144
175
  },
145
176
  output: {
146
177
  schema: {
@@ -156,17 +187,23 @@ export function registerTools({ ctx, getConfig, paths, state }) {
156
187
  async execute(args) {
157
188
  const c = cfg();
158
189
  if (!c.enabled) return { ok: false, message: "memory-palace is currently disabled in settings." };
190
+ const entry = String(args.content ?? "").trim();
191
+ if (!entry) return { ok: false, message: "content 不能为空。" };
192
+ // 章节化:与项目级 / memory_write 同一套格式,默认落「用户偏好」。
193
+ const section = String(args.section ?? "").trim() || "用户偏好";
159
194
  try {
160
195
  const file = expandHome(c.userMemoryPath);
161
196
  const fileShort = toHomeShort(file);
162
- const line = `- ${args.content}`;
163
- const wrote = await appendLineDedup(file, line);
164
- return {
165
- ok: true,
166
- message: wrote
167
- ? `Saved to user-level memory (${fileShort}).`
168
- : `Already present in user-level memory (${fileShort}); skipped.`,
169
- };
197
+ // 用户级目录可能尚不存在(首次写入)——mkdir 由本路径负责。
198
+ await mkdir(dirname(file), { recursive: true });
199
+ const md = readMdSync(file);
200
+ if (sectionHasEntry(md, section, entry)) {
201
+ return { ok: true, message: `Already present in user-level memory (${fileShort}); skipped.` };
202
+ }
203
+ const r = upsertSectionText(md, section, entry);
204
+ if (!r.ok) return { ok: false, message: `Failed: ${r.reason}` };
205
+ await writeFile(file, r.text, "utf8");
206
+ return { ok: true, message: `Saved to user-level memory (${fileShort}) under 「${section}」.` };
170
207
  } catch (e) {
171
208
  return { ok: false, message: `Failed: ${e}` };
172
209
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-memory-palace",
3
- "version": "1.8.4-alpha.1",
3
+ "version": "1.8.4-alpha.2",
4
4
  "description": "把 WorkBuddy 的文件式记忆系统移植进 DeepSeek Harness:用户级 + 工作区的人类可读 Markdown 记忆(~/.deepseek-harness/MEMORY.md 与 <cwd>/.deepseek-harness/memory/)。",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",