@yandy0725/pi-memory 1.3.0 → 1.3.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
@@ -6,11 +6,11 @@ Aligned with Claude Code's auto memory mechanism: per-topic MEMORY.md index, aut
6
6
 
7
7
  ## Features
8
8
 
9
- - **One `memory` tool**, four actions: `add` (append entry), `remove` (delete entry by title), `read` (load topic or entry), `search` (query memory or session history)
9
+ - **One `memory` tool**, three actions: `add` (append entry), `remove` (delete entry by title), `search` (query memory or session history)
10
10
  - **Topic-based file organization**: each `memory add` writes a `## entry` block to a named `.md` file under the project's memory directory
11
11
  - **`MEMORY.md` index** — one compact line per topic file with a relevance hook: `- [Name](file.md) — summary`
12
12
  - **Memory types**: four categories — `user`, `feedback` (default), `project`, `reference` — stored in topic file frontmatter
13
- - **Auto-surfacing** ⭐: on every user message, a side-query LLM selects up to N relevant topic files and injects their content into the agent's context. No manual `memory read` needed. Session-level deduplication prevents re-injecting the same topic.
13
+ - **Auto-surfacing** ⭐: on every user message, a side-query LLM selects up to N relevant topic files and injects their content into the agent's context. No manual `read` needed — use the built-in `read` tool to inspect memory files. Session-level deduplication prevents re-injecting the same topic.
14
14
  - **Extract memories** ⭐: after each agent run, an async subagent analyzes the conversation and automatically writes learnings to memory — preferences, conventions, debugging insights
15
15
  - **Snapshot injection**: on every new session, the MEMORY.md index is appended to the system prompt, keeping the agent aware of past work
16
16
  - **`/dream` command**: launches a headless agent with a four-phase consolidation (Orient → Gather → Consolidate → Prune) to deduplicate, merge, and rebuild all memory files
@@ -121,7 +121,7 @@ MEMORY.md is a compact **pointer index** — one line per topic file, not per en
121
121
  - [API Conventions](api.md) — REST handlers in src/api/handlers/; standard error format
122
122
  ```
123
123
 
124
- Only the index is injected into the system prompt on every session (first 200 lines / 25KB). Topic file content is **not** loaded at session start — it's surfaced on demand via auto-surfacing or explicit `memory read`.
124
+ Only the index is injected into the system prompt on every session (first 200 lines / 25KB). Topic file content is **not** loaded at session start — it's surfaced on demand via auto-surfacing or the built-in `read` tool.
125
125
 
126
126
  ### Topic file format
127
127
 
@@ -164,9 +164,9 @@ On every user message (`before_agent_start` hook):
164
164
  ### Extract memories
165
165
 
166
166
  After each agent run (`agent_end` hook):
167
- 1. An async subagent is forked with the conversation transcript
168
- 2. It analyzes whether there are learnings worth persisting
169
- 3. If yes, it writes directly to memory files — preferences, conventions, debugging insights
167
+ 1. An async headless subagent is spawned with the conversation transcript
168
+ 2. It uses `ls` and `read` to check existing topic files and MEMORY.md, then `memory_add` to persist learnings
169
+ 3. If it finds learnings worth persisting, it writes them — preferences, conventions, debugging insights
170
170
  4. The subagent runs independently; its results benefit future sessions
171
171
 
172
172
  Memory extraction is selective: it ignores one-time tasks, code snippets derivable from the project, and anything already in CLAUDE.md.
@@ -174,7 +174,7 @@ Memory extraction is selective: it ignores one-time tasks, code snippets derivab
174
174
  ## Tool reference
175
175
 
176
176
  ```
177
- memory(action: "add" | "remove" | "search" | "read",
177
+ memory(action: "add" | "remove" | "search",
178
178
  content?, topic?, title?, type?,
179
179
  entry?, query?, scope?)
180
180
  ```
@@ -194,13 +194,6 @@ Deletes an entry by title. Searches across all topic files for the matching `##`
194
194
 
195
195
  - **`entry`** (required) — exact entry title to remove
196
196
 
197
- ### `read`
198
-
199
- Loads memory content. Either an entire topic file or a single entry block.
200
-
201
- - **`topic`** (optional) — topic name, e.g. `"debugging"` or `"debugging.md"`. Loads the entire topic file
202
- - **`entry`** (optional) — entry title. Returns the specific `## Entry Title` block
203
-
204
197
  ### `search`
205
198
 
206
199
  Queries either memory files or session history. Memory search returns the full entry block (entire `##` section) for each match.
@@ -251,4 +244,4 @@ The hash is derived from the project's git root (or absolute path), ensuring eac
251
244
 
252
245
  On every `session_start`, the `MEMORY.md` index is read and appended to the system prompt via `before_agent_start`. If the index exceeds `memIndexMaxLines` or `memIndexMaxBytes`, it is truncated with a `[truncated]` marker — the agent still gets the most relevant portion. This snapshot is a static copy at the start of the session.
253
246
 
254
- Topic file content is surfaced separately via **auto-surfacing** (automatic, per-turn, relevance-based) or explicit `memory read`.
247
+ Topic file content is surfaced separately via **auto-surfacing** (automatic, per-turn, relevance-based) or the built-in `read` tool.
package/README.zh.md CHANGED
@@ -6,12 +6,12 @@
6
6
 
7
7
  ## 功能
8
8
 
9
- - **一个 `memory` 工具**,四种操作:`add`(追加条目)、`remove`(按标题删除条目)、`read`(加载主题或条目)、`search`(查询记忆或会话历史)
9
+ - **一个 `memory` 工具**,三种操作:`add`(追加条目)、`remove`(按标题删除条目)、`search`(查询记忆或会话历史)
10
10
  - **基于主题的文件组织**:每次 `memory add` 向指定名称的 `.md` 文件写入 `## 条目` 区块
11
11
  - **`MEMORY.md` 索引**:每个 topic 文件一行紧凑指针 `- [名称](文件.md) — 摘要`,同 topic 自动合并
12
12
  - **记忆类型系统**:四种分类 — `user`(用户)、`feedback`(反馈,默认)、`project`(项目)、`reference`(引用)— 存储在 topic 文件 frontmatter 中
13
- - **Auto-surfacing** ⭐:每次用户发消息时,side-query LLM 自动选出最多 N 个相关 topic 文件并将其内容注入 agent context。无需手动 `memory read`。Session 内去重防止同一 topic 重复注入
14
- - **Extract memories** ⭐:每次 agent 运行结束后,异步子 agent 分析对话内容,自动将 learnings 写入 memory — 偏好、约定、调试心法等
13
+ - **Auto-surfacing** ⭐:每次用户发消息时,side-query LLM 自动选出最多 N 个相关 topic 文件并将其内容注入 agent context。无需手动 `read` — 用内置 `read` 工具即可查看记忆文件。Session 内去重防止同一 topic 重复注入
14
+ - **Extract memories** ⭐:每次 agent 运行结束后,异步无头子 agent 分析对话内容,用 `ls`/`read` 检查已有 topic 文件,再用 `memory_add` 写入新记忆 — 偏好、约定、调试心法等
15
15
  - **快照注入**:每个新会话启动时,MEMORY.md 索引追加到系统提示中
16
16
  - **`/dream` 命令**:四阶段(Orient → Gather → Consolidate → Prune)无头代理整理,去重、合并、重建全部记忆文件
17
17
  - **梦醒提醒**:经过 N 个会话或 N 小时后,温和通知建议运行 `/dream`
@@ -121,7 +121,7 @@ MEMORY.md 是一个**紧凑指针索引** — 每个 topic 文件一行,而非
121
121
  - [API Conventions](api.md) — REST handlers 在 src/api/handlers/;使用标准错误格式
122
122
  ```
123
123
 
124
- 每次会话只有索引被注入系统提示(前 200 行 / 25KB)。Topic 文件内容**不会**在启动时加载 — 通过 auto-surfacing 按需注入或显式 `memory read`。
124
+ 每次会话只有索引被注入系统提示(前 200 行 / 25KB)。Topic 文件内容**不会**在启动时加载 — 通过 auto-surfacing 按需注入或内置 `read` 工具显式加载。
125
125
 
126
126
  ### Topic 文件格式
127
127
 
@@ -174,7 +174,7 @@ staging 上连接超时 30s
174
174
  ## 工具参考
175
175
 
176
176
  ```
177
- memory(action: "add" | "remove" | "search" | "read",
177
+ memory(action: "add" | "remove" | "search",
178
178
  content?, topic?, title?, type?,
179
179
  entry?, query?, scope?)
180
180
  ```
@@ -194,13 +194,6 @@ memory(action: "add" | "remove" | "search" | "read",
194
194
 
195
195
  - **`entry`**(必填)— 要删除的条目标题
196
196
 
197
- ### `read`
198
-
199
- 加载记忆内容。可以是整个 topic 文件或单个条目区块。
200
-
201
- - **`topic`**(可选)— 主题名称,如 `"debugging"` 或 `"debugging.md"`。加载整个 topic 文件
202
- - **`entry`**(可选)— 条目标题。返回对应的 `## 条目标题` 区块
203
-
204
197
  ### `search`
205
198
 
206
199
  查询记忆文件或会话历史。记忆搜索返回匹配条目的完整区块(整个 `##` 区域)。
@@ -249,4 +242,4 @@ memory(action: "add" | "remove" | "search" | "read",
249
242
 
250
243
  每次 `session_start` 读取 `MEMORY.md` 索引,通过 `before_agent_start` 追加到系统提示中。超限则截断并标记 `[truncated]`。此快照是会话开始时的静态副本。
251
244
 
252
- Topic 文件内容通过 **auto-surfacing**(自动、per-turn、基于相关性)或显式 `memory read` 按需加载。
245
+ Topic 文件内容通过 **auto-surfacing**(自动、per-turn、基于相关性)或内置 `read` 工具按需加载。
package/index.ts CHANGED
@@ -12,7 +12,7 @@ import {
12
12
  runSideQuery,
13
13
  scanTopics,
14
14
  } from "./src/inject";
15
- import { createMemoryTool } from "./src/memory-tool";
15
+ import { createMemoryTool, createMemoryTools } from "./src/memory-tool";
16
16
  import { readDreamMeta, shouldNudge, writeDreamMeta } from "./src/nudge";
17
17
  import { resolveMemoryDir } from "./src/paths";
18
18
  import { searchSessions } from "./src/session-search";
@@ -156,6 +156,10 @@ export default function (pi: ExtensionAPI) {
156
156
  memoryDir,
157
157
  modelRegistry: ctx.modelRegistry,
158
158
  parentModel: ctx.model,
159
+ customTools: createMemoryTools(memoryDir, {
160
+ maxLines: config.memIndexMaxLines,
161
+ maxBytes: config.memIndexMaxBytes,
162
+ }),
159
163
  sessionPersistence: resolveDefault(config, "extractMemories", "sessionPersistence"),
160
164
  messages: event.messages.map((m) => ({
161
165
  // biome-ignore lint/suspicious/noExplicitAny: pi event message union type
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "1.3.0",
6
+ "version": "1.3.1",
7
7
  "description": "File-system driven persistent memory layer for pi coding agent",
8
8
  "license": "MIT",
9
9
  "repository": {
@@ -1,2 +1,2 @@
1
- /** Tools available to the headless memory-agent sub-session (file I/O only). */
2
- export const MEMORY_AGENT_TOOLS = ["read", "write", "edit", "ls"] as const;
1
+ /** Default built-in tools for headless agents (file I/O only, no bash). */
2
+ export const FILE_IO_TOOLS = ["read", "write", "edit", "ls"] as const;
@@ -8,8 +8,9 @@ import {
8
8
  getAgentDir,
9
9
  SessionManager,
10
10
  SettingsManager,
11
+ type ToolDefinition,
11
12
  } from "@earendil-works/pi-coding-agent";
12
- import { MEMORY_AGENT_TOOLS } from "./agent-config";
13
+ import { FILE_IO_TOOLS } from "./agent-config";
13
14
  import type { SessionPersistenceConfig, ThinkLevel } from "./config";
14
15
  import { resolveModel } from "./model-resolver";
15
16
 
@@ -25,6 +26,10 @@ export interface HeadlessAgentOpts {
25
26
  timeoutMs?: number;
26
27
  /** Session persistence config. When enabled, sessions are written to disk. */
27
28
  sessionPersistence?: SessionPersistenceConfig;
29
+ /** Built-in tool name allowlist. Defaults to FILE_IO_TOOLS. Pass [] for no built-in tools. */
30
+ tools?: string[];
31
+ /** Custom tool definitions. Defaults to []. */
32
+ customTools?: ToolDefinition[];
28
33
  }
29
34
 
30
35
  const GRACE_TURNS = 1;
@@ -67,7 +72,8 @@ export async function runHeadlessAgent(opts: HeadlessAgentOpts): Promise<string>
67
72
 
68
73
  const created = await createAgentSession({
69
74
  cwd: opts.cwd,
70
- tools: [...MEMORY_AGENT_TOOLS],
75
+ tools: opts.tools ?? [...FILE_IO_TOOLS],
76
+ customTools: opts.customTools ?? [],
71
77
  model: resolvedModel as any,
72
78
  thinkingLevel: opts.thinkLevel as any,
73
79
  modelRegistry: opts.modelRegistry,
package/src/extract.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { Model } from "@earendil-works/pi-ai";
2
- import type { ModelRegistry } from "@earendil-works/pi-coding-agent";
2
+ import type { ModelRegistry, ToolDefinition } from "@earendil-works/pi-coding-agent";
3
3
  import { runHeadlessAgent } from "./agent-runner";
4
4
  import type { SessionPersistenceConfig, ThinkLevel } from "./config";
5
5
 
@@ -12,9 +12,10 @@ export interface RunExtractOpts {
12
12
  modelRegistry: ModelRegistry;
13
13
  parentModel?: Model<any>;
14
14
  sessionPersistence?: SessionPersistenceConfig;
15
+ customTools?: ToolDefinition[];
15
16
  }
16
17
 
17
- /** Build extraction task prompt. (unchanged) */
18
+ /** Build extraction task prompt using memory tools instead of raw file I/O. */
18
19
  export function buildExtractTask(
19
20
  memoryDir: string,
20
21
  messages: Array<{ role: string; content: string }>,
@@ -30,25 +31,16 @@ export function buildExtractTask(
30
31
  const truncatedAssistant = assistantText.slice(0, maxChars / 2);
31
32
 
32
33
  return [
33
- `You are a memory extraction agent. Your cwd is the memory directory at ${memoryDir}.`,
34
+ `You are a memory extraction agent. Your working directory is the memory directory at ${memoryDir}.`,
34
35
  "",
35
- "Analyze the conversation snippet below. If you find valuable learnings, write them to topic files in this directory using ONLY file read/write/edit tools. Do NOT use bash, web search, or any other tools.",
36
- "The memory directory contains topic files with this frontmatter format:",
36
+ "Analyze the conversation snippet below. If you find valuable learnings, persist them using the memory tools.",
37
37
  "",
38
- "```yaml",
39
- "---",
40
- "name: Topic Name",
41
- "description: Brief summary for relevance matching",
42
- "type: feedback # one of: user, feedback, project, reference",
43
- "updated: 2026-07-13",
44
- "---",
38
+ "You have these tools available:",
39
+ "- 'ls' and 'read': list files and read topic files in the memory directory to check for existing topics",
40
+ "- memory_search: search across all memory files for relevant existing entries",
41
+ "- memory_add: persist a new memory entry to a topic file (creates the topic if new)",
45
42
  "",
46
- "## Entry Title",
47
- "Entry content here.",
48
- "```",
49
- "",
50
- "And MEMORY.md index:",
51
- "- [Name](file.md) — one-line hook summary",
43
+ "Use 'ls' to list files and 'read' to inspect MEMORY.md and topic files. Use memory_search to find related entries. Use memory_add to write new memories. Do NOT use 'bash', 'write', 'edit', or any other tools.",
52
44
  "",
53
45
  "Worth remembering:",
54
46
  "- User preferences, coding style choices, tooling preferences",
@@ -64,12 +56,12 @@ export function buildExtractTask(
64
56
  "- Git history or recent changes",
65
57
  "",
66
58
  "When writing memories:",
59
+ "- Use 'ls' and 'read' first to check for existing topic files and MEMORY.md index",
60
+ "- Use memory_search to find overlapping or related memories before adding",
67
61
  "- Use descriptive, self-contained entry titles (only index lines are injected into future sessions)",
68
- "- Choose the appropriate type: user, feedback, project, reference",
69
- '- Default type is "feedback"',
62
+ '- Choose the appropriate type: user, feedback, project, reference (default "feedback")',
70
63
  "- Be concise but complete",
71
64
  "- If unsure, do NOT write anything",
72
- "- Use the write/edit tools to directly modify topic files and MEMORY.md",
73
65
  "",
74
66
  "=== Conversation ===",
75
67
  `User: ${truncatedUser}`,
@@ -91,6 +83,9 @@ export async function runExtract(opts: RunExtractOpts): Promise<void> {
91
83
  thinkLevel: opts.thinkLevel,
92
84
  maxTurns: 5,
93
85
  timeoutMs: 120_000,
86
+
87
+ tools: ["read", "ls"],
88
+ customTools: opts.customTools ?? [],
94
89
  sessionPersistence: opts.sessionPersistence,
95
90
  }).catch(() => {
96
91
  /* silently ignore extract errors */
package/src/inject.ts CHANGED
@@ -163,6 +163,7 @@ export async function runSideQuery(
163
163
  thinkLevel,
164
164
  maxTurns: 1,
165
165
  timeoutMs: 30_000,
166
+ tools: [],
166
167
  sessionPersistence,
167
168
  });
168
169
  return parseSelectedFiles(result, candidates, maxFiles);
@@ -1,7 +1,7 @@
1
1
  import { mkdir, readdir, readFile, unlink, writeFile } from "node:fs/promises";
2
2
  import { dirname, join } from "node:path";
3
3
  import { StringEnum } from "@earendil-works/pi-ai";
4
- import { withFileMutationQueue } from "@earendil-works/pi-coding-agent";
4
+ import { withFileMutationQueue, type ToolDefinition } from "@earendil-works/pi-coding-agent";
5
5
  import { Text } from "@earendil-works/pi-tui";
6
6
  import { Type } from "typebox";
7
7
  import {
@@ -21,6 +21,7 @@ import {
21
21
  hasEntries,
22
22
  parseEntries,
23
23
  removeEntrySection,
24
+ replaceFrontmatterField,
24
25
  updateFrontmatterDate,
25
26
  } from "./topic-file";
26
27
 
@@ -35,15 +36,6 @@ export interface AddParams {
35
36
  export interface RemoveParams {
36
37
  entry: string;
37
38
  }
38
- export interface ReadParams {
39
- topic?: string;
40
- entry?: string;
41
- }
42
- export interface ReadResult {
43
- ok: boolean;
44
- error?: string;
45
- content?: string;
46
- }
47
39
  export interface ActionResult {
48
40
  ok: boolean;
49
41
  error?: string;
@@ -102,18 +94,19 @@ export async function doAdd(memoryDir: string, p: AddParams): Promise<ActionResu
102
94
  const topicContent = appendContent(fm, p.title, p.content);
103
95
  await writeFile(topicPath, topicContent, "utf8");
104
96
  } else {
105
- // Existing topic: append entry, then regenerate hook from ALL entry titles
97
+ // Existing topic: append entry, then regenerate hook + description from ALL entry titles
106
98
  const raw = await readFile(topicPath, "utf8");
107
99
  const refreshed = updateFrontmatterDate(raw, today());
108
100
  const topicContent = appendContent(refreshed, p.title, p.content);
109
- await writeFile(topicPath, topicContent, "utf8");
110
101
 
111
- // Build hook from all entries (comma-separated titles, trimmed to ~150 chars)
102
+ // Build hook + description from all entries
112
103
  const allEntries = parseEntries(topicContent);
113
104
  const hook = allEntries
114
105
  .map((e) => e.title)
115
106
  .join("; ")
116
107
  .slice(0, 150);
108
+ const withDesc = replaceFrontmatterField(topicContent, "description", hook);
109
+
117
110
  next = updateHook(entries, topic, hook);
118
111
  if (!checkCapacity(next, p.maxLines, p.maxBytes)) {
119
112
  return {
@@ -121,6 +114,7 @@ export async function doAdd(memoryDir: string, p: AddParams): Promise<ActionResu
121
114
  error: `MEMORY.md capacity exceeded (max ${p.maxLines} lines / ${p.maxBytes} bytes). Current entries: ${serializeIndex(entries)}`,
122
115
  };
123
116
  }
117
+ await writeFile(topicPath, withDesc, "utf8");
124
118
  }
125
119
 
126
120
  // write index
@@ -165,12 +159,13 @@ export async function doRemove(memoryDir: string, p: RemoveParams): Promise<Acti
165
159
  const afterRemoval = removeEntrySection(raw, p.entry);
166
160
 
167
161
  if (hasEntries(afterRemoval)) {
168
- // Still has entries: update hook to remaining first entry, refresh date
162
+ // Still has entries: update hook + description from remaining entries, refresh date
169
163
  const remaining = parseEntries(afterRemoval);
170
- const newHook = remaining.length > 0 ? remaining[0].title : "";
164
+ const newHook = remaining.map((e) => e.title).join("; ").slice(0, 150);
171
165
  const nextEntries = updateHook(entries, topicFile, newHook);
172
- const refreshed = updateFrontmatterDate(afterRemoval, today());
173
- await writeFile(topicPath, refreshed, "utf8");
166
+ const withDate = updateFrontmatterDate(afterRemoval, today());
167
+ const withDesc = replaceFrontmatterField(withDate, "description", newHook);
168
+ await writeFile(topicPath, withDesc, "utf8");
174
169
  await writeFile(join(memoryDir, MEMORY_MD), `${serializeIndex(nextEntries)}\n`, "utf8");
175
170
  } else {
176
171
  // Last entry removed: delete topic file and remove from index
@@ -188,38 +183,6 @@ export async function doRemove(memoryDir: string, p: RemoveParams): Promise<Acti
188
183
  });
189
184
  }
190
185
 
191
- export async function doRead(memoryDir: string, p: ReadParams): Promise<ReadResult> {
192
- if (p.topic) {
193
- const topicName = p.topic.endsWith(".md") ? p.topic : `${p.topic}.md`;
194
- let topicPath: string;
195
- try {
196
- topicPath = safeTopicPath(memoryDir, topicName);
197
- // biome-ignore lint/suspicious/noExplicitAny: error catch
198
- } catch (e: any) {
199
- return { ok: false, error: e.message };
200
- }
201
- try {
202
- const content = await readFile(topicPath, "utf8");
203
- return { ok: true, content };
204
- } catch {
205
- return { ok: false, error: `Topic "${p.topic}" not found` };
206
- }
207
- }
208
- if (p.entry) {
209
- const files = (await readdir(memoryDir).catch(() => [])).filter((f) => f.endsWith(".md") && f !== MEMORY_MD);
210
- for (const f of files) {
211
- const raw = await readFile(join(memoryDir, f), "utf8").catch(() => "");
212
- const entries = parseEntries(raw);
213
- const found = entries.find((e) => e.title === p.entry);
214
- if (found) {
215
- return { ok: true, content: `## ${found.title}\n\n${found.content}` };
216
- }
217
- }
218
- return { ok: false, error: `Entry "${p.entry}" not found in any topic` };
219
- }
220
- return { ok: false, error: "Either topic or entry must be provided" };
221
- }
222
-
223
186
  export async function searchMemory(memoryDir: string, query: string): Promise<string> {
224
187
  const files = (await readdir(memoryDir).catch(() => [])).filter((f) => f.endsWith(".md") && f !== MEMORY_MD);
225
188
  const q = query.toLowerCase();
@@ -248,23 +211,94 @@ export interface MemoryToolDeps {
248
211
  cwd: () => string;
249
212
  }
250
213
 
214
+ export function createMemoryTools(
215
+ memoryDir: string,
216
+ cfg: { maxLines: number; maxBytes: number },
217
+ ): ToolDefinition[] {
218
+ return [
219
+ {
220
+ name: "memory_add",
221
+ label: "Memory Add",
222
+ description:
223
+ "Add a new memory entry to a topic file. Creates the topic if it doesn't exist. Use memory_search and the 'ls'/'read' tools to check for existing topics first.",
224
+ parameters: Type.Object({
225
+ content: Type.String({ description: "Knowledge text to store." }),
226
+ topic: Type.String({ description: "Target topic filename, e.g. 'debugging.md'." }),
227
+ title: Type.String({
228
+ description:
229
+ "Descriptive, self-contained title. Only index lines are injected into prompts — make titles self-descriptive.",
230
+ }),
231
+ type: Type.Optional(
232
+ StringEnum(["user", "feedback", "project", "reference"] as const),
233
+ ),
234
+ }),
235
+ async execute(
236
+ _id: string,
237
+ params: any,
238
+ _signal: AbortSignal | undefined,
239
+ _onUpdate: any,
240
+ _ctx: any,
241
+ ) {
242
+ if (!params.content) throw new Error("content is required");
243
+ if (!params.topic) throw new Error("topic is required");
244
+ if (!params.title) throw new Error("title is required");
245
+ const r = await doAdd(memoryDir, {
246
+ content: params.content,
247
+ topic: params.topic,
248
+ title: params.title,
249
+ type: params.type,
250
+ maxLines: cfg.maxLines,
251
+ maxBytes: cfg.maxBytes,
252
+ });
253
+ if (!r.ok) throw new Error(r.error);
254
+ return {
255
+ details: {},
256
+ content: [{
257
+ type: "text",
258
+ text: `Added "${params.title}" to ${params.topic}. Index has ${r.entries?.length ?? 0} entries.`,
259
+ }],
260
+ };
261
+ },
262
+ },
263
+ {
264
+ name: "memory_search",
265
+ label: "Memory Search",
266
+ description:
267
+ "Search all memory topic files for entries matching a query. Case-insensitive. Use this to find related memories before adding new ones.",
268
+ parameters: Type.Object({
269
+ query: Type.String({ description: "Search query." }),
270
+ }),
271
+ async execute(
272
+ _id: string,
273
+ params: any,
274
+ _signal: AbortSignal | undefined,
275
+ _onUpdate: any,
276
+ _ctx: any,
277
+ ) {
278
+ if (!params.query) throw new Error("query is required");
279
+ const text = await searchMemory(memoryDir, params.query);
280
+ return { details: {}, content: [{ type: "text", text }] };
281
+ },
282
+ },
283
+ ];
284
+ }
285
+
251
286
  export function createMemoryTool(deps: MemoryToolDeps) {
252
287
  return {
253
288
  name: "memory",
254
289
  label: "Memory",
255
290
  description:
256
- "Read/write project memory across sessions. action 'add' appends content under a topic (auto-created) as an entry; 'remove' deletes an entry by title; 'read' loads a topic or entry; 'search' queries memory files or history sessions. IMPORTANT: only MEMORY.md index lines are injected into system prompts — entry titles must be self-contained and descriptive (topic file content is NOT injected automatically — it is auto-surfaced for relevant queries).",
291
+ "Read/write project memory across sessions. action 'add' appends content under a topic (auto-created) as an entry; 'remove' deletes an entry by title; 'search' queries memory files or history sessions. IMPORTANT: only MEMORY.md index lines are injected into system prompts — entry titles must be self-contained and descriptive (topic file content is NOT injected automatically — it is auto-surfaced for relevant queries). Use built-in 'read' and 'ls' tools to read topic files and MEMORY.md.",
257
292
  promptSnippet:
258
- "Read/write project memory across sessions (add/remove/search/read). Only index titles are injected — make titles self-descriptive.",
293
+ "Read/write project memory across sessions (add/remove/search). Only index titles are injected — make titles self-descriptive.",
259
294
  promptGuidelines: [
260
295
  "Use memory to persist project facts, user preferences, and lessons learned across sessions.",
261
296
  "Use memory action 'add' with an explicit topic filename and a descriptive, self-contained entry title — only the index line (title + topic) is injected into future prompts, NOT the topic file content. The title alone must convey what was learned.",
262
297
  "Use memory action 'search' with scope='sessions' to find past work in history sessions.",
263
- "Use memory action 'read' with topic or entry to load stored knowledge.",
264
- "Auto-surfacing: relevant topic files are automatically selected and their content injected into the conversation context. Use 'read' to load additional topics when needed — you don't need to read what's already been surfaced.",
298
+ "Auto-surfacing: relevant topic files are automatically selected and their content injected into the conversation context. Use built-in 'read' and 'ls' to load additional topics when needed — you don't need to read what's already been surfaced.",
265
299
  ],
266
300
  parameters: Type.Object({
267
- action: StringEnum(["add", "remove", "search", "read"] as const),
301
+ action: StringEnum(["add", "remove", "search"] as const),
268
302
  // add
269
303
  content: Type.Optional(Type.String({ description: "Knowledge text to store (add)." })),
270
304
  topic: Type.Optional(
@@ -342,14 +376,6 @@ export function createMemoryTool(deps: MemoryToolDeps) {
342
376
  }
343
377
  break;
344
378
  }
345
- case "read": {
346
- if (!params.topic && !params.entry) throw new Error("topic or entry is required for read");
347
- const r = await doRead(dir, { topic: params.topic, entry: params.entry });
348
- if (!r.ok) throw new Error(r.error);
349
- // biome-ignore lint/style/noNonNullAssertion: content assertion
350
- text = r.content!;
351
- break;
352
- }
353
379
  default:
354
380
  throw new Error(`Unknown action: ${params.action}`);
355
381
  }
package/src/topic-file.ts CHANGED
@@ -45,6 +45,11 @@ export function updateFrontmatterDate(raw: string, date: string): string {
45
45
  return raw.replace(/^(---\n(?:.*\n)*?)updated: .+(\n---)/m, `$1updated: ${date}$2`);
46
46
  }
47
47
 
48
+ export function replaceFrontmatterField(raw: string, field: string, value: string): string {
49
+ const regex = new RegExp(`^(---\n(?:.*\n)*?)${field}: .+(\n)`, "m");
50
+ return raw.replace(regex, (_full: string, prefix: string, nl: string) => `${prefix}${field}: ${value}${nl}`);
51
+ }
52
+
48
53
  export function removeEntrySection(raw: string, title: string): string {
49
54
  const marker = `## ${title}`;
50
55
  // find start of this entry block