opencode-claude-memory 1.5.2 → 1.6.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/src/index.ts DELETED
@@ -1,142 +0,0 @@
1
- import type { Plugin } from "@opencode-ai/plugin"
2
- import { tool } from "@opencode-ai/plugin"
3
- import { buildMemorySystemPrompt } from "./prompt.js"
4
- import { recallRelevantMemories, formatRecalledMemories } from "./recall.js"
5
- import {
6
- saveMemory,
7
- deleteMemory,
8
- listMemories,
9
- searchMemories,
10
- readMemory,
11
- MEMORY_TYPES,
12
- } from "./memory.js"
13
- import { getMemoryDir } from "./paths.js"
14
-
15
- export const MemoryPlugin: Plugin = async ({ worktree }) => {
16
- getMemoryDir(worktree)
17
-
18
- return {
19
- "experimental.chat.system.transform": async (_input, output) => {
20
- let query: string | undefined
21
- if (_input && typeof _input === "object") {
22
- const messages = (_input as { messages?: unknown }).messages
23
- if (Array.isArray(messages)) {
24
- const lastUserMsg = [...messages]
25
- .reverse()
26
- .find((message) =>
27
- message && typeof message === "object" && "role" in message && (message as { role?: unknown }).role === "user",
28
- )
29
-
30
- if (lastUserMsg && typeof lastUserMsg === "object" && "content" in lastUserMsg) {
31
- const content = (lastUserMsg as { content?: unknown }).content
32
- query = typeof content === "string" ? content : JSON.stringify(content)
33
- }
34
- }
35
- }
36
-
37
- const recalled = recallRelevantMemories(worktree, query)
38
- const recalledSection = formatRecalledMemories(recalled)
39
- const memoryPrompt = buildMemorySystemPrompt(worktree, recalledSection)
40
- output.system.push(memoryPrompt)
41
- },
42
-
43
- tool: {
44
- memory_save: tool({
45
- description:
46
- "Save or update a memory for future conversations. " +
47
- "Each memory is stored as a markdown file with frontmatter. " +
48
- "Use this when the user explicitly asks you to remember something, " +
49
- "or when you observe important information worth preserving across sessions " +
50
- "(user preferences, feedback, project context, external references). " +
51
- "Check existing memories first with memory_list or memory_search to avoid duplicates.",
52
- args: {
53
- file_name: tool.schema
54
- .string()
55
- .describe(
56
- 'File name for the memory (without .md extension). Use snake_case, e.g. "user_role", "feedback_testing_style", "project_auth_rewrite"',
57
- ),
58
- name: tool.schema.string().describe("Human-readable name for this memory"),
59
- description: tool.schema
60
- .string()
61
- .describe("One-line description — used to decide relevance in future conversations, so be specific"),
62
- type: tool.schema
63
- .enum(MEMORY_TYPES)
64
- .describe(
65
- "Memory type: user (about the person), feedback (guidance on approach), project (ongoing work context), reference (pointers to external systems)",
66
- ),
67
- content: tool.schema
68
- .string()
69
- .describe(
70
- "Memory content. For feedback/project types, structure as: rule/fact, then **Why:** and **How to apply:** lines",
71
- ),
72
- },
73
- async execute(args) {
74
- const filePath = saveMemory(worktree, args.file_name, args.name, args.description, args.type, args.content)
75
- return `Memory saved to ${filePath}`
76
- },
77
- }),
78
-
79
- memory_delete: tool({
80
- description: "Delete a memory that is outdated, wrong, or no longer relevant. Also removes it from the index.",
81
- args: {
82
- file_name: tool.schema.string().describe("File name of the memory to delete (with or without .md extension)"),
83
- },
84
- async execute(args) {
85
- const deleted = deleteMemory(worktree, args.file_name)
86
- return deleted ? `Memory "${args.file_name}" deleted.` : `Memory "${args.file_name}" not found.`
87
- },
88
- }),
89
-
90
- memory_list: tool({
91
- description:
92
- "List all saved memories with their names, types, and descriptions. " +
93
- "Use this to check what memories exist before saving a new one (to avoid duplicates) " +
94
- "or when you need to recall what's been stored.",
95
- args: {},
96
- async execute() {
97
- const entries = listMemories(worktree)
98
- if (entries.length === 0) {
99
- return "No memories saved yet."
100
- }
101
- const lines = entries.map(
102
- (e) => `- **${e.name}** (${e.type}) [${e.fileName}]: ${e.description}`,
103
- )
104
- return `${entries.length} memories found:\n${lines.join("\n")}`
105
- },
106
- }),
107
-
108
- memory_search: tool({
109
- description:
110
- "Search memories by keyword. Searches across names, descriptions, and content. " +
111
- "Use this to find relevant memories before answering questions or when the user references past conversations.",
112
- args: {
113
- query: tool.schema.string().describe("Search query — searches across name, description, and content"),
114
- },
115
- async execute(args) {
116
- const results = searchMemories(worktree, args.query)
117
- if (results.length === 0) {
118
- return `No memories matching "${args.query}".`
119
- }
120
- const lines = results.map(
121
- (e) => `- **${e.name}** (${e.type}) [${e.fileName}]: ${e.description}\n Content: ${e.content.slice(0, 200)}${e.content.length > 200 ? "..." : ""}`,
122
- )
123
- return `${results.length} matches for "${args.query}":\n${lines.join("\n")}`
124
- },
125
- }),
126
-
127
- memory_read: tool({
128
- description: "Read the full content of a specific memory file.",
129
- args: {
130
- file_name: tool.schema.string().describe("File name of the memory to read (with or without .md extension)"),
131
- },
132
- async execute(args) {
133
- const entry = readMemory(worktree, args.file_name)
134
- if (!entry) {
135
- return `Memory "${args.file_name}" not found.`
136
- }
137
- return `# ${entry.name}\n**Type:** ${entry.type}\n**Description:** ${entry.description}\n\n${entry.content}`
138
- },
139
- }),
140
- },
141
- }
142
- }
package/src/memory.ts DELETED
@@ -1,243 +0,0 @@
1
- import { readFileSync, writeFileSync, readdirSync, unlinkSync, existsSync } from "fs"
2
- import { join, basename } from "path"
3
- import {
4
- getMemoryDir,
5
- getMemoryEntrypoint,
6
- ENTRYPOINT_NAME,
7
- validateMemoryFileName,
8
- MAX_MEMORY_FILES,
9
- MAX_MEMORY_FILE_BYTES,
10
- FRONTMATTER_MAX_LINES,
11
- } from "./paths.js"
12
-
13
- export const MEMORY_TYPES = ["user", "feedback", "project", "reference"] as const
14
- export type MemoryType = (typeof MEMORY_TYPES)[number]
15
-
16
- export type MemoryEntry = {
17
- filePath: string
18
- fileName: string
19
- name: string
20
- description: string
21
- type: MemoryType
22
- content: string
23
- rawContent: string
24
- }
25
-
26
- function parseFrontmatter(raw: string): { frontmatter: Record<string, string>; content: string } {
27
- const trimmed = raw.trim()
28
- if (!trimmed.startsWith("---")) {
29
- return { frontmatter: {}, content: trimmed }
30
- }
31
-
32
- const lines = trimmed.split("\n")
33
- let closingLineIdx = -1
34
- for (let i = 1; i < Math.min(lines.length, FRONTMATTER_MAX_LINES); i++) {
35
- if (lines[i].trimEnd() === "---") {
36
- closingLineIdx = i
37
- break
38
- }
39
- }
40
- if (closingLineIdx === -1) {
41
- return { frontmatter: {}, content: trimmed }
42
- }
43
-
44
- const endIndex = lines.slice(0, closingLineIdx).join("\n").length + 1
45
-
46
- const frontmatterBlock = trimmed.slice(3, endIndex).trim()
47
- const content = trimmed.slice(endIndex + 3).trim()
48
-
49
- const frontmatter: Record<string, string> = {}
50
- for (const line of frontmatterBlock.split("\n")) {
51
- const colonIdx = line.indexOf(":")
52
- if (colonIdx === -1) continue
53
- const key = line.slice(0, colonIdx).trim()
54
- const value = line.slice(colonIdx + 1).trim()
55
- if (key && value) {
56
- frontmatter[key] = value
57
- }
58
- }
59
-
60
- return { frontmatter, content }
61
- }
62
-
63
- function buildFrontmatter(name: string, description: string, type: MemoryType): string {
64
- return `---\nname: ${name}\ndescription: ${description}\ntype: ${type}\n---`
65
- }
66
-
67
- function parseMemoryType(raw: string | undefined): MemoryType | undefined {
68
- if (!raw) return undefined
69
- return MEMORY_TYPES.find((t) => t === raw)
70
- }
71
-
72
- export function listMemories(worktree: string): MemoryEntry[] {
73
- const memDir = getMemoryDir(worktree)
74
- const entries: MemoryEntry[] = []
75
-
76
- let files: string[]
77
- try {
78
- files = readdirSync(memDir)
79
- .filter((f) => f.endsWith(".md") && f !== ENTRYPOINT_NAME)
80
- .sort()
81
- .slice(0, MAX_MEMORY_FILES)
82
- } catch {
83
- return entries
84
- }
85
-
86
- for (const fileName of files) {
87
- const filePath = join(memDir, fileName)
88
- try {
89
- const rawContent = readFileSync(filePath, "utf-8")
90
- const { frontmatter, content } = parseFrontmatter(rawContent)
91
- entries.push({
92
- filePath,
93
- fileName,
94
- name: frontmatter.name ?? fileName.replace(/\.md$/, ""),
95
- description: frontmatter.description ?? "",
96
- type: parseMemoryType(frontmatter.type) ?? "user",
97
- content,
98
- rawContent,
99
- })
100
- } catch {
101
-
102
- }
103
- }
104
-
105
- return entries
106
- }
107
-
108
- export function readMemory(worktree: string, fileName: string): MemoryEntry | null {
109
- const safeName = validateMemoryFileName(fileName)
110
- const memDir = getMemoryDir(worktree)
111
- const filePath = join(memDir, safeName)
112
-
113
- try {
114
- const rawContent = readFileSync(filePath, "utf-8")
115
- const { frontmatter, content } = parseFrontmatter(rawContent)
116
- return {
117
- filePath,
118
- fileName: basename(filePath),
119
- name: frontmatter.name ?? fileName.replace(/\.md$/, ""),
120
- description: frontmatter.description ?? "",
121
- type: parseMemoryType(frontmatter.type) ?? "user",
122
- content,
123
- rawContent,
124
- }
125
- } catch {
126
- return null
127
- }
128
- }
129
-
130
- export function saveMemory(
131
- worktree: string,
132
- fileName: string,
133
- name: string,
134
- description: string,
135
- type: MemoryType,
136
- content: string,
137
- ): string {
138
- const safeName = validateMemoryFileName(fileName)
139
- const memDir = getMemoryDir(worktree)
140
- const filePath = join(memDir, safeName)
141
-
142
- const fileContent = `${buildFrontmatter(name, description, type)}\n\n${content.trim()}\n`
143
- if (Buffer.byteLength(fileContent, "utf-8") > MAX_MEMORY_FILE_BYTES) {
144
- throw new Error(
145
- `Memory file content exceeds the ${MAX_MEMORY_FILE_BYTES}-byte limit`,
146
- )
147
- }
148
- writeFileSync(filePath, fileContent, "utf-8")
149
-
150
- updateIndex(worktree, safeName, name, description)
151
-
152
- return filePath
153
- }
154
-
155
- export function deleteMemory(worktree: string, fileName: string): boolean {
156
- const safeName = validateMemoryFileName(fileName)
157
- const memDir = getMemoryDir(worktree)
158
- const filePath = join(memDir, safeName)
159
-
160
- try {
161
- unlinkSync(filePath)
162
- removeFromIndex(worktree, safeName)
163
- return true
164
- } catch {
165
- return false
166
- }
167
- }
168
-
169
- export function searchMemories(worktree: string, query: string): MemoryEntry[] {
170
- const all = listMemories(worktree)
171
- const lowerQuery = query.toLowerCase()
172
-
173
- return all.filter(
174
- (entry) =>
175
- entry.name.toLowerCase().includes(lowerQuery) ||
176
- entry.description.toLowerCase().includes(lowerQuery) ||
177
- entry.content.toLowerCase().includes(lowerQuery),
178
- )
179
- }
180
-
181
- export function readIndex(worktree: string): string {
182
- const entrypoint = getMemoryEntrypoint(worktree)
183
- try {
184
- return readFileSync(entrypoint, "utf-8")
185
- } catch {
186
- return ""
187
- }
188
- }
189
-
190
- function updateIndex(worktree: string, fileName: string, name: string, description: string): void {
191
- const entrypoint = getMemoryEntrypoint(worktree)
192
- const existing = readIndex(worktree)
193
- const lines = existing.split("\n").filter((l) => l.trim())
194
-
195
- const pointer = `- [${name}](${fileName}) — ${description}`
196
- const existingIdx = lines.findIndex((l) => l.includes(`(${fileName})`))
197
-
198
- if (existingIdx >= 0) {
199
- lines[existingIdx] = pointer
200
- } else {
201
- lines.push(pointer)
202
- }
203
-
204
- writeFileSync(entrypoint, lines.join("\n") + "\n", "utf-8")
205
- }
206
-
207
- function removeFromIndex(worktree: string, fileName: string): void {
208
- const entrypoint = getMemoryEntrypoint(worktree)
209
- const existing = readIndex(worktree)
210
- const lines = existing
211
- .split("\n")
212
- .filter((l) => l.trim() && !l.includes(`(${fileName})`))
213
-
214
- writeFileSync(entrypoint, lines.length > 0 ? lines.join("\n") + "\n" : "", "utf-8")
215
- }
216
-
217
- export function truncateEntrypoint(raw: string): { content: string; wasTruncated: boolean } {
218
- const trimmed = raw.trim()
219
- if (!trimmed) return { content: "", wasTruncated: false }
220
-
221
- const lines = trimmed.split("\n")
222
- const lineCount = lines.length
223
- const byteCount = trimmed.length
224
-
225
- const wasLineTruncated = lineCount > 200
226
- const wasByteTruncated = byteCount > 25_000
227
-
228
- if (!wasLineTruncated && !wasByteTruncated) {
229
- return { content: trimmed, wasTruncated: false }
230
- }
231
-
232
- let truncated = wasLineTruncated ? lines.slice(0, 200).join("\n") : trimmed
233
-
234
- if (truncated.length > 25_000) {
235
- const cutAt = truncated.lastIndexOf("\n", 25_000)
236
- truncated = truncated.slice(0, cutAt > 0 ? cutAt : 25_000)
237
- }
238
-
239
- return {
240
- content: truncated + "\n\n> WARNING: MEMORY.md was truncated. Keep index entries concise.",
241
- wasTruncated: true,
242
- }
243
- }
package/src/paths.ts DELETED
@@ -1,165 +0,0 @@
1
- // Claude Code compatible memory directory path resolution.
2
- // Directory: ~/.claude/projects/<sanitizePath(canonicalGitRoot)>/memory/
3
- // Ensures bidirectional memory sharing between Claude Code and OpenCode.
4
-
5
- import { homedir } from "os"
6
- import { join, dirname, resolve, sep } from "path"
7
- import { mkdirSync, existsSync, readFileSync, statSync, realpathSync } from "fs"
8
-
9
- export const ENTRYPOINT_NAME = "MEMORY.md"
10
- export const MAX_ENTRYPOINT_LINES = 200
11
- export const MAX_ENTRYPOINT_BYTES = 25_000
12
-
13
- export const MAX_MEMORY_FILES = 200
14
- export const MAX_MEMORY_FILE_BYTES = 40_000
15
- export const FRONTMATTER_MAX_LINES = 30
16
-
17
- export function validateMemoryFileName(fileName: string): string {
18
- const base = fileName.endsWith(".md") ? fileName.slice(0, -3) : fileName
19
-
20
- if (base.length === 0) {
21
- throw new Error("Memory file name cannot be empty")
22
- }
23
- if (base.includes("/") || base.includes("\\")) {
24
- throw new Error(`Memory file name must not contain path separators: ${fileName}`)
25
- }
26
- if (base.includes("..")) {
27
- throw new Error(`Memory file name must not contain path traversal: ${fileName}`)
28
- }
29
- if (base.includes("\0")) {
30
- throw new Error(`Memory file name must not contain null bytes: ${fileName}`)
31
- }
32
- if (base.startsWith(".")) {
33
- throw new Error(`Memory file name must not start with '.': ${fileName}`)
34
- }
35
- if (base.toUpperCase() === "MEMORY") {
36
- throw new Error(`'MEMORY' is a reserved name and cannot be used as a memory file name`)
37
- }
38
-
39
- return `${base}.md`
40
- }
41
-
42
- const MAX_SANITIZED_LENGTH = 200
43
-
44
- // Exact copy of Claude Code's djb2Hash() from utils/hash.ts
45
- function djb2Hash(str: string): number {
46
- let hash = 0
47
- for (let i = 0; i < str.length; i++) {
48
- hash = ((hash << 5) - hash + str.charCodeAt(i)) | 0
49
- }
50
- return hash
51
- }
52
-
53
- function simpleHash(str: string): string {
54
- return Math.abs(djb2Hash(str)).toString(36)
55
- }
56
-
57
- // Exact copy of Claude Code's sanitizePath() from utils/sessionStoragePortable.ts
58
- export function sanitizePath(name: string): string {
59
- const sanitized = name.replace(/[^a-zA-Z0-9]/g, "-")
60
- if (sanitized.length <= MAX_SANITIZED_LENGTH) {
61
- return sanitized
62
- }
63
- const hash = simpleHash(name)
64
- return `${sanitized.slice(0, MAX_SANITIZED_LENGTH)}-${hash}`
65
- }
66
-
67
- // Matches Claude Code's findGitRoot() from utils/git.ts
68
- function findGitRoot(startPath: string): string | null {
69
- let current = resolve(startPath)
70
- const root = current.substring(0, current.indexOf(sep) + 1) || sep
71
-
72
- while (current !== root) {
73
- try {
74
- const gitPath = join(current, ".git")
75
- const s = statSync(gitPath)
76
- if (s.isDirectory() || s.isFile()) {
77
- return current.normalize("NFC")
78
- }
79
- } catch {}
80
- const parent = dirname(current)
81
- if (parent === current) break
82
- current = parent
83
- }
84
-
85
- try {
86
- const gitPath = join(root, ".git")
87
- const s = statSync(gitPath)
88
- if (s.isDirectory() || s.isFile()) {
89
- return root.normalize("NFC")
90
- }
91
- } catch {}
92
-
93
- return null
94
- }
95
-
96
- // Matches Claude Code's resolveCanonicalRoot() from utils/git.ts
97
- // Resolves worktrees to the main repo root via .git -> gitdir -> commondir chain
98
- function resolveCanonicalRoot(gitRoot: string): string {
99
- try {
100
- const gitContent = readFileSync(join(gitRoot, ".git"), "utf-8").trim()
101
- if (!gitContent.startsWith("gitdir:")) {
102
- return gitRoot
103
- }
104
- const worktreeGitDir = resolve(gitRoot, gitContent.slice("gitdir:".length).trim())
105
-
106
- const commonDir = resolve(
107
- worktreeGitDir,
108
- readFileSync(join(worktreeGitDir, "commondir"), "utf-8").trim(),
109
- )
110
-
111
- // SECURITY: validate worktreeGitDir is a direct child of <commonDir>/worktrees/
112
- if (resolve(dirname(worktreeGitDir)) !== join(commonDir, "worktrees")) {
113
- return gitRoot
114
- }
115
-
116
- // SECURITY: validate gitdir back-link points to our .git
117
- const backlink = realpathSync(
118
- readFileSync(join(worktreeGitDir, "gitdir"), "utf-8").trim(),
119
- )
120
- if (backlink !== join(realpathSync(gitRoot), ".git")) {
121
- return gitRoot
122
- }
123
-
124
- if (commonDir.endsWith(`${sep}.git`) || commonDir.endsWith("/.git")) {
125
- return dirname(commonDir).normalize("NFC")
126
- }
127
-
128
- return commonDir.normalize("NFC")
129
- } catch {
130
- return gitRoot
131
- }
132
- }
133
-
134
- export function findCanonicalGitRoot(startPath: string): string | null {
135
- const root = findGitRoot(startPath)
136
- if (!root) return null
137
- return resolveCanonicalRoot(root)
138
- }
139
-
140
- function getClaudeConfigHomeDir(): string {
141
- return (process.env.CLAUDE_CONFIG_DIR ?? join(homedir(), ".claude")).normalize("NFC")
142
- }
143
-
144
- export function getMemoryDir(worktree: string): string {
145
- const canonicalRoot = findCanonicalGitRoot(worktree) ?? worktree
146
- const projectsDir = join(getClaudeConfigHomeDir(), "projects")
147
- const memoryDir = join(projectsDir, sanitizePath(canonicalRoot), "memory")
148
- ensureDir(memoryDir)
149
- return memoryDir
150
- }
151
-
152
- export function getMemoryEntrypoint(worktree: string): string {
153
- return join(getMemoryDir(worktree), ENTRYPOINT_NAME)
154
- }
155
-
156
- export function isMemoryPath(absolutePath: string, worktree: string): boolean {
157
- const memDir = getMemoryDir(worktree)
158
- return absolutePath.startsWith(memDir)
159
- }
160
-
161
- export function ensureDir(dir: string): void {
162
- if (!existsSync(dir)) {
163
- mkdirSync(dir, { recursive: true })
164
- }
165
- }
package/src/prompt.ts DELETED
@@ -1,147 +0,0 @@
1
- import { MEMORY_TYPES } from "./memory.js"
2
- import { readIndex, truncateEntrypoint } from "./memory.js"
3
- import { getMemoryDir, ENTRYPOINT_NAME } from "./paths.js"
4
-
5
- const FRONTMATTER_EXAMPLE = `\`\`\`markdown
6
- ---
7
- name: {{memory name}}
8
- description: {{one-line description — used to decide relevance in future conversations, so be specific}}
9
- type: {{${MEMORY_TYPES.join(", ")}}}
10
- ---
11
-
12
- {{memory content — for feedback/project types, structure as: rule/fact, then **Why:** and **How to apply:** lines}}
13
- \`\`\``
14
-
15
- const TYPES_SECTION = `## Types of memory
16
-
17
- There are several discrete types of memory that you can store:
18
-
19
- <types>
20
- <type>
21
- <name>user</name>
22
- <description>Information about the user's role, goals, responsibilities, and knowledge. Great user memories help you tailor your future behavior to the user's preferences and perspective.</description>
23
- <when_to_save>When you learn any details about the user's role, preferences, responsibilities, or knowledge</when_to_save>
24
- <how_to_use>When your work should be informed by the user's profile or perspective.</how_to_use>
25
- <examples>
26
- user: I'm a data scientist investigating what logging we have in place
27
- assistant: [saves user memory: user is a data scientist, currently focused on observability/logging]
28
- </examples>
29
- </type>
30
- <type>
31
- <name>feedback</name>
32
- <description>Guidance the user has given you about how to approach work — both what to avoid and what to keep doing. Record from failure AND success.</description>
33
- <when_to_save>Any time the user corrects your approach ("no not that", "don't", "stop doing X") OR confirms a non-obvious approach worked ("yes exactly", "perfect, keep doing that"). Include *why* so you can judge edge cases later.</when_to_save>
34
- <how_to_use>Let these memories guide your behavior so that the user does not need to offer the same guidance twice.</how_to_use>
35
- <body_structure>Lead with the rule itself, then a **Why:** line and a **How to apply:** line.</body_structure>
36
- <examples>
37
- user: don't mock the database in these tests — we got burned last quarter when mocked tests passed but the prod migration failed
38
- assistant: [saves feedback memory: integration tests must hit a real database, not mocks]
39
- </examples>
40
- </type>
41
- <type>
42
- <name>project</name>
43
- <description>Information about ongoing work, goals, initiatives, bugs, or incidents that is not derivable from the code or git history.</description>
44
- <when_to_save>When you learn who is doing what, why, or by when. Always convert relative dates to absolute dates when saving.</when_to_save>
45
- <how_to_use>Use these memories to understand the broader context behind the user's request.</how_to_use>
46
- <body_structure>Lead with the fact or decision, then a **Why:** line and a **How to apply:** line.</body_structure>
47
- <examples>
48
- user: we're freezing all non-critical merges after Thursday — mobile team is cutting a release branch
49
- assistant: [saves project memory: merge freeze begins 2026-03-05 for mobile release cut]
50
- </examples>
51
- </type>
52
- <type>
53
- <name>reference</name>
54
- <description>Pointers to where information can be found in external systems.</description>
55
- <when_to_save>When you learn about resources in external systems and their purpose.</when_to_save>
56
- <how_to_use>When the user references an external system or information that may be in an external system.</how_to_use>
57
- <examples>
58
- user: check the Linear project "INGEST" if you want context on these tickets
59
- assistant: [saves reference memory: pipeline bugs are tracked in Linear project "INGEST"]
60
- </examples>
61
- </type>
62
- </types>`
63
-
64
- const WHAT_NOT_TO_SAVE = `## What NOT to save in memory
65
-
66
- - Code patterns, conventions, architecture, file paths, or project structure — these can be derived by reading the current project state.
67
- - Git history, recent changes, or who-changed-what — \`git log\` / \`git blame\` are authoritative.
68
- - Debugging solutions or fix recipes — the fix is in the code; the commit message has the context.
69
- - Anything already documented in AGENTS.md or project rules files.
70
- - Ephemeral task details: in-progress work, temporary state, current conversation context.
71
-
72
- These exclusions apply even when the user explicitly asks you to save. If they ask you to save a PR list or activity summary, ask what was *surprising* or *non-obvious* about it — that is the part worth keeping.`
73
-
74
- const WHEN_TO_ACCESS = `## When to access memories
75
- - When memories seem relevant, or the user references prior-conversation work.
76
- - You MUST access memory when the user explicitly asks you to check, recall, or remember.
77
- - If the user says to *ignore* or *not use* memory: proceed as if MEMORY.md were empty.
78
- - Memory records can become stale over time. Before answering based solely on memory, verify against current state. If a recalled memory conflicts with current information, trust what you observe now — and update or remove the stale memory.`
79
-
80
- const TRUSTING_RECALL = `## Before recommending from memory
81
-
82
- A memory that names a specific function, file, or flag is a claim that it existed *when the memory was written*. It may have been renamed, removed, or never merged. Before recommending it:
83
-
84
- - If the memory names a file path: check the file exists.
85
- - If the memory names a function or flag: grep for it.
86
- - If the user is about to act on your recommendation, verify first.
87
-
88
- "The memory says X exists" is not the same as "X exists now."
89
-
90
- A memory that summarizes repo state is frozen in time. If the user asks about *recent* or *current* state, prefer \`git log\` or reading the code over recalling the snapshot.`
91
-
92
- export function buildMemorySystemPrompt(worktree: string, recalledMemoriesSection?: string): string {
93
- const memoryDir = getMemoryDir(worktree)
94
- const indexContent = readIndex(worktree)
95
-
96
- const lines: string[] = [
97
- "# Auto Memory",
98
- "",
99
- `You have a persistent, file-based memory system at \`${memoryDir}\`. This directory already exists — write to it directly (do not run mkdir or check for its existence).`,
100
- "",
101
- "You should build up this memory system over time so that future conversations can have a complete picture of who the user is, how they'd like to collaborate with you, what behaviors to avoid or repeat, and the context behind the work the user gives you.",
102
- "",
103
- "If the user explicitly asks you to remember something, save it immediately using the `memory_save` tool as whichever type fits best. If they ask you to forget something, find and remove the relevant entry using `memory_delete`.",
104
- "",
105
- TYPES_SECTION,
106
- "",
107
- WHAT_NOT_TO_SAVE,
108
- "",
109
- `## How to save memories`,
110
- "",
111
- "Use the `memory_save` tool to create or update a memory. Each memory goes in its own file with frontmatter:",
112
- "",
113
- FRONTMATTER_EXAMPLE,
114
- "",
115
- `- The \`${ENTRYPOINT_NAME}\` index is managed automatically — you don't need to edit it`,
116
- "- Organize memory semantically by topic, not chronologically",
117
- "- Update or remove memories that turn out to be wrong or outdated",
118
- "- Do not write duplicate memories. First use `memory_list` or `memory_search` to check if there is an existing memory you can update before writing a new one.",
119
- "",
120
- WHEN_TO_ACCESS,
121
- "",
122
- TRUSTING_RECALL,
123
- "",
124
- "## Memory and other forms of persistence",
125
- "Memory is one of several persistence mechanisms. The distinction is that memory can be recalled in future conversations and should not be used for persisting information that is only useful within the scope of the current conversation.",
126
- "- When to use or update a plan instead of memory: If you are about to start a non-trivial implementation task, use a Plan rather than saving this to memory.",
127
- "- When to use or update tasks instead of memory: When you need to break your work into discrete steps or track progress, use tasks instead of saving to memory.",
128
- "",
129
- ]
130
-
131
- if (indexContent.trim()) {
132
- const { content: truncated } = truncateEntrypoint(indexContent)
133
- lines.push(`## ${ENTRYPOINT_NAME}`, "", truncated)
134
- } else {
135
- lines.push(
136
- `## ${ENTRYPOINT_NAME}`,
137
- "",
138
- `Your ${ENTRYPOINT_NAME} is currently empty. When you save new memories, they will appear here.`,
139
- )
140
- }
141
-
142
- if (recalledMemoriesSection?.trim()) {
143
- lines.push("", recalledMemoriesSection)
144
- }
145
-
146
- return lines.join("\n")
147
- }