opencode-claude-memory 2.2.0 → 2.3.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.
- package/README.md +27 -7
- package/dist/config.d.ts +3 -0
- package/dist/config.js +4 -0
- package/dist/extraction/ExtractionCoordinator.js +9 -6
- package/dist/extraction/autodream.js +1 -1
- package/dist/host/v1/host.js +3 -5
- package/dist/store/MemoryStore.d.ts +4 -0
- package/dist/store/MemoryStore.js +43 -3
- package/dist/store/autoMemoryDirectory.d.ts +4 -0
- package/dist/store/autoMemoryDirectory.js +52 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -73,7 +73,7 @@ The hook names below are OpenCode 1.x's; on 2.x the same steps run on the V2 plu
|
|
|
73
73
|
4. **Auto-dream** — after each extracted session the gate is evaluated (`autodream.minHours` since the last pass **and** `autodream.minSessions` extracted since). When it passes, `opencode-memory-dream` runs with all five memory tools. A lock file prevents two OpenCode processes from consolidating at once.
|
|
74
74
|
5. **Ignore memory** — "ignore memory" in a user message switches memory off for the rest of the session (no index, no recall); "use memory again" switches it back on.
|
|
75
75
|
|
|
76
|
-
State that is private to the plugin (watermarks, auto-dream gate, lock and, on OpenCode 2.x, the log file) lives
|
|
76
|
+
State that is private to the plugin (watermarks, auto-dream gate, lock and, on OpenCode 2.x, the log file) lives under `<CLAUDE_CONFIG_DIR>/opencode-memory/`, never inside the memory directory. The default location is `<project>/`; custom memory directories use the namespaces described below.
|
|
77
77
|
|
|
78
78
|
## 🔧 Configuration
|
|
79
79
|
|
|
@@ -102,7 +102,8 @@ All behaviour is configured through OpenCode's own configuration. There are no `
|
|
|
102
102
|
- When the same plugin is listed in both the global and the project `opencode.json`, OpenCode keeps the **last** declaration (project wins); options are not merged across files.
|
|
103
103
|
- The three agents are registered hidden with a memory-only tool sandbox. On 1.x, override any field (`model`, `steps`, `temperature`, …) under `agent.<name>`; the plugin fills in the rest.
|
|
104
104
|
- `CLAUDE_CONFIG_DIR` is honoured exactly like Claude Code does, and is the only environment variable the plugin reads.
|
|
105
|
-
- `readOnly: true`
|
|
105
|
+
- `readOnly: true` prevents this plugin from writing to the Claude config directory: no `memory_save` / `memory_delete`, no extraction or auto-dream (whatever `extract` / `autodream` say), no plugin state or log writes, and the memory folder is not created. The index, recall and `memory_list` / `memory_search` / `memory_read` keep working, and the prompt tells the model the memory is read-only.
|
|
106
|
+
- This option does not restrict the host's shell or file-editing tools, or other plugins. To enforce read-only access for the entire host, also configure host permissions or filesystem access controls; the memory prompt alone does not enforce it.
|
|
106
107
|
|
|
107
108
|
```jsonc
|
|
108
109
|
// opencode.json — OpenCode 2.x
|
|
@@ -124,9 +125,28 @@ All behaviour is configured through OpenCode's own configuration. There are no `
|
|
|
124
125
|
- Agent overrides live under `agents.<name>` and use 2.x agent fields (`model`, `steps`, `permissions`, …). The plugin's sandbox rules (`*` deny, then allow for the agent's memory tools) come first and your own `permissions` after them; 2.x applies the last matching rule, so your rule wins.
|
|
125
126
|
- The main agent needs no extra permission on 2.x's default `build` agent, which allows everything. If you restrict permissions (for example `*` → `ask` or `deny`), add `{ "action": "memory_*", "resource": "*", "effect": "allow" }` to your `permissions` so the memory tools are neither blocked nor prompted for.
|
|
126
127
|
|
|
128
|
+
**Custom memory directory.** Set `autoMemoryDirectory` in the plugin options to use a fixed memory directory, matching [Claude Code's setting](https://code.claude.com/docs/en/settings-reference#automemorydirectory). For example, on OpenCode 2.x:
|
|
129
|
+
|
|
130
|
+
```jsonc
|
|
131
|
+
{
|
|
132
|
+
"plugins": [
|
|
133
|
+
{ "package": "opencode-claude-memory", "options": { "autoMemoryDirectory": "~/shared-memory" } }
|
|
134
|
+
]
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
The same option works in the 1.x plugin options. Configure the same path as `autoMemoryDirectory` in Claude Code's settings to share this store between both tools; this plugin reads OpenCode options, not Claude Code settings files. OpenCode's plugin declaration precedence above still applies.
|
|
139
|
+
|
|
140
|
+
- The value is the **final memory directory**: `~/shared-memory/MEMORY.md` and its topic files. No project name or `memory/` suffix is appended. All projects configured with the same path share that store; it replaces their default memory directories rather than merging with them. Existing memories are not moved.
|
|
141
|
+
- Path handling matches Claude Code **2.1.292**: expand `~/` (also `~\`), normalise the path and Unicode (NFC), and retain one trailing separator. The directory is not resolved against a Git root. Relative or empty paths, filesystem roots, home-only `~/`, tilde paths escaping the home directory, NUL bytes and network/device roots disable the override and fall back to the default project directory. Whitespace is not trimmed and environment variables are not expanded. Non-string values remain configuration errors.
|
|
142
|
+
- Working directories and session/event handling retain their actual project scope. Watermarks, the auto-dream gate, trash and logs remain project-scoped and are also keyed by the target directory (`<project>/memory-stores/<directory-hash>/`), so switching stores does not reuse another store's watermarks. Within one `CLAUDE_CONFIG_DIR`, extraction and auto-dream targeting the same physical directory share a maintenance lock, including directory symlinks and overrides pointing to a default project store. Auto-dream thresholds remain per project.
|
|
143
|
+
- A symlink alias of this project's default memory directory retains its existing watermarks and auto-dream gate.
|
|
144
|
+
- The memory directory must not equal, contain or reside inside the plugin's private state root (`<CLAUDE_CONFIG_DIR>/opencode-memory/`), including through symlinks. Such a configuration is rejected before creating directories, so deleted-memory backups cannot be scanned as live memories.
|
|
145
|
+
- `readOnly: true` applies to a custom directory too: it is not created, memory writes are disabled, and no plugin state or logs are written.
|
|
146
|
+
|
|
127
147
|
**Models for the memory agents.** OpenCode Zen's free models (e.g. `opencode/big-pickle`) reject every request whose tool list lacks OpenCode's `shell` tool (HTTP 403, *"OpenCode's free tier can only be used from within OpenCode"*). The sandboxed extraction and auto-dream forks are exactly such requests on both 1.x and 2.x, and so is recall on 2.x. On a free Zen model, extraction, auto-dream and (on 2.x) recall therefore fail. Configure a paid model for `opencode-memory-extract`, `opencode-memory-dream` and `opencode-memory-recall` (e.g. `opencode/gpt-6-luna`); the main agent can stay on a free model.
|
|
128
148
|
|
|
129
|
-
**Logs.** On 1.x logs go to the OpenCode service log (`opencode` log directory, service `opencode-claude-memory`). 2.x gives plugins no log API, so the plugin writes
|
|
149
|
+
**Logs.** On 1.x logs go to the OpenCode service log (`opencode` log directory, service `opencode-claude-memory`). 2.x gives plugins no log API, so the plugin writes `opencode-memory.log` in its state directory instead (rotated at about 1 MB). This is `<CLAUDE_CONFIG_DIR>/opencode-memory/<project>/` by default, or the custom-store state namespace described above.
|
|
130
150
|
|
|
131
151
|
## 🆕 Differences on OpenCode 2.x
|
|
132
152
|
|
|
@@ -143,7 +163,7 @@ The V2 plugin API exposes less than V1, so a few behaviours differ:
|
|
|
143
163
|
|
|
144
164
|
| Aspect | Claude Code | This plugin |
|
|
145
165
|
|---|---|---|
|
|
146
|
-
| Memory directory | `~/.claude/projects/<sanitized canonical git root>/memory
|
|
166
|
+
| Memory directory | `~/.claude/projects/<sanitized canonical git root>/memory/`, or `autoMemoryDirectory` | same default layout and custom-directory path semantics (verified against Claude Code 2.1.292) |
|
|
147
167
|
| File format | Markdown + `name` (kebab-case slug) / `description` / `metadata.type` frontmatter | identical; older top-level `type:` still read; frontmatter parsed only within the first 30 lines, as in Claude Code |
|
|
148
168
|
| Taxonomy | `user`, `feedback`, `project`, `reference` | identical |
|
|
149
169
|
| `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 |
|
|
@@ -203,13 +223,13 @@ Everything the environment variables used to control now lives under `extract`,
|
|
|
203
223
|
|
|
204
224
|
**Do I need to migrate existing memory?** No. Existing Claude Code memory files are used as they are.
|
|
205
225
|
|
|
206
|
-
**Where is data stored?** `~/.claude/projects/<project>/memory/` (or `$CLAUDE_CONFIG_DIR/projects/...`). Plugin state lives
|
|
226
|
+
**Where is data stored?** `~/.claude/projects/<project>/memory/` (or `$CLAUDE_CONFIG_DIR/projects/...`), unless `autoMemoryDirectory` selects a custom store. Plugin state lives under `$CLAUDE_CONFIG_DIR/opencode-memory/`, scoped by project and, for custom stores, target directory. Memories the plugin deletes that are not purely its own (Claude Code's, another tool's, or one of its own that another tool has since edited) are copied to `trash/<timestamp>/` in that state directory first.
|
|
207
227
|
|
|
208
228
|
**Can I disable extraction, auto-dream or recall?** Yes — `extract.enabled`, `autodream.enabled`, `recall.enabled` in the plugin options.
|
|
209
229
|
|
|
210
|
-
**Can
|
|
230
|
+
**Can I make this plugin read Claude Code's memory without changing it?** Yes — `readOnly: true`. The plugin does not write under `CLAUDE_CONFIG_DIR`. Enforcing read-only access for the entire host also requires host permissions or filesystem access controls; see [Configuration](#-configuration).
|
|
211
231
|
|
|
212
|
-
**My recall model has no structured output.** On 1.x
|
|
232
|
+
**My recall model has no structured output.** On 1.x each selector request asks for structured output first. If the server answers with a `StructuredOutputError`, the plugin retries that request once with the schema described in the prompt and parses the text. Later requests still try structured output: a single failure does not establish that the model lacks support. A model that consistently fails structured output therefore needs two attempts per recall request. Other errors are not retried.
|
|
213
233
|
|
|
214
234
|
**Why did my first answer take a moment longer?** The system prompt waits up to `recall.waitMs` for the selector. Set it to `0` to never wait (recalled memories then appear from the second LLM call of a turn onwards).
|
|
215
235
|
|
package/dist/config.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
export declare const MemoryOptionsSchema: z.ZodObject<{
|
|
3
|
+
autoMemoryDirectory: z.ZodOptional<z.ZodString>;
|
|
3
4
|
extract: z.ZodDefault<z.ZodObject<{
|
|
4
5
|
enabled: z.ZodDefault<z.ZodBoolean>;
|
|
5
6
|
timeoutMs: z.ZodDefault<z.ZodNumber>;
|
|
@@ -73,7 +74,9 @@ export declare const MemoryOptionsSchema: z.ZodObject<{
|
|
|
73
74
|
maxMemories: number;
|
|
74
75
|
};
|
|
75
76
|
readOnly: boolean;
|
|
77
|
+
autoMemoryDirectory?: string | undefined;
|
|
76
78
|
}, {
|
|
79
|
+
autoMemoryDirectory?: string | undefined;
|
|
77
80
|
extract?: {
|
|
78
81
|
enabled?: boolean | undefined;
|
|
79
82
|
timeoutMs?: number | undefined;
|
package/dist/config.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { homedir } from "node:os";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { z } from "zod";
|
|
4
|
+
import { resolveAutoMemoryDirectory } from "./store/autoMemoryDirectory.js";
|
|
4
5
|
const MAX_TIMER_DELAY_MS = 2_147_483_647;
|
|
5
6
|
const timeoutMs = (fallback) => z.number().int().positive().max(MAX_TIMER_DELAY_MS).default(fallback);
|
|
6
7
|
// Plugin options come from `opencode.json`: `"plugin": [["opencode-claude-memory", { ... }]]`.
|
|
@@ -8,6 +9,8 @@ const timeoutMs = (fallback) => z.number().int().positive().max(MAX_TIMER_DELAY_
|
|
|
8
9
|
// falling back to defaults.
|
|
9
10
|
export const MemoryOptionsSchema = z
|
|
10
11
|
.object({
|
|
12
|
+
// Final memory directory, with Claude Code's autoMemoryDirectory path semantics.
|
|
13
|
+
autoMemoryDirectory: z.string().optional(),
|
|
11
14
|
extract: z
|
|
12
15
|
.object({
|
|
13
16
|
enabled: z.boolean().default(true),
|
|
@@ -78,6 +81,7 @@ export function parseConfig(options, env = process.env, homeDir = homedir()) {
|
|
|
78
81
|
const parsed = parseMemoryOptions(options);
|
|
79
82
|
return {
|
|
80
83
|
...parsed,
|
|
84
|
+
autoMemoryDirectory: resolveAutoMemoryDirectory(parsed.autoMemoryDirectory, homeDir),
|
|
81
85
|
// Read-only switches off everything that writes, whatever the per-feature options say.
|
|
82
86
|
...(parsed.readOnly
|
|
83
87
|
? { extract: { ...parsed.extract, enabled: false }, autodream: { ...parsed.autodream, enabled: false } }
|
|
@@ -98,7 +98,7 @@ export class ExtractionCoordinator {
|
|
|
98
98
|
this.deps = deps;
|
|
99
99
|
this.now = deps.now ?? Date.now;
|
|
100
100
|
this.state = deps.state ?? new ExtractionStateStore(deps.store.stateDir, this.now);
|
|
101
|
-
this.lock = deps.lock ?? new MaintenanceLock(
|
|
101
|
+
this.lock = deps.lock ?? new MaintenanceLock(deps.store.maintenanceLockPath, this.now);
|
|
102
102
|
this.autodream = deps.host
|
|
103
103
|
? new AutoDream({ ...deps, host: deps.host, state: this.state, now: this.now, lock: this.lock })
|
|
104
104
|
: undefined;
|
|
@@ -192,11 +192,14 @@ export class ExtractionCoordinator {
|
|
|
192
192
|
if (!host)
|
|
193
193
|
return;
|
|
194
194
|
this.warnLegacyShellHook();
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
store.
|
|
198
|
-
|
|
199
|
-
|
|
195
|
+
// The old shell hook maintained the default project store, never a custom destination.
|
|
196
|
+
if (store.usesDefaultMemoryDirectory) {
|
|
197
|
+
migrateLegacyAutodreamState(this.state, `${store.claudeConfigDir}/opencode-memory`, [
|
|
198
|
+
store.gitRoot ?? store.memoryRoot,
|
|
199
|
+
store.memoryRoot,
|
|
200
|
+
store.canonicalRoot,
|
|
201
|
+
]);
|
|
202
|
+
}
|
|
200
203
|
if (config.extract.catchUpLimit <= 0)
|
|
201
204
|
return;
|
|
202
205
|
let sessions;
|
|
@@ -21,7 +21,7 @@ export class AutoDream {
|
|
|
21
21
|
constructor(deps) {
|
|
22
22
|
this.deps = deps;
|
|
23
23
|
this.now = deps.now ?? Date.now;
|
|
24
|
-
this.lock = deps.lock ?? new MaintenanceLock(deps.
|
|
24
|
+
this.lock = deps.lock ?? new MaintenanceLock(deps.store.maintenanceLockPath, this.now);
|
|
25
25
|
}
|
|
26
26
|
// Called from the extraction coordinator's persisted update after a session was extracted.
|
|
27
27
|
static noteSession(state, sessionID) {
|
package/dist/host/v1/host.js
CHANGED
|
@@ -32,9 +32,6 @@ function isStructuredOutputError(error) {
|
|
|
32
32
|
return error instanceof ForkSessionError && error.message.includes("StructuredOutputError");
|
|
33
33
|
}
|
|
34
34
|
export function createV1Host({ client, directory, toolsFor }) {
|
|
35
|
-
// Cleared by the first StructuredOutputError: from then on this host describes the schema in the
|
|
36
|
-
// prompt and the caller parses the text, as the V2 host always does.
|
|
37
|
-
let structuredOutput = true;
|
|
38
35
|
return {
|
|
39
36
|
async readTranscript(sessionID) {
|
|
40
37
|
const response = await withDeadline("session.messages", SDK_READ_TIMEOUT_MS, (signal) => client.session.messages({ path: { id: sessionID }, query: { directory }, signal }));
|
|
@@ -87,14 +84,15 @@ export function createV1Host({ client, directory, toolsFor }) {
|
|
|
87
84
|
onFinished: input.onFinished,
|
|
88
85
|
onCleanupFailed: input.onCleanupFailed,
|
|
89
86
|
});
|
|
90
|
-
if (
|
|
87
|
+
if (input.schema) {
|
|
91
88
|
try {
|
|
92
89
|
return forkAnswerText(await run(true));
|
|
93
90
|
}
|
|
94
91
|
catch (error) {
|
|
95
92
|
if (!isStructuredOutputError(error))
|
|
96
93
|
throw error;
|
|
97
|
-
|
|
94
|
+
// One failed answer does not establish a model capability. Retry only this request as
|
|
95
|
+
// text, leaving later requests (in this session or another) free to use structured output.
|
|
98
96
|
}
|
|
99
97
|
}
|
|
100
98
|
return forkAnswerText(await run(false));
|
|
@@ -22,6 +22,8 @@ export type ListOptions = {
|
|
|
22
22
|
};
|
|
23
23
|
export type MemoryStoreOptions = {
|
|
24
24
|
claudeConfigDir: string;
|
|
25
|
+
autoMemoryDirectory?: string;
|
|
26
|
+
homeDir?: string;
|
|
25
27
|
readOnly?: boolean;
|
|
26
28
|
now?: () => Date;
|
|
27
29
|
};
|
|
@@ -32,8 +34,10 @@ export declare class MemoryStore {
|
|
|
32
34
|
readonly claudeConfigDir: string;
|
|
33
35
|
readonly projectDir: string;
|
|
34
36
|
readonly memoryDir: string;
|
|
37
|
+
readonly usesDefaultMemoryDirectory: boolean;
|
|
35
38
|
readonly entrypoint: string;
|
|
36
39
|
readonly stateDir: string;
|
|
40
|
+
readonly maintenanceLockPath: string;
|
|
37
41
|
readonly readOnly: boolean;
|
|
38
42
|
private readonly now;
|
|
39
43
|
constructor(memoryRoot: string, options: MemoryStoreOptions);
|
|
@@ -1,10 +1,29 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
1
2
|
import { cpSync, lstatSync, mkdirSync, readFileSync, realpathSync, statSync, unlinkSync } from "node:fs";
|
|
2
|
-
import { dirname, join } from "node:path";
|
|
3
|
+
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
3
4
|
import { writeFileAtomicSync } from "../util/exclusiveFile.js";
|
|
5
|
+
import { resolveAutoMemoryDirectory } from "./autoMemoryDirectory.js";
|
|
4
6
|
import { buildFrontmatter, editFrontmatter, FRONTMATTER_MAX_LINES, frontmatterKeys, ORIGIN, parseFrontmatter, parseMemoryType, } from "./frontmatter.js";
|
|
5
7
|
import { buildIndexPointer, findIndexPointerLine, readIndexFile, removeIndexLine, upsertIndexLine, } from "./indexFile.js";
|
|
6
8
|
import { ENTRYPOINT_NAME, findCanonicalGitRoot, findGitRoot, MAX_MEMORY_FILE_BYTES, resolveMemoryFilePath, sanitizePath, } from "./paths.js";
|
|
7
9
|
import { formatMemoryManifest, nameFromFilename, readMemoryEntry, scanMemoryFiles, } from "./scan.js";
|
|
10
|
+
function realDirectory(directory) {
|
|
11
|
+
const absolute = resolve(directory);
|
|
12
|
+
try {
|
|
13
|
+
// Filesystems such as ext4 can keep canonically equivalent Unicode names distinct.
|
|
14
|
+
return realpathSync.native(absolute);
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
// Resolve existing ancestors too: read-only stores and private state may not exist yet,
|
|
18
|
+
// but their parents can still contain symlinks.
|
|
19
|
+
const parent = dirname(absolute);
|
|
20
|
+
return parent === absolute ? absolute : join(realDirectory(parent), basename(absolute));
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function containsDirectory(root, directory) {
|
|
24
|
+
const fromRoot = relative(root, directory);
|
|
25
|
+
return fromRoot === "" || (!isAbsolute(fromRoot) && fromRoot !== ".." && !fromRoot.startsWith(`..${sep}`));
|
|
26
|
+
}
|
|
8
27
|
// Owns the resolved memory paths for one project. Path resolution (git root, worktree canonical
|
|
9
28
|
// root, sanitised project key) happens exactly once, in the constructor.
|
|
10
29
|
export class MemoryStore {
|
|
@@ -14,10 +33,13 @@ export class MemoryStore {
|
|
|
14
33
|
claudeConfigDir;
|
|
15
34
|
projectDir;
|
|
16
35
|
memoryDir;
|
|
36
|
+
usesDefaultMemoryDirectory;
|
|
17
37
|
entrypoint;
|
|
18
38
|
// Plugin-private state (extraction watermarks, auto-dream gate) lives next to, not inside, the
|
|
19
39
|
// Claude Code project directory so Claude Code never sees it.
|
|
20
40
|
stateDir;
|
|
41
|
+
// Shared by every project targeting this memory directory; watermarks stay project-scoped.
|
|
42
|
+
maintenanceLockPath;
|
|
21
43
|
readOnly;
|
|
22
44
|
now;
|
|
23
45
|
constructor(memoryRoot, options) {
|
|
@@ -29,11 +51,29 @@ export class MemoryStore {
|
|
|
29
51
|
this.now = options.now ?? (() => new Date());
|
|
30
52
|
const projectKey = sanitizePath(this.canonicalRoot);
|
|
31
53
|
this.projectDir = join(this.claudeConfigDir, "projects", projectKey);
|
|
32
|
-
|
|
54
|
+
const defaultMemoryDir = join(this.projectDir, "memory");
|
|
55
|
+
this.memoryDir = resolveAutoMemoryDirectory(options.autoMemoryDirectory, options.homeDir ?? "") ?? defaultMemoryDir;
|
|
33
56
|
this.entrypoint = join(this.memoryDir, ENTRYPOINT_NAME);
|
|
34
|
-
|
|
57
|
+
const directory = realDirectory(this.memoryDir);
|
|
58
|
+
const privateRoot = join(this.claudeConfigDir, "opencode-memory");
|
|
59
|
+
const privateDirectory = realDirectory(privateRoot);
|
|
60
|
+
if (containsDirectory(directory, privateDirectory) || containsDirectory(privateDirectory, directory)) {
|
|
61
|
+
throw new Error(`Memory directory must not overlap the plugin state directory: ${privateRoot}`);
|
|
62
|
+
}
|
|
35
63
|
if (!this.readOnly)
|
|
36
64
|
mkdirSync(this.memoryDir, { recursive: true });
|
|
65
|
+
this.usesDefaultMemoryDirectory = directory === realDirectory(defaultMemoryDir);
|
|
66
|
+
const key = createHash("sha256").update(directory).digest("hex");
|
|
67
|
+
const projectStateDir = join(privateRoot, projectKey);
|
|
68
|
+
this.stateDir = this.usesDefaultMemoryDirectory ? projectStateDir : join(projectStateDir, "memory-stores", key);
|
|
69
|
+
// Recognise the default Claude directory layout too, so an explicit override pointing to
|
|
70
|
+
// another project's default store shares its existing maintenance lock.
|
|
71
|
+
const fromProjects = relative(realDirectory(join(this.claudeConfigDir, "projects")), directory);
|
|
72
|
+
const parts = fromProjects.split(sep);
|
|
73
|
+
this.maintenanceLockPath =
|
|
74
|
+
!isAbsolute(fromProjects) && parts.length === 2 && parts[0] !== ".." && parts[1] === "memory"
|
|
75
|
+
? join(privateRoot, parts[0] ?? "", "maintenance.lock")
|
|
76
|
+
: join(privateRoot, "memory-stores", key, "maintenance.lock");
|
|
37
77
|
}
|
|
38
78
|
scan() {
|
|
39
79
|
return scanMemoryFiles(this.memoryDir);
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
type DirectoryPaths = Pick<typeof path, "isAbsolute" | "join" | "normalize" | "sep">;
|
|
3
|
+
export declare function resolveAutoMemoryDirectory(value: string | undefined, homeDir: string, paths?: DirectoryPaths): string | undefined;
|
|
4
|
+
export {};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
// Matches Claude Code 2.1.292's custom auto-memory directory resolver: expand only ~/ (or ~\),
|
|
3
|
+
// normalise lexically, reject unsafe roots, then return an NFC path with one trailing separator.
|
|
4
|
+
// No git discovery, realpath, cwd-relative resolution, whitespace trimming or environment expansion.
|
|
5
|
+
// An invalid string disables the override; it does not make the plugin fail to load.
|
|
6
|
+
export function resolveAutoMemoryDirectory(value, homeDir, paths = path) {
|
|
7
|
+
if (!value)
|
|
8
|
+
return undefined;
|
|
9
|
+
let directory = value;
|
|
10
|
+
if (directory.startsWith("~/") || directory.startsWith("~\\")) {
|
|
11
|
+
const suffix = directory.slice(2);
|
|
12
|
+
const relative = paths.normalize(suffix || ".");
|
|
13
|
+
if (relative === "." ||
|
|
14
|
+
relative === ".." ||
|
|
15
|
+
relative.startsWith(`..${paths.sep}`) ||
|
|
16
|
+
relative.startsWith("../") ||
|
|
17
|
+
relative.startsWith("..\\")) {
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
directory = paths.join(homeDir, suffix);
|
|
21
|
+
}
|
|
22
|
+
const normalized = paths.normalize(directory).replace(/[/\\]+$/, "");
|
|
23
|
+
if (!paths.isAbsolute(normalized) ||
|
|
24
|
+
normalized.length < 3 ||
|
|
25
|
+
/^[A-Za-z]:$/.test(normalized) ||
|
|
26
|
+
normalized.includes("\0") ||
|
|
27
|
+
isNetworkOrDevicePath(normalized)) {
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
return `${normalized}${paths.sep}`.normalize("NFC");
|
|
31
|
+
}
|
|
32
|
+
// Claude Code also rejects UNC/NT namespaces, POSIX network automounts and macOS filesystem
|
|
33
|
+
// pseudo-paths. These checks apply on every platform, after the platform's path normalisation.
|
|
34
|
+
function isNetworkOrDevicePath(directory) {
|
|
35
|
+
const ntNamespace = /^[\\/]\?\?[\\/]/;
|
|
36
|
+
if (/^[\\/]{2}/.test(directory) || ntNamespace.test(directory))
|
|
37
|
+
return true;
|
|
38
|
+
if (directory.includes("??") && ntNamespace.test(path.win32.normalize(directory)))
|
|
39
|
+
return true;
|
|
40
|
+
if (!directory.startsWith("/"))
|
|
41
|
+
return false;
|
|
42
|
+
const parts = directory.split("/").filter(Boolean);
|
|
43
|
+
const first = parts[0] ?? "";
|
|
44
|
+
if (first.toLowerCase() === "network" || /^\.(vol|file|nofollow|resolve)$/i.test(first))
|
|
45
|
+
return true;
|
|
46
|
+
const fold = (part) => part
|
|
47
|
+
.replace(/[\u200c-\u200f\u202a-\u202e\u206a-\u206f\ufeff]/g, "")
|
|
48
|
+
.toUpperCase()
|
|
49
|
+
.toLowerCase();
|
|
50
|
+
return ((parts.length >= 2 && fold(first) === "net") ||
|
|
51
|
+
(parts.length >= 3 && fold(first) === "network" && fold(parts[1] ?? "") === "servers"));
|
|
52
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-claude-memory",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.0",
|
|
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",
|