opencode-claude-memory 1.7.7 → 2.0.0

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.
Files changed (69) hide show
  1. package/README.md +104 -214
  2. package/dist/agents.d.ts +12 -0
  3. package/dist/agents.js +69 -0
  4. package/dist/config.d.ts +112 -0
  5. package/dist/config.js +79 -0
  6. package/dist/extraction/ExtractionCoordinator.d.ts +62 -0
  7. package/dist/extraction/ExtractionCoordinator.js +386 -0
  8. package/dist/extraction/autodream.d.ts +33 -0
  9. package/dist/extraction/autodream.js +85 -0
  10. package/dist/extraction/forkSession.d.ts +37 -0
  11. package/dist/extraction/forkSession.js +133 -0
  12. package/dist/extraction/lock.d.ts +29 -0
  13. package/dist/extraction/lock.js +191 -0
  14. package/dist/extraction/prompts.d.ts +5 -0
  15. package/dist/extraction/prompts.js +78 -0
  16. package/dist/extraction/state.d.ts +38 -0
  17. package/dist/extraction/state.js +159 -0
  18. package/dist/hooks/ignore.d.ts +6 -0
  19. package/dist/hooks/ignore.js +53 -0
  20. package/dist/hooks/messages.d.ts +14 -0
  21. package/dist/hooks/messages.js +96 -0
  22. package/dist/index.d.ts +7 -1
  23. package/dist/index.js +50 -665
  24. package/dist/prompt/sections.d.ts +7 -0
  25. package/dist/{prompt.js → prompt/sections.js} +16 -72
  26. package/dist/prompt/systemPrompt.d.ts +7 -0
  27. package/dist/prompt/systemPrompt.js +64 -0
  28. package/dist/recall/RecallCoordinator.d.ts +36 -0
  29. package/dist/recall/RecallCoordinator.js +140 -0
  30. package/dist/recall/format.d.ts +17 -0
  31. package/dist/{recall.js → recall/format.js} +13 -36
  32. package/dist/{recallSelector.d.ts → recall/selector.d.ts} +26 -16
  33. package/dist/recall/selector.js +92 -0
  34. package/dist/sdk.d.ts +12 -0
  35. package/dist/sdk.js +8 -0
  36. package/dist/store/MemoryStore.d.ts +41 -0
  37. package/dist/store/MemoryStore.js +105 -0
  38. package/dist/store/frontmatter.d.ts +25 -0
  39. package/dist/store/frontmatter.js +61 -0
  40. package/dist/store/indexFile.d.ts +15 -0
  41. package/dist/store/indexFile.js +153 -0
  42. package/dist/{paths.d.ts → store/paths.d.ts} +7 -6
  43. package/dist/store/paths.js +187 -0
  44. package/dist/store/scan.d.ts +29 -0
  45. package/dist/store/scan.js +112 -0
  46. package/dist/tools.d.ts +9 -0
  47. package/dist/tools.js +142 -0
  48. package/dist/util/exclusiveFile.d.ts +11 -0
  49. package/dist/util/exclusiveFile.js +108 -0
  50. package/dist/util/legacyShellHook.d.ts +3 -0
  51. package/dist/util/legacyShellHook.js +23 -0
  52. package/dist/util/log.d.ts +6 -0
  53. package/dist/util/log.js +29 -0
  54. package/dist/util/ownedSessions.d.ts +7 -0
  55. package/dist/util/ownedSessions.js +39 -0
  56. package/dist/util/timeout.d.ts +5 -0
  57. package/dist/util/timeout.js +39 -0
  58. package/package.json +15 -8
  59. package/bin/opencode-memory +0 -1436
  60. package/dist/memory.d.ts +0 -31
  61. package/dist/memory.js +0 -224
  62. package/dist/memoryScan.d.ts +0 -20
  63. package/dist/memoryScan.js +0 -106
  64. package/dist/nativeExtraction.d.ts +0 -9
  65. package/dist/nativeExtraction.js +0 -80
  66. package/dist/paths.js +0 -141
  67. package/dist/prompt.d.ts +0 -4
  68. package/dist/recall.d.ts +0 -12
  69. package/dist/recallSelector.js +0 -154
package/dist/memory.d.ts DELETED
@@ -1,31 +0,0 @@
1
- export declare const MEMORY_TYPES: readonly ["user", "feedback", "project", "reference"];
2
- export type MemoryType = (typeof MEMORY_TYPES)[number];
3
- export type MemoryEntry = {
4
- filePath: string;
5
- fileName: string;
6
- name: string;
7
- description: string;
8
- type: MemoryType;
9
- content: string;
10
- rawContent: string;
11
- };
12
- export declare function listMemories(worktree: string): MemoryEntry[];
13
- export declare function readMemory(worktree: string, fileName: string): MemoryEntry | null;
14
- export type SaveMemoryResult = {
15
- filePath: string;
16
- fileName: string;
17
- unchanged: boolean;
18
- };
19
- export declare function saveMemory(worktree: string, fileName: string, name: string, description: string, type: MemoryType, content: string): string;
20
- export declare function saveMemoryDetailed(worktree: string, fileName: string, name: string, description: string, type: MemoryType, content: string): SaveMemoryResult;
21
- export declare function deleteMemory(worktree: string, fileName: string): boolean;
22
- export declare function searchMemories(worktree: string, query: string): MemoryEntry[];
23
- export declare function readIndex(worktree: string): string;
24
- export type EntrypointTruncation = {
25
- content: string;
26
- lineCount: number;
27
- byteCount: number;
28
- wasLineTruncated: boolean;
29
- wasByteTruncated: boolean;
30
- };
31
- export declare function truncateEntrypoint(raw: string): EntrypointTruncation;
package/dist/memory.js DELETED
@@ -1,224 +0,0 @@
1
- import { readFileSync, writeFileSync, readdirSync, unlinkSync } from "fs";
2
- import { join, basename } from "path";
3
- import { getMemoryDir, getMemoryEntrypoint, ENTRYPOINT_NAME, validateMemoryFileName, MAX_MEMORY_FILES, MAX_MEMORY_FILE_BYTES, MAX_ENTRYPOINT_LINES, MAX_ENTRYPOINT_BYTES, FRONTMATTER_MAX_LINES, } from "./paths.js";
4
- export const MEMORY_TYPES = ["user", "feedback", "project", "reference"];
5
- function parseFrontmatter(raw) {
6
- const trimmed = raw.trim();
7
- if (!trimmed.startsWith("---")) {
8
- return { frontmatter: {}, content: trimmed };
9
- }
10
- const lines = trimmed.split("\n");
11
- let closingLineIdx = -1;
12
- for (let i = 1; i < Math.min(lines.length, FRONTMATTER_MAX_LINES); i++) {
13
- if (lines[i].trimEnd() === "---") {
14
- closingLineIdx = i;
15
- break;
16
- }
17
- }
18
- if (closingLineIdx === -1) {
19
- return { frontmatter: {}, content: trimmed };
20
- }
21
- const endIndex = lines.slice(0, closingLineIdx).join("\n").length + 1;
22
- const frontmatterBlock = trimmed.slice(3, endIndex).trim();
23
- const content = trimmed.slice(endIndex + 3).trim();
24
- const frontmatter = {};
25
- for (const line of frontmatterBlock.split("\n")) {
26
- const colonIdx = line.indexOf(":");
27
- if (colonIdx === -1)
28
- continue;
29
- const key = line.slice(0, colonIdx).trim();
30
- const value = line.slice(colonIdx + 1).trim();
31
- if (key && value) {
32
- frontmatter[key] = value;
33
- }
34
- }
35
- return { frontmatter, content };
36
- }
37
- function buildFrontmatter(name, description, type) {
38
- return `---\nname: ${name}\ndescription: ${description}\ntype: ${type}\n---`;
39
- }
40
- function parseMemoryType(raw) {
41
- if (!raw)
42
- return undefined;
43
- return MEMORY_TYPES.find((t) => t === raw);
44
- }
45
- export function listMemories(worktree) {
46
- const memDir = getMemoryDir(worktree);
47
- const entries = [];
48
- let files;
49
- try {
50
- files = readdirSync(memDir, { encoding: "utf-8" })
51
- .filter((f) => f.endsWith(".md") && f !== ENTRYPOINT_NAME)
52
- .sort()
53
- .slice(0, MAX_MEMORY_FILES);
54
- }
55
- catch {
56
- return entries;
57
- }
58
- for (const fileName of files) {
59
- const filePath = join(memDir, fileName);
60
- try {
61
- const rawContent = readFileSync(filePath, "utf-8");
62
- const { frontmatter, content } = parseFrontmatter(rawContent);
63
- entries.push({
64
- filePath,
65
- fileName,
66
- name: frontmatter.name ?? fileName.replace(/\.md$/, ""),
67
- description: frontmatter.description ?? "",
68
- type: parseMemoryType(frontmatter.type) ?? "user",
69
- content,
70
- rawContent,
71
- });
72
- }
73
- catch {
74
- }
75
- }
76
- return entries;
77
- }
78
- export function readMemory(worktree, fileName) {
79
- const safeName = validateMemoryFileName(fileName);
80
- const memDir = getMemoryDir(worktree);
81
- const filePath = join(memDir, safeName);
82
- try {
83
- const rawContent = readFileSync(filePath, "utf-8");
84
- const { frontmatter, content } = parseFrontmatter(rawContent);
85
- return {
86
- filePath,
87
- fileName: basename(filePath),
88
- name: frontmatter.name ?? fileName.replace(/\.md$/, ""),
89
- description: frontmatter.description ?? "",
90
- type: parseMemoryType(frontmatter.type) ?? "user",
91
- content,
92
- rawContent,
93
- };
94
- }
95
- catch {
96
- return null;
97
- }
98
- }
99
- export function saveMemory(worktree, fileName, name, description, type, content) {
100
- return saveMemoryDetailed(worktree, fileName, name, description, type, content).filePath;
101
- }
102
- // Same as saveMemory() but reports whether anything was actually written. An identical re-save is a
103
- // no-op: the extraction fork uses that signal to tell the model it is repeating itself (see #35).
104
- export function saveMemoryDetailed(worktree, fileName, name, description, type, content) {
105
- const safeName = validateMemoryFileName(fileName);
106
- if (typeof name !== "string" || !name.trim()) {
107
- throw new Error("Memory name is required");
108
- }
109
- const memDir = getMemoryDir(worktree);
110
- const filePath = join(memDir, safeName);
111
- const fileContent = `${buildFrontmatter(name, description, type)}\n\n${content.trim()}\n`;
112
- if (Buffer.byteLength(fileContent, "utf-8") > MAX_MEMORY_FILE_BYTES) {
113
- throw new Error(`Memory file content exceeds the ${MAX_MEMORY_FILE_BYTES}-byte limit`);
114
- }
115
- if (isMemoryUnchanged(worktree, filePath, fileContent, buildIndexPointer(safeName, name, description))) {
116
- return { filePath, fileName: safeName, unchanged: true };
117
- }
118
- writeFileSync(filePath, fileContent, "utf-8");
119
- updateIndex(worktree, safeName, name, description);
120
- return { filePath, fileName: safeName, unchanged: false };
121
- }
122
- function isMemoryUnchanged(worktree, filePath, fileContent, pointer) {
123
- try {
124
- if (readFileSync(filePath, "utf-8") !== fileContent)
125
- return false;
126
- }
127
- catch {
128
- return false;
129
- }
130
- return readIndex(worktree).split("\n").some((line) => line.trimEnd() === pointer);
131
- }
132
- export function deleteMemory(worktree, fileName) {
133
- const safeName = validateMemoryFileName(fileName);
134
- const memDir = getMemoryDir(worktree);
135
- const filePath = join(memDir, safeName);
136
- try {
137
- unlinkSync(filePath);
138
- removeFromIndex(worktree, safeName);
139
- return true;
140
- }
141
- catch {
142
- return false;
143
- }
144
- }
145
- export function searchMemories(worktree, query) {
146
- const all = listMemories(worktree);
147
- const lowerQuery = query.toLowerCase();
148
- return all.filter((entry) => entry.name.toLowerCase().includes(lowerQuery) ||
149
- entry.description.toLowerCase().includes(lowerQuery) ||
150
- entry.content.toLowerCase().includes(lowerQuery));
151
- }
152
- export function readIndex(worktree) {
153
- const entrypoint = getMemoryEntrypoint(worktree);
154
- try {
155
- return readFileSync(entrypoint, "utf-8");
156
- }
157
- catch {
158
- return "";
159
- }
160
- }
161
- function buildIndexPointer(fileName, name, description) {
162
- return `- [${name}](${fileName}) — ${description}`;
163
- }
164
- function updateIndex(worktree, fileName, name, description) {
165
- const entrypoint = getMemoryEntrypoint(worktree);
166
- const existing = readIndex(worktree);
167
- const lines = existing.split("\n").filter((l) => l.trim());
168
- const pointer = buildIndexPointer(fileName, name, description);
169
- const existingIdx = lines.findIndex((l) => l.includes(`(${fileName})`));
170
- if (existingIdx >= 0) {
171
- lines[existingIdx] = pointer;
172
- }
173
- else {
174
- lines.push(pointer);
175
- }
176
- writeFileSync(entrypoint, lines.join("\n") + "\n", "utf-8");
177
- }
178
- function removeFromIndex(worktree, fileName) {
179
- const entrypoint = getMemoryEntrypoint(worktree);
180
- const existing = readIndex(worktree);
181
- const lines = existing
182
- .split("\n")
183
- .filter((l) => l.trim() && !l.includes(`(${fileName})`));
184
- writeFileSync(entrypoint, lines.length > 0 ? lines.join("\n") + "\n" : "", "utf-8");
185
- }
186
- function formatFileSize(bytes) {
187
- if (bytes < 1024)
188
- return `${bytes}B`;
189
- return `${(bytes / 1024).toFixed(1)}KB`;
190
- }
191
- // Port of Claude Code's truncateEntrypointContent() from memdir.ts.
192
- // Uses .length (char count, same as Claude Code) for byte measurement.
193
- export function truncateEntrypoint(raw) {
194
- const trimmed = raw.trim();
195
- if (!trimmed)
196
- return { content: "", lineCount: 0, byteCount: 0, wasLineTruncated: false, wasByteTruncated: false };
197
- const contentLines = trimmed.split("\n");
198
- const lineCount = contentLines.length;
199
- const byteCount = trimmed.length;
200
- const wasLineTruncated = lineCount > MAX_ENTRYPOINT_LINES;
201
- const wasByteTruncated = byteCount > MAX_ENTRYPOINT_BYTES;
202
- if (!wasLineTruncated && !wasByteTruncated) {
203
- return { content: trimmed, lineCount, byteCount, wasLineTruncated, wasByteTruncated };
204
- }
205
- let truncated = wasLineTruncated
206
- ? contentLines.slice(0, MAX_ENTRYPOINT_LINES).join("\n")
207
- : trimmed;
208
- if (truncated.length > MAX_ENTRYPOINT_BYTES) {
209
- const cutAt = truncated.lastIndexOf("\n", MAX_ENTRYPOINT_BYTES);
210
- truncated = truncated.slice(0, cutAt > 0 ? cutAt : MAX_ENTRYPOINT_BYTES);
211
- }
212
- const reason = wasByteTruncated && !wasLineTruncated
213
- ? `${formatFileSize(byteCount)} (limit: ${formatFileSize(MAX_ENTRYPOINT_BYTES)}) — index entries are too long`
214
- : wasLineTruncated && !wasByteTruncated
215
- ? `${lineCount} lines (limit: ${MAX_ENTRYPOINT_LINES})`
216
- : `${lineCount} lines and ${formatFileSize(byteCount)}`;
217
- return {
218
- content: truncated + `\n\n> WARNING: ${ENTRYPOINT_NAME} is ${reason}. Only part of it was loaded. Keep index entries to one line under ~200 chars; move detail into topic files.`,
219
- lineCount,
220
- byteCount,
221
- wasLineTruncated,
222
- wasByteTruncated,
223
- };
224
- }
@@ -1,20 +0,0 @@
1
- import type { MemoryType } from "./memory.js";
2
- export type MemoryHeader = {
3
- filename: string;
4
- filePath: string;
5
- mtimeMs: number;
6
- name: string | null;
7
- description: string | null;
8
- type: MemoryType | undefined;
9
- };
10
- /**
11
- * Recursive scan of memory directory. Reads only frontmatter (first N lines),
12
- * returns headers sorted by mtime desc, capped at MAX_MEMORY_FILES.
13
- * Port of Claude Code's scanMemoryFiles().
14
- */
15
- export declare function scanMemoryFiles(memoryDir: string): MemoryHeader[];
16
- export declare function formatMemoryManifest(memories: MemoryHeader[]): string;
17
- export declare function getMemoryManifest(worktree: string): {
18
- headers: MemoryHeader[];
19
- manifest: string;
20
- };
@@ -1,106 +0,0 @@
1
- import { readdirSync, readFileSync, statSync } from "fs";
2
- import { basename, join } from "path";
3
- import { getMemoryDir, ENTRYPOINT_NAME, MAX_MEMORY_FILES, FRONTMATTER_MAX_LINES, } from "./paths.js";
4
- const MEMORY_TYPES = ["user", "feedback", "project", "reference"];
5
- function parseMemoryType(raw) {
6
- if (!raw)
7
- return undefined;
8
- return MEMORY_TYPES.includes(raw) ? raw : undefined;
9
- }
10
- function readFileHeader(filePath, maxLines) {
11
- try {
12
- const raw = readFileSync(filePath, "utf-8");
13
- const stat = statSync(filePath);
14
- const lines = raw.split("\n");
15
- const header = lines.slice(0, maxLines).join("\n");
16
- return { content: header, mtimeMs: stat.mtimeMs };
17
- }
18
- catch {
19
- return { content: "", mtimeMs: 0 };
20
- }
21
- }
22
- function parseFrontmatterHeader(raw) {
23
- const trimmed = raw.trim();
24
- if (!trimmed.startsWith("---")) {
25
- return {};
26
- }
27
- const lines = trimmed.split("\n");
28
- let closingLineIdx = -1;
29
- for (let i = 1; i < lines.length; i++) {
30
- if (lines[i].trimEnd() === "---") {
31
- closingLineIdx = i;
32
- break;
33
- }
34
- }
35
- if (closingLineIdx === -1) {
36
- return {};
37
- }
38
- const frontmatter = {};
39
- for (let i = 1; i < closingLineIdx; i++) {
40
- const line = lines[i];
41
- const colonIdx = line.indexOf(":");
42
- if (colonIdx === -1)
43
- continue;
44
- const key = line.slice(0, colonIdx).trim();
45
- const value = line.slice(colonIdx + 1).trim();
46
- if (key && value) {
47
- frontmatter[key] = value;
48
- }
49
- }
50
- return frontmatter;
51
- }
52
- /**
53
- * Recursive scan of memory directory. Reads only frontmatter (first N lines),
54
- * returns headers sorted by mtime desc, capped at MAX_MEMORY_FILES.
55
- * Port of Claude Code's scanMemoryFiles().
56
- */
57
- export function scanMemoryFiles(memoryDir) {
58
- try {
59
- const entries = readdirSync(memoryDir, { recursive: true, encoding: "utf-8" });
60
- const mdFiles = entries.filter((f) => f.endsWith(".md") && basename(f) !== ENTRYPOINT_NAME);
61
- const headers = [];
62
- for (const relativePath of mdFiles) {
63
- const filePath = join(memoryDir, relativePath);
64
- try {
65
- const { content, mtimeMs } = readFileHeader(filePath, FRONTMATTER_MAX_LINES);
66
- const frontmatter = parseFrontmatterHeader(content);
67
- headers.push({
68
- filename: relativePath,
69
- filePath,
70
- mtimeMs,
71
- name: frontmatter.name || null,
72
- description: frontmatter.description || null,
73
- type: parseMemoryType(frontmatter.type),
74
- });
75
- }
76
- catch {
77
- // skip unreadable files
78
- }
79
- }
80
- return headers
81
- .sort((a, b) => b.mtimeMs - a.mtimeMs)
82
- .slice(0, MAX_MEMORY_FILES);
83
- }
84
- catch {
85
- return [];
86
- }
87
- }
88
- // Port of Claude Code's formatMemoryManifest():
89
- // `- [type] filename (ISO timestamp): description` per line
90
- export function formatMemoryManifest(memories) {
91
- return memories
92
- .map((m) => {
93
- const tag = m.type ? `[${m.type}] ` : "";
94
- const ts = new Date(m.mtimeMs).toISOString();
95
- return m.description
96
- ? `- ${tag}${m.filename} (${ts}): ${m.description}`
97
- : `- ${tag}${m.filename} (${ts})`;
98
- })
99
- .join("\n");
100
- }
101
- export function getMemoryManifest(worktree) {
102
- const memoryDir = getMemoryDir(worktree);
103
- const headers = scanMemoryFiles(memoryDir);
104
- const manifest = formatMemoryManifest(headers);
105
- return { headers, manifest };
106
- }
@@ -1,9 +0,0 @@
1
- export declare function getNativeExtractTimeoutMs(raw?: string | undefined): number;
2
- export declare function getNativeExtractMaxSteps(raw?: string | undefined): number | undefined;
3
- export type MemorySaveOutcome = {
4
- filePath: string;
5
- fileName: string;
6
- unchanged: boolean;
7
- };
8
- export declare function formatMemorySaveResult(outcome: MemorySaveOutcome, savedThisRun?: readonly string[]): string;
9
- export declare function logNativeExtractionFailure(client: unknown, directory: string, sessionID: string, error: unknown): void;
@@ -1,80 +0,0 @@
1
- const DEFAULT_NATIVE_EXTRACT_TIMEOUT_MS = 120_000;
2
- const MAX_TIMER_DELAY_MS = 2_147_483_647;
3
- // A legitimate extraction is 1 memory_list + a handful of memory_save calls; 30 agentic steps leaves
4
- // generous headroom while still terminating a model that keeps re-saving the same files (#35).
5
- const DEFAULT_NATIVE_EXTRACT_MAX_STEPS = 30;
6
- function getErrorMessage(error) {
7
- if (error instanceof Error)
8
- return error.message;
9
- if (error && typeof error === "object") {
10
- const message = error.message;
11
- if (typeof message === "string")
12
- return message;
13
- }
14
- return String(error);
15
- }
16
- export function getNativeExtractTimeoutMs(raw = process.env.OPENCODE_MEMORY_EXTRACT_TIMEOUT_MS) {
17
- const timeout = Number(raw);
18
- return Number.isSafeInteger(timeout) && timeout > 0 && timeout <= MAX_TIMER_DELAY_MS
19
- ? timeout
20
- : DEFAULT_NATIVE_EXTRACT_TIMEOUT_MS;
21
- }
22
- // Step cap for the extraction agent (OpenCode `agent.<name>.steps`). Unset/invalid → default;
23
- // "0" → undefined, i.e. no cap (the wall-clock timeout becomes the only backstop).
24
- export function getNativeExtractMaxSteps(raw = process.env.OPENCODE_MEMORY_EXTRACT_MAX_STEPS) {
25
- const trimmed = raw?.trim();
26
- // Only a plain non-negative integer counts; "" / " " must not become Number("") === 0 (no cap).
27
- if (!trimmed || !/^\d+$/.test(trimmed))
28
- return DEFAULT_NATIVE_EXTRACT_MAX_STEPS;
29
- const steps = Number(trimmed);
30
- if (!Number.isSafeInteger(steps))
31
- return DEFAULT_NATIVE_EXTRACT_MAX_STEPS;
32
- return steps === 0 ? undefined : steps;
33
- }
34
- // Tool result for memory_save. Inside an extraction fork, `savedThisRun` (file names already saved
35
- // by this fork, in order) is appended as an explicit done-signal so the model does not lose track on
36
- // long transcripts and re-save the same memories until the timeout kills it (#35).
37
- export function formatMemorySaveResult(outcome, savedThisRun) {
38
- const inExtractionRun = savedThisRun !== undefined;
39
- // The current file is recorded before formatting, so "earlier" means it appeared before this call.
40
- const savedEarlierThisRun = inExtractionRun && savedThisRun.indexOf(outcome.fileName) < savedThisRun.length - 1;
41
- let head;
42
- if (outcome.unchanged) {
43
- head = savedEarlierThisRun
44
- ? `Skipped: "${outcome.fileName}" was already saved earlier in this extraction run with identical content — nothing written.`
45
- : `Skipped: "${outcome.fileName}" already exists with identical content — nothing written (${outcome.filePath}).`;
46
- }
47
- else if (savedEarlierThisRun) {
48
- head = `Updated "${outcome.fileName}" (first saved earlier in this extraction run) at ${outcome.filePath}`;
49
- }
50
- else {
51
- head = `Memory saved to ${outcome.filePath}`;
52
- }
53
- if (!inExtractionRun)
54
- return head;
55
- const unique = Array.from(new Set(savedThisRun));
56
- return (`${head}\n\n` +
57
- `Saved so far in this extraction run (${unique.length}): ${unique.join(", ")}\n` +
58
- "These memories are already persisted — do not call memory_save for them again. " +
59
- "Once every distinct memory worth keeping is saved, stop calling tools and reply with a one-line summary.");
60
- }
61
- export function logNativeExtractionFailure(client, directory, sessionID, error) {
62
- const c = client;
63
- if (!c?.app?.log)
64
- return;
65
- try {
66
- const message = getErrorMessage(error);
67
- void c.app.log({
68
- body: {
69
- service: "opencode-claude-memory",
70
- level: "error",
71
- message: "Native extraction failed",
72
- extra: { error: message, sessionID },
73
- },
74
- query: { directory },
75
- }).catch(() => { });
76
- }
77
- catch {
78
- // Logging must stay best-effort: stderr is rendered into the OpenCode chat UI.
79
- }
80
- }
package/dist/paths.js DELETED
@@ -1,141 +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
- import { homedir } from "os";
5
- import { join, dirname, resolve, sep } from "path";
6
- import { mkdirSync, existsSync, readFileSync, statSync, realpathSync } from "fs";
7
- export const ENTRYPOINT_NAME = "MEMORY.md";
8
- export const MAX_ENTRYPOINT_LINES = 200;
9
- export const MAX_ENTRYPOINT_BYTES = 25_000;
10
- export const MAX_MEMORY_FILES = 200;
11
- export const MAX_MEMORY_FILE_BYTES = 40_000;
12
- export const FRONTMATTER_MAX_LINES = 30;
13
- export function validateMemoryFileName(fileName) {
14
- const base = fileName.endsWith(".md") ? fileName.slice(0, -3) : fileName;
15
- if (base.length === 0) {
16
- throw new Error("Memory file name cannot be empty");
17
- }
18
- if (base.includes("/") || base.includes("\\")) {
19
- throw new Error(`Memory file name must not contain path separators: ${fileName}`);
20
- }
21
- if (base.includes("..")) {
22
- throw new Error(`Memory file name must not contain path traversal: ${fileName}`);
23
- }
24
- if (base.includes("\0")) {
25
- throw new Error(`Memory file name must not contain null bytes: ${fileName}`);
26
- }
27
- if (base.startsWith(".")) {
28
- throw new Error(`Memory file name must not start with '.': ${fileName}`);
29
- }
30
- if (base.toUpperCase() === "MEMORY") {
31
- throw new Error(`'MEMORY' is a reserved name and cannot be used as a memory file name`);
32
- }
33
- return `${base}.md`;
34
- }
35
- const MAX_SANITIZED_LENGTH = 200;
36
- // Exact copy of Claude Code's djb2Hash() from utils/hash.ts
37
- function djb2Hash(str) {
38
- let hash = 0;
39
- for (let i = 0; i < str.length; i++) {
40
- hash = ((hash << 5) - hash + str.charCodeAt(i)) | 0;
41
- }
42
- return hash;
43
- }
44
- function simpleHash(str) {
45
- return Math.abs(djb2Hash(str)).toString(36);
46
- }
47
- // Exact copy of Claude Code's sanitizePath() from utils/sessionStoragePortable.ts
48
- export function sanitizePath(name) {
49
- const sanitized = name.replace(/[^a-zA-Z0-9]/g, "-");
50
- if (sanitized.length <= MAX_SANITIZED_LENGTH) {
51
- return sanitized;
52
- }
53
- const hash = simpleHash(name);
54
- return `${sanitized.slice(0, MAX_SANITIZED_LENGTH)}-${hash}`;
55
- }
56
- // Matches Claude Code's findGitRoot() from utils/git.ts
57
- function findGitRoot(startPath) {
58
- let current = resolve(startPath);
59
- const root = current.substring(0, current.indexOf(sep) + 1) || sep;
60
- while (current !== root) {
61
- try {
62
- const gitPath = join(current, ".git");
63
- const s = statSync(gitPath);
64
- if (s.isDirectory() || s.isFile()) {
65
- return current.normalize("NFC");
66
- }
67
- }
68
- catch { }
69
- const parent = dirname(current);
70
- if (parent === current)
71
- break;
72
- current = parent;
73
- }
74
- try {
75
- const gitPath = join(root, ".git");
76
- const s = statSync(gitPath);
77
- if (s.isDirectory() || s.isFile()) {
78
- return root.normalize("NFC");
79
- }
80
- }
81
- catch { }
82
- return null;
83
- }
84
- // Matches Claude Code's resolveCanonicalRoot() from utils/git.ts
85
- // Resolves worktrees to the main repo root via .git -> gitdir -> commondir chain
86
- function resolveCanonicalRoot(gitRoot) {
87
- try {
88
- const gitContent = readFileSync(join(gitRoot, ".git"), "utf-8").trim();
89
- if (!gitContent.startsWith("gitdir:")) {
90
- return gitRoot;
91
- }
92
- const worktreeGitDir = resolve(gitRoot, gitContent.slice("gitdir:".length).trim());
93
- const commonDir = resolve(worktreeGitDir, readFileSync(join(worktreeGitDir, "commondir"), "utf-8").trim());
94
- // SECURITY: validate worktreeGitDir is a direct child of <commonDir>/worktrees/
95
- if (resolve(dirname(worktreeGitDir)) !== join(commonDir, "worktrees")) {
96
- return gitRoot;
97
- }
98
- // SECURITY: validate gitdir back-link points to our .git
99
- const backlink = realpathSync(readFileSync(join(worktreeGitDir, "gitdir"), "utf-8").trim());
100
- if (backlink !== join(realpathSync(gitRoot), ".git")) {
101
- return gitRoot;
102
- }
103
- if (commonDir.endsWith(`${sep}.git`) || commonDir.endsWith("/.git")) {
104
- return dirname(commonDir).normalize("NFC");
105
- }
106
- return commonDir.normalize("NFC");
107
- }
108
- catch {
109
- return gitRoot;
110
- }
111
- }
112
- export function findCanonicalGitRoot(startPath) {
113
- const root = findGitRoot(startPath);
114
- if (!root)
115
- return null;
116
- return resolveCanonicalRoot(root);
117
- }
118
- function getClaudeConfigHomeDir() {
119
- return (process.env.CLAUDE_CONFIG_DIR ?? join(homedir(), ".claude")).normalize("NFC");
120
- }
121
- export function getProjectDir(worktree) {
122
- const canonicalRoot = findCanonicalGitRoot(worktree) ?? worktree;
123
- return join(getClaudeConfigHomeDir(), "projects", sanitizePath(canonicalRoot));
124
- }
125
- export function getMemoryDir(worktree) {
126
- const memoryDir = join(getProjectDir(worktree), "memory");
127
- ensureDir(memoryDir);
128
- return memoryDir;
129
- }
130
- export function getMemoryEntrypoint(worktree) {
131
- return join(getMemoryDir(worktree), ENTRYPOINT_NAME);
132
- }
133
- export function isMemoryPath(absolutePath, worktree) {
134
- const memDir = getMemoryDir(worktree);
135
- return absolutePath.startsWith(memDir);
136
- }
137
- export function ensureDir(dir) {
138
- if (!existsSync(dir)) {
139
- mkdirSync(dir, { recursive: true });
140
- }
141
- }
package/dist/prompt.d.ts DELETED
@@ -1,4 +0,0 @@
1
- export type BuildMemorySystemPromptOptions = {
2
- includeIndex?: boolean;
3
- };
4
- export declare function buildMemorySystemPrompt(worktree: string, recalledMemoriesSection?: string, options?: BuildMemorySystemPromptOptions): string;
package/dist/recall.d.ts DELETED
@@ -1,12 +0,0 @@
1
- import type { MemoryHeader } from "./memoryScan.js";
2
- export type RecalledMemory = {
3
- fileName: string;
4
- filePath: string;
5
- name: string;
6
- type: string;
7
- description: string;
8
- content: string;
9
- ageInDays: number;
10
- };
11
- export declare function recallSelectedMemories(headers: readonly MemoryHeader[], selectedFilenames: readonly string[], alreadySurfaced?: ReadonlySet<string>): RecalledMemory[];
12
- export declare function formatRecalledMemories(memories: RecalledMemory[]): string;