opencode-claude-memory 2.1.1 → 2.1.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 +1 -1
- package/dist/store/MemoryStore.d.ts +0 -1
- package/dist/store/MemoryStore.js +46 -18
- package/dist/store/indexFile.d.ts +1 -0
- package/dist/store/indexFile.js +8 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -144,7 +144,7 @@ The V2 plugin API exposes less than V1, so a few behaviours differ:
|
|
|
144
144
|
| Memory directory | `~/.claude/projects/<sanitized canonical git root>/memory/` | identical (`sanitizePath`, worktree → main repo resolution ported byte for byte) |
|
|
145
145
|
| File format | Markdown + `name` / `description` / `type` frontmatter | identical; frontmatter parsed only within the first 30 lines, as in Claude Code |
|
|
146
146
|
| Taxonomy | `user`, `feedback`, `project`, `reference` | identical |
|
|
147
|
-
| `MEMORY.md` | one-line pointers, hand-organisable | read with the same truncation rules; written with minimal line-level edits |
|
|
147
|
+
| `MEMORY.md` | one-line pointers, hand-organisable | read with the same truncation rules; written with minimal line-level edits, hand-written pointer lines are never replaced |
|
|
148
148
|
| Sub-directories | `team/x.md` etc. | scanned, recalled and addressable from every tool |
|
|
149
149
|
| System prompt | memory instructions + index + recalled memories | ported sections (`memoryTypes.ts`, `memdir.ts`) |
|
|
150
150
|
| Recall | LLM side query | LLM side query (`findRelevantMemories.ts` port): a hidden child session on 1.x, a `generate.text` call on 2.x |
|
|
@@ -2,7 +2,7 @@ import { cpSync, lstatSync, mkdirSync, readFileSync, realpathSync, statSync, unl
|
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
import { writeFileAtomicSync } from "../util/exclusiveFile.js";
|
|
4
4
|
import { buildFrontmatter, editFrontmatter, FRONTMATTER_MAX_LINES, frontmatterKeys, ORIGIN, parseFrontmatter, parseMemoryType, } from "./frontmatter.js";
|
|
5
|
-
import { buildIndexPointer,
|
|
5
|
+
import { buildIndexPointer, findIndexPointerLine, readIndexFile, removeIndexLine, upsertIndexLine, } from "./indexFile.js";
|
|
6
6
|
import { ENTRYPOINT_NAME, findCanonicalGitRoot, findGitRoot, MAX_MEMORY_FILE_BYTES, resolveMemoryFilePath, sanitizePath, } from "./paths.js";
|
|
7
7
|
import { formatMemoryManifest, nameFromFilename, readMemoryEntry, scanMemoryFiles, } from "./scan.js";
|
|
8
8
|
// Owns the resolved memory paths for one project. Path resolution (git root, worktree canonical
|
|
@@ -65,9 +65,14 @@ export class MemoryStore {
|
|
|
65
65
|
}
|
|
66
66
|
const existing = readTextFile(filePath);
|
|
67
67
|
const parsed = existing === null ? null : parseFrontmatter(existing);
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
68
|
+
const index = this.readIndex();
|
|
69
|
+
const nextIndex = indexAfterSave(index, relativePath, input, parsed);
|
|
70
|
+
if (parsed !== null && isUnchanged(parsed, relativePath, input)) {
|
|
71
|
+
// The same memory: at most a missing index pointer is added, the file itself is not rewritten.
|
|
72
|
+
if (nextIndex === index)
|
|
73
|
+
return { filePath, fileName: relativePath, unchanged: true };
|
|
74
|
+
this.writeIndex(nextIndex);
|
|
75
|
+
return { filePath, fileName: relativePath, unchanged: false };
|
|
71
76
|
}
|
|
72
77
|
const modified = this.now().toISOString();
|
|
73
78
|
const fileContent = existing === null || parsed === null
|
|
@@ -83,7 +88,8 @@ export class MemoryStore {
|
|
|
83
88
|
throw new Error(`Memory "${relativePath}" frontmatter would exceed ${FRONTMATTER_MAX_LINES} lines; trim its frontmatter first`);
|
|
84
89
|
}
|
|
85
90
|
writeFileAtomicSync(filePath, fileContent);
|
|
86
|
-
|
|
91
|
+
if (nextIndex !== index)
|
|
92
|
+
this.writeIndex(nextIndex);
|
|
87
93
|
return { filePath, fileName: relativePath, unchanged: false };
|
|
88
94
|
}
|
|
89
95
|
// Deleting a memory that is not purely this plugin's (Claude Code's, dsh's, a hand-written one, or
|
|
@@ -123,19 +129,41 @@ export class MemoryStore {
|
|
|
123
129
|
writeIndex(content) {
|
|
124
130
|
writeFileAtomicSync(this.entrypoint, content);
|
|
125
131
|
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
132
|
+
}
|
|
133
|
+
// A memory's name and description as the scanner (and `read()`) reports them: missing fields
|
|
134
|
+
// count as their defaults.
|
|
135
|
+
function previousFields(parsed, relativePath) {
|
|
136
|
+
return {
|
|
137
|
+
name: parsed.frontmatter.name ?? nameFromFilename(relativePath),
|
|
138
|
+
description: parsed.frontmatter.description ?? "",
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
// Unchanged means the same name, description, type and body, whatever else the frontmatter holds,
|
|
142
|
+
// so an identical re-save neither bumps `modified` nor stamps provenance.
|
|
143
|
+
function isUnchanged(parsed, relativePath, input) {
|
|
144
|
+
if (!parsed.hasFrontmatter)
|
|
145
|
+
return false;
|
|
146
|
+
const before = previousFields(parsed, relativePath);
|
|
147
|
+
return (before.name === input.name &&
|
|
148
|
+
before.description === input.description &&
|
|
149
|
+
(parseMemoryType(parsed.frontmatter.type) ?? "user") === input.type &&
|
|
150
|
+
parsed.body.replace(/\r\n/g, "\n") === input.content.trim().replace(/\r\n/g, "\n"));
|
|
151
|
+
}
|
|
152
|
+
// MEMORY.md after a save. Index lines are often hand-written (Claude Code's format is
|
|
153
|
+
// `- [Title](file.md) — one-line hook`, and neither part has to repeat the frontmatter), so an
|
|
154
|
+
// existing pointer is kept unless it is still exactly the line this plugin generates from the
|
|
155
|
+
// memory's previous name and description: only such a line follows a new name or description. A
|
|
156
|
+
// memory without a pointer gains one. No other line is touched.
|
|
157
|
+
function indexAfterSave(raw, relativePath, input, parsed) {
|
|
158
|
+
const pointer = buildIndexPointer(relativePath, input.name, input.description);
|
|
159
|
+
const current = findIndexPointerLine(raw, relativePath);
|
|
160
|
+
if (current === undefined)
|
|
161
|
+
return upsertIndexLine(raw, relativePath, pointer);
|
|
162
|
+
if (current === pointer || parsed === null)
|
|
163
|
+
return raw;
|
|
164
|
+
const before = previousFields(parsed, relativePath);
|
|
165
|
+
const generatedBefore = buildIndexPointer(relativePath, before.name, before.description);
|
|
166
|
+
return current === generatedBefore ? upsertIndexLine(raw, relativePath, pointer) : raw;
|
|
139
167
|
}
|
|
140
168
|
// The frontmatter keys of a memory this plugin creates (buildFrontmatter), each on one line; its own
|
|
141
169
|
// later saves keep exactly this set.
|
|
@@ -2,6 +2,7 @@ export declare const POINTER_RE: RegExp;
|
|
|
2
2
|
export declare function readIndexFile(entrypoint: string): string;
|
|
3
3
|
export declare function buildIndexPointer(fileName: string, name: string, description: string): string;
|
|
4
4
|
export declare function indexHasPointer(raw: string, pointer: string): boolean;
|
|
5
|
+
export declare function findIndexPointerLine(raw: string, fileName: string): string | undefined;
|
|
5
6
|
export declare function pointerTarget(line: string): string | undefined;
|
|
6
7
|
export declare function upsertIndexLine(raw: string, fileName: string, pointer: string): string;
|
|
7
8
|
export declare function removeIndexLine(raw: string, fileName: string): string;
|
package/dist/store/indexFile.js
CHANGED
|
@@ -20,6 +20,14 @@ export function buildIndexPointer(fileName, name, description) {
|
|
|
20
20
|
export function indexHasPointer(raw, pointer) {
|
|
21
21
|
return raw.split(/\r?\n/).some((line) => line.trimEnd() === pointer);
|
|
22
22
|
}
|
|
23
|
+
// The first pointer line for `fileName` without its indentation and trailing whitespace, or
|
|
24
|
+
// undefined when the index has none.
|
|
25
|
+
export function findIndexPointerLine(raw, fileName) {
|
|
26
|
+
return raw
|
|
27
|
+
.split(/\r?\n/)
|
|
28
|
+
.find((line) => pointerTarget(line) === fileName)
|
|
29
|
+
?.trim();
|
|
30
|
+
}
|
|
23
31
|
function detectEol(raw) {
|
|
24
32
|
return raw.includes("\r\n") ? "\r\n" : "\n";
|
|
25
33
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-claude-memory",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "OpenCode plugin for Claude Code memory: persistent, local-first shared memory with Claude Code-compatible Markdown files, LLM recall, in-process auto extraction, and auto-dream consolidation",
|
|
6
6
|
"main": "dist/index.js",
|