opencode-claude-memory 2.1.3 → 2.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -1
- package/dist/agents.d.ts +1 -1
- package/dist/agents.js +10 -6
- package/dist/config.d.ts +3 -0
- package/dist/config.js +11 -1
- package/dist/extraction/ExtractionCoordinator.js +3 -0
- package/dist/host/generate.d.ts +2 -0
- package/dist/host/generate.js +7 -0
- package/dist/host/v1/agents.d.ts +2 -2
- package/dist/host/v1/agents.js +4 -4
- package/dist/host/v1/host.js +27 -5
- package/dist/host/v1/plugin.js +1 -1
- package/dist/host/v2/agents.d.ts +1 -1
- package/dist/host/v2/agents.js +2 -2
- package/dist/host/v2/host.d.ts +0 -1
- package/dist/host/v2/host.js +1 -7
- package/dist/host/v2/plugin.js +4 -3
- package/dist/prompt/sections.d.ts +1 -0
- package/dist/prompt/sections.js +8 -0
- package/dist/prompt/systemPrompt.d.ts +3 -1
- package/dist/prompt/systemPrompt.js +33 -16
- package/dist/recall/selector.js +3 -0
- package/dist/store/MemoryStore.d.ts +3 -0
- package/dist/store/MemoryStore.js +12 -1
- package/dist/tools.js +4 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -86,7 +86,8 @@ All behaviour is configured through OpenCode's own configuration. There are no `
|
|
|
86
86
|
["opencode-claude-memory", {
|
|
87
87
|
"extract": { "enabled": true, "timeoutMs": 120000, "debounceMs": 10000, "maxConversationChars": 60000, "catchUpLimit": 5 },
|
|
88
88
|
"autodream": { "enabled": true, "minHours": 24, "minSessions": 5, "timeoutMs": 300000 },
|
|
89
|
-
"recall": { "enabled": true, "waitMs": 1500, "timeoutMs": 30000, "maxMemories": 5 }
|
|
89
|
+
"recall": { "enabled": true, "waitMs": 1500, "timeoutMs": 30000, "maxMemories": 5 },
|
|
90
|
+
"readOnly": false
|
|
90
91
|
}]
|
|
91
92
|
],
|
|
92
93
|
"agent": {
|
|
@@ -101,6 +102,8 @@ All behaviour is configured through OpenCode's own configuration. There are no `
|
|
|
101
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.
|
|
102
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.
|
|
103
104
|
- `CLAUDE_CONFIG_DIR` is honoured exactly like Claude Code does, and is the only environment variable the plugin reads.
|
|
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.
|
|
104
107
|
|
|
105
108
|
```jsonc
|
|
106
109
|
// opencode.json — OpenCode 2.x
|
|
@@ -205,6 +208,10 @@ Everything the environment variables used to control now lives under `extract`,
|
|
|
205
208
|
|
|
206
209
|
**Can I disable extraction, auto-dream or recall?** Yes — `extract.enabled`, `autodream.enabled`, `recall.enabled` in the plugin options.
|
|
207
210
|
|
|
211
|
+
**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).
|
|
212
|
+
|
|
213
|
+
**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.
|
|
214
|
+
|
|
208
215
|
**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).
|
|
209
216
|
|
|
210
217
|
**Does the extraction fork see my whole conversation?** Only the messages after the last extraction, capped at `extract.maxConversationChars` (newest first); on OpenCode 2.x only those after the last compaction. The fork can only call memory tools.
|
package/dist/agents.d.ts
CHANGED
|
@@ -7,4 +7,4 @@ export type MemoryAgentDefaults = {
|
|
|
7
7
|
steps?: number;
|
|
8
8
|
allowedTools: readonly MemoryToolName[];
|
|
9
9
|
};
|
|
10
|
-
export declare function memoryAgentDefaults(agents: MemoryAgents): Record<string, MemoryAgentDefaults>;
|
|
10
|
+
export declare function memoryAgentDefaults(agents: MemoryAgents, readOnly?: boolean): Record<string, MemoryAgentDefaults>;
|
package/dist/agents.js
CHANGED
|
@@ -7,13 +7,17 @@ export const MEMORY_TOOL_NAMES = [
|
|
|
7
7
|
"memory_search",
|
|
8
8
|
"memory_read",
|
|
9
9
|
];
|
|
10
|
-
export function memoryAgentDefaults(agents) {
|
|
10
|
+
export function memoryAgentDefaults(agents, readOnly = false) {
|
|
11
|
+
const recall = {
|
|
12
|
+
temperature: 0,
|
|
13
|
+
prompt: SELECT_MEMORIES_SYSTEM_PROMPT,
|
|
14
|
+
allowedTools: [],
|
|
15
|
+
};
|
|
16
|
+
// Read-only runs no extraction or auto-dream, so their agents (which may write) are not registered.
|
|
17
|
+
if (readOnly)
|
|
18
|
+
return { [agents.recall]: recall };
|
|
11
19
|
return {
|
|
12
|
-
[agents.recall]:
|
|
13
|
-
temperature: 0,
|
|
14
|
-
prompt: SELECT_MEMORIES_SYSTEM_PROMPT,
|
|
15
|
-
allowedTools: [],
|
|
16
|
-
},
|
|
20
|
+
[agents.recall]: recall,
|
|
17
21
|
[agents.extract]: {
|
|
18
22
|
prompt: EXTRACT_PROMPT,
|
|
19
23
|
// A legitimate extraction is a handful of memory_save calls; the step cap terminates a model
|
package/dist/config.d.ts
CHANGED
|
@@ -51,6 +51,7 @@ export declare const MemoryOptionsSchema: z.ZodObject<{
|
|
|
51
51
|
waitMs?: number | undefined;
|
|
52
52
|
maxMemories?: number | undefined;
|
|
53
53
|
}>>;
|
|
54
|
+
readOnly: z.ZodDefault<z.ZodBoolean>;
|
|
54
55
|
}, "strict", z.ZodTypeAny, {
|
|
55
56
|
extract: {
|
|
56
57
|
enabled: boolean;
|
|
@@ -71,6 +72,7 @@ export declare const MemoryOptionsSchema: z.ZodObject<{
|
|
|
71
72
|
waitMs: number;
|
|
72
73
|
maxMemories: number;
|
|
73
74
|
};
|
|
75
|
+
readOnly: boolean;
|
|
74
76
|
}, {
|
|
75
77
|
extract?: {
|
|
76
78
|
enabled?: boolean | undefined;
|
|
@@ -91,6 +93,7 @@ export declare const MemoryOptionsSchema: z.ZodObject<{
|
|
|
91
93
|
waitMs?: number | undefined;
|
|
92
94
|
maxMemories?: number | undefined;
|
|
93
95
|
} | undefined;
|
|
96
|
+
readOnly?: boolean | undefined;
|
|
94
97
|
}>;
|
|
95
98
|
export type MemoryOptions = z.infer<typeof MemoryOptionsSchema>;
|
|
96
99
|
export declare const MEMORY_AGENTS: {
|
package/dist/config.js
CHANGED
|
@@ -36,6 +36,11 @@ export const MemoryOptionsSchema = z
|
|
|
36
36
|
})
|
|
37
37
|
.strict()
|
|
38
38
|
.default({}),
|
|
39
|
+
// Use Claude Code's memory without ever writing to the Claude config directory: no memory_save /
|
|
40
|
+
// memory_delete tools, no extraction or auto-dream, no plugin state or log files, and the memory
|
|
41
|
+
// folder is not created. Recall and memory_list / memory_search / memory_read keep working. For
|
|
42
|
+
// hosts that may read what Claude Code remembers but must not add to it.
|
|
43
|
+
readOnly: z.boolean().default(false),
|
|
39
44
|
})
|
|
40
45
|
.strict();
|
|
41
46
|
// Agent names are fixed. Users customise the agents themselves (`agent.opencode-memory-extract.model`
|
|
@@ -70,8 +75,13 @@ export function parseMemoryOptions(options) {
|
|
|
70
75
|
return result.data;
|
|
71
76
|
}
|
|
72
77
|
export function parseConfig(options, env = process.env, homeDir = homedir()) {
|
|
78
|
+
const parsed = parseMemoryOptions(options);
|
|
73
79
|
return {
|
|
74
|
-
...
|
|
80
|
+
...parsed,
|
|
81
|
+
// Read-only switches off everything that writes, whatever the per-feature options say.
|
|
82
|
+
...(parsed.readOnly
|
|
83
|
+
? { extract: { ...parsed.extract, enabled: false }, autodream: { ...parsed.autodream, enabled: false } }
|
|
84
|
+
: {}),
|
|
75
85
|
claudeConfigDir: resolveClaudeConfigDir(env),
|
|
76
86
|
homeDir,
|
|
77
87
|
agents: MEMORY_AGENTS,
|
|
@@ -136,6 +136,9 @@ export class ExtractionCoordinator {
|
|
|
136
136
|
this.clearTimer(sessionID);
|
|
137
137
|
this.busy.delete(sessionID);
|
|
138
138
|
this.savedByMainAgent.delete(sessionID);
|
|
139
|
+
// Existing state may come from an earlier writable run; read-only mode must leave it untouched.
|
|
140
|
+
if (this.deps.config.readOnly)
|
|
141
|
+
return;
|
|
139
142
|
// A deleted session never needs catching up: hosts that cannot list sessions (V2) would otherwise
|
|
140
143
|
// keep spending catch-up slots and a failing transcript read on it until the 30-day prune.
|
|
141
144
|
if (!this.state.getSession(sessionID))
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export function buildGeneratePrompt(input) {
|
|
2
|
+
const sections = [input.system?.trim(), input.text];
|
|
3
|
+
if (input.schema) {
|
|
4
|
+
sections.push(`Respond with only a JSON object matching this JSON schema, without code fences or commentary:\n${JSON.stringify(input.schema)}`);
|
|
5
|
+
}
|
|
6
|
+
return sections.filter((section) => Boolean(section)).join("\n\n");
|
|
7
|
+
}
|
package/dist/host/v1/agents.d.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import type { MemoryAgents } from "../../config.js";
|
|
2
2
|
import type { AgentConfig, PluginConfig } from "./sdk.js";
|
|
3
|
-
export declare function buildAgentDefaults(agents: MemoryAgents): Record<string, AgentConfig>;
|
|
3
|
+
export declare function buildAgentDefaults(agents: MemoryAgents, readOnly?: boolean): Record<string, AgentConfig>;
|
|
4
4
|
export declare function mergeAgentConfig(defaults: AgentConfig, user: AgentConfig | undefined): AgentConfig;
|
|
5
5
|
export declare class AgentRegistry {
|
|
6
6
|
private readonly defaults;
|
|
7
7
|
private readonly merged;
|
|
8
|
-
constructor(agents: MemoryAgents);
|
|
8
|
+
constructor(agents: MemoryAgents, readOnly?: boolean);
|
|
9
9
|
register(config: PluginConfig): void;
|
|
10
10
|
toolsFor(name: string): Record<string, boolean> | undefined;
|
|
11
11
|
}
|
package/dist/host/v1/agents.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// V1 registration of the hidden memory agents. Users override any field in opencode.json
|
|
2
2
|
// (`agent.opencode-memory-extract.model`, ...); the plugin only fills in what they did not set.
|
|
3
3
|
import { memoryAgentDefaults } from "../../agents.js";
|
|
4
|
-
export function buildAgentDefaults(agents) {
|
|
5
|
-
return Object.fromEntries(Object.entries(memoryAgentDefaults(agents)).map(([name, defaults]) => {
|
|
4
|
+
export function buildAgentDefaults(agents, readOnly = false) {
|
|
5
|
+
return Object.fromEntries(Object.entries(memoryAgentDefaults(agents, readOnly)).map(([name, defaults]) => {
|
|
6
6
|
const config = {
|
|
7
7
|
mode: "all",
|
|
8
8
|
hidden: true,
|
|
@@ -27,8 +27,8 @@ export function mergeAgentConfig(defaults, user) {
|
|
|
27
27
|
export class AgentRegistry {
|
|
28
28
|
defaults;
|
|
29
29
|
merged;
|
|
30
|
-
constructor(agents) {
|
|
31
|
-
this.defaults = buildAgentDefaults(agents);
|
|
30
|
+
constructor(agents, readOnly = false) {
|
|
31
|
+
this.defaults = buildAgentDefaults(agents, readOnly);
|
|
32
32
|
this.merged = { ...this.defaults };
|
|
33
33
|
}
|
|
34
34
|
// `config` hook: merge defaults under the user's own entries and remember the outcome so forks can
|
package/dist/host/v1/host.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
// MemoryHost over the V1 SDK client (`server` plugin entry). Every call keeps the exact SDK traffic
|
|
2
2
|
// the coordinators used to make directly: deadlines, `directory` query and the tool sandbox.
|
|
3
3
|
import { withDeadline } from "../../util/timeout.js";
|
|
4
|
+
import { buildGeneratePrompt } from "../generate.js";
|
|
4
5
|
import { SDK_READ_TIMEOUT_MS } from "../types.js";
|
|
5
|
-
import { runForkSession } from "./fork.js";
|
|
6
|
+
import { ForkSessionError, runForkSession } from "./fork.js";
|
|
6
7
|
import { unwrapData } from "./sdk.js";
|
|
7
8
|
// Structured output when the server produced it, otherwise the text parts of the answer.
|
|
8
9
|
export function forkAnswerText(response) {
|
|
@@ -25,6 +26,11 @@ export function forkAnswerText(response) {
|
|
|
25
26
|
.filter((text) => typeof text === "string")
|
|
26
27
|
.join("\n");
|
|
27
28
|
}
|
|
29
|
+
// A model or provider without structured output fails a json_schema request (the server reports a
|
|
30
|
+
// StructuredOutputError) even though its text answer is fine, so recall silently selected nothing.
|
|
31
|
+
function isStructuredOutputError(error) {
|
|
32
|
+
return error instanceof ForkSessionError && error.message.includes("StructuredOutputError");
|
|
33
|
+
}
|
|
28
34
|
export function createV1Host({ client, directory, toolsFor }) {
|
|
29
35
|
return {
|
|
30
36
|
async readTranscript(sessionID) {
|
|
@@ -58,7 +64,7 @@ export function createV1Host({ client, directory, toolsFor }) {
|
|
|
58
64
|
});
|
|
59
65
|
},
|
|
60
66
|
async generate(input) {
|
|
61
|
-
const
|
|
67
|
+
const run = (structured) => runForkSession({
|
|
62
68
|
client,
|
|
63
69
|
directory,
|
|
64
70
|
parentSessionID: input.parentSessionID,
|
|
@@ -66,14 +72,30 @@ export function createV1Host({ client, directory, toolsFor }) {
|
|
|
66
72
|
agent: input.agent,
|
|
67
73
|
system: input.system,
|
|
68
74
|
tools: toolsFor?.(input.agent) ?? { "*": false },
|
|
69
|
-
...(input.schema ? { format: { type: "json_schema", schema: input.schema } } : {}),
|
|
70
|
-
parts: [
|
|
75
|
+
...(structured && input.schema ? { format: { type: "json_schema", schema: input.schema } } : {}),
|
|
76
|
+
parts: [
|
|
77
|
+
{
|
|
78
|
+
type: "text",
|
|
79
|
+
text: structured ? input.text : buildGeneratePrompt({ text: input.text, schema: input.schema }),
|
|
80
|
+
},
|
|
81
|
+
],
|
|
71
82
|
timeoutMs: input.timeoutMs,
|
|
72
83
|
onCreated: input.onCreated,
|
|
73
84
|
onFinished: input.onFinished,
|
|
74
85
|
onCleanupFailed: input.onCleanupFailed,
|
|
75
86
|
});
|
|
76
|
-
|
|
87
|
+
if (input.schema) {
|
|
88
|
+
try {
|
|
89
|
+
return forkAnswerText(await run(true));
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
if (!isStructuredOutputError(error))
|
|
93
|
+
throw error;
|
|
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.
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return forkAnswerText(await run(false));
|
|
77
99
|
},
|
|
78
100
|
};
|
|
79
101
|
}
|
package/dist/host/v1/plugin.js
CHANGED
|
@@ -18,7 +18,7 @@ export const createMemoryPlugin = (env, homeDir) => async ({ worktree, directory
|
|
|
18
18
|
const store = new MemoryStore(resolveMemoryRoot(worktree, dir), config);
|
|
19
19
|
const log = createLogger(client, dir);
|
|
20
20
|
const owned = new OwnedSessions();
|
|
21
|
-
const agents = new AgentRegistry(config.agents);
|
|
21
|
+
const agents = new AgentRegistry(config.agents, config.readOnly);
|
|
22
22
|
const host = client
|
|
23
23
|
? createV1Host({ client, directory: dir, toolsFor: (name) => agents.toolsFor(name) })
|
|
24
24
|
: undefined;
|
package/dist/host/v2/agents.d.ts
CHANGED
|
@@ -7,7 +7,7 @@ export declare const PERMISSIONS_PROBE_AGENT = "opencode-memory-permissions-prob
|
|
|
7
7
|
export declare function sandboxRules(allowedTools: readonly string[]): PermissionRule[];
|
|
8
8
|
export declare const PROBE_RULES: readonly PermissionRule[];
|
|
9
9
|
export declare function userRules(rules: readonly PermissionRule[], baseline: readonly PermissionRule[]): PermissionRule[];
|
|
10
|
-
export declare function applyMemoryAgents(editor: AgentEditor, agents: MemoryAgents): void;
|
|
10
|
+
export declare function applyMemoryAgents(editor: AgentEditor, agents: MemoryAgents, readOnly?: boolean): void;
|
|
11
11
|
export declare function ownAgentRules(resolved: readonly PermissionRule[], sandbox: readonly PermissionRule[], probe: readonly PermissionRule[]): PermissionRule[] | undefined;
|
|
12
12
|
export declare function forkSessionRules(sandbox: readonly PermissionRule[], resolved: readonly PermissionRule[] | undefined, probe: readonly PermissionRule[] | undefined): {
|
|
13
13
|
rules: PermissionRule[];
|
package/dist/host/v2/agents.js
CHANGED
|
@@ -25,7 +25,7 @@ export function userRules(rules, baseline) {
|
|
|
25
25
|
const base = new Set(baseline.map(ruleKey));
|
|
26
26
|
return rules.filter((rule) => !base.has(ruleKey(rule))).map((rule) => ({ ...rule }));
|
|
27
27
|
}
|
|
28
|
-
export function applyMemoryAgents(editor, agents) {
|
|
28
|
+
export function applyMemoryAgents(editor, agents, readOnly = false) {
|
|
29
29
|
editor.update(BASELINE_PROBE_AGENT, () => { });
|
|
30
30
|
const baseline = (editor.get(BASELINE_PROBE_AGENT)?.permissions ?? []).map((rule) => ({ ...rule }));
|
|
31
31
|
editor.remove(BASELINE_PROBE_AGENT);
|
|
@@ -35,7 +35,7 @@ export function applyMemoryAgents(editor, agents) {
|
|
|
35
35
|
agent.description = "opencode-claude-memory internal: permission probe, never runs";
|
|
36
36
|
agent.permissions = PROBE_RULES.map((rule) => ({ ...rule }));
|
|
37
37
|
});
|
|
38
|
-
for (const [name, defaults] of Object.entries(memoryAgentDefaults(agents))) {
|
|
38
|
+
for (const [name, defaults] of Object.entries(memoryAgentDefaults(agents, readOnly))) {
|
|
39
39
|
applyAgent(editor, name, defaults, baseline);
|
|
40
40
|
}
|
|
41
41
|
}
|
package/dist/host/v2/host.d.ts
CHANGED
|
@@ -17,7 +17,6 @@ export type V2HostOptions = {
|
|
|
17
17
|
sandboxFor: (name: string) => PermissionRule[];
|
|
18
18
|
log?: Logger;
|
|
19
19
|
};
|
|
20
|
-
export declare function buildGeneratePrompt(input: Pick<GenerateInput, "system" | "text" | "schema">): string;
|
|
21
20
|
export declare class V2Host implements MemoryHost {
|
|
22
21
|
private readonly options;
|
|
23
22
|
private readonly forkSystems;
|
package/dist/host/v2/host.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { withDeadline } from "../../util/timeout.js";
|
|
2
|
+
import { buildGeneratePrompt } from "../generate.js";
|
|
2
3
|
import { SDK_READ_TIMEOUT_MS } from "../types.js";
|
|
3
4
|
import { forkSessionRules, PERMISSIONS_PROBE_AGENT } from "./agents.js";
|
|
4
5
|
import { runV2Fork } from "./fork.js";
|
|
@@ -7,13 +8,6 @@ import { toTranscript } from "./transcript.js";
|
|
|
7
8
|
function bounded(what, timeoutMs, call) {
|
|
8
9
|
return withDeadline(what, timeoutMs, () => call());
|
|
9
10
|
}
|
|
10
|
-
export function buildGeneratePrompt(input) {
|
|
11
|
-
const sections = [input.system?.trim(), input.text];
|
|
12
|
-
if (input.schema) {
|
|
13
|
-
sections.push(`Respond with only a JSON object matching this JSON schema, without code fences or commentary:\n${JSON.stringify(input.schema)}`);
|
|
14
|
-
}
|
|
15
|
-
return sections.filter((section) => Boolean(section)).join("\n\n");
|
|
16
|
-
}
|
|
17
11
|
export class V2Host {
|
|
18
12
|
options;
|
|
19
13
|
// Per-fork system prompt, injected by the context hook (V2 prompts carry no system field).
|
package/dist/host/v2/plugin.js
CHANGED
|
@@ -41,9 +41,10 @@ export const createV2Setup = (env, homeDir) => async (ctx) => {
|
|
|
41
41
|
const directory = ctx.location.directory;
|
|
42
42
|
const worktree = ctx.location.project?.directory ?? directory;
|
|
43
43
|
const store = new MemoryStore(resolveMemoryRoot(worktree, directory), config);
|
|
44
|
-
|
|
44
|
+
// The log file lives under the Claude config directory, which read-only mode never writes.
|
|
45
|
+
const log = config.readOnly ? () => { } : createFileLogger(store.stateDir);
|
|
45
46
|
const owned = new OwnedSessions();
|
|
46
|
-
const defaults = memoryAgentDefaults(config.agents);
|
|
47
|
+
const defaults = memoryAgentDefaults(config.agents, config.readOnly);
|
|
47
48
|
const host = new V2Host({
|
|
48
49
|
session: ctx.session,
|
|
49
50
|
generateText: (input) => ctx.generate.text(input),
|
|
@@ -73,7 +74,7 @@ export const createV2Setup = (env, homeDir) => async (ctx) => {
|
|
|
73
74
|
}
|
|
74
75
|
};
|
|
75
76
|
const registerHooks = async () => {
|
|
76
|
-
await register("agent.transform", () => ctx.agent.transform((editor) => applyMemoryAgents(editor, config.agents)));
|
|
77
|
+
await register("agent.transform", () => ctx.agent.transform((editor) => applyMemoryAgents(editor, config.agents, config.readOnly)));
|
|
77
78
|
await register("tool.transform", () => ctx.tool.transform((editor) => {
|
|
78
79
|
for (const spec of buildMemoryToolSpecs(store, extraction)) {
|
|
79
80
|
editor.add(toV2Tool(spec));
|
|
@@ -2,6 +2,7 @@ export declare const FRONTMATTER_EXAMPLE: string[];
|
|
|
2
2
|
export declare const TYPES_SECTION: string;
|
|
3
3
|
export declare const WHAT_NOT_TO_SAVE: string;
|
|
4
4
|
export declare const WHEN_TO_ACCESS: string;
|
|
5
|
+
export declare const WHEN_TO_ACCESS_READ_ONLY: string;
|
|
5
6
|
export declare const TRUSTING_RECALL: string;
|
|
6
7
|
export declare const PERSISTENCE_SECTION: string;
|
|
7
8
|
export declare function buildSearchingPastContextSection(memoryDir: string, projectDir: string): string[];
|
package/dist/prompt/sections.js
CHANGED
|
@@ -102,6 +102,14 @@ export const WHEN_TO_ACCESS = [
|
|
|
102
102
|
"- Memory records can become stale over time. Use memory as context for what was true at a given point in time. Before answering the user or building assumptions based solely on information in memory records, verify that the memory is still correct and up-to-date by reading the current state of the files or resources. If a recalled memory conflicts with current information, trust what you observe now and answer from that, not from the memory.",
|
|
103
103
|
'- Do not delete a memory because it looks stale or because what it names cannot be found: it may live on another branch, in another checkout or outside the repo, and the note is still history. Update the memory with what you observed instead (e.g. "`scripts/deploy.sh` not found as of 2026-03-05"), so the next session does not repeat the check. Delete a memory only when the user asks you to forget it, or when a corrected or merged memory replaces it.',
|
|
104
104
|
].join("\n");
|
|
105
|
+
// WHEN_TO_ACCESS for `readOnly`: the same reading rules, without the bullets that edit or delete.
|
|
106
|
+
export const WHEN_TO_ACCESS_READ_ONLY = [
|
|
107
|
+
"## When to access memories",
|
|
108
|
+
"- When memories seem relevant, or the user references prior-conversation work.",
|
|
109
|
+
"- You MUST access memory when the user explicitly asks you to check or recall something.",
|
|
110
|
+
"- If the user says to *ignore* or *not use* memory: proceed as if MEMORY.md were empty. Do not apply remembered facts, cite, compare against, or mention memory content.",
|
|
111
|
+
"- Memory records can become stale over time. Use memory as context for what was true at a given point in time. Before answering the user or building assumptions based solely on information in memory records, verify that the memory is still correct and up-to-date by reading the current state of the files or resources. If a recalled memory conflicts with current information, trust what you observe now and answer from that, not from the memory.",
|
|
112
|
+
].join("\n");
|
|
105
113
|
// Port of Claude Code's TRUSTING_RECALL_SECTION from memoryTypes.ts
|
|
106
114
|
export const TRUSTING_RECALL = [
|
|
107
115
|
"## Before recommending from memory",
|
|
@@ -4,4 +4,6 @@ export declare const RECALLED_MEMORIES_HEADING = "## Recalled Memories";
|
|
|
4
4
|
export type BuildMemorySystemPromptOptions = {
|
|
5
5
|
includeIndex?: boolean;
|
|
6
6
|
};
|
|
7
|
-
export declare function buildMemorySystemPrompt(store: Pick<MemoryStore, "memoryDir" | "projectDir" | "readIndex"
|
|
7
|
+
export declare function buildMemorySystemPrompt(store: Pick<MemoryStore, "memoryDir" | "projectDir" | "readIndex"> & {
|
|
8
|
+
readOnly?: boolean;
|
|
9
|
+
}, recalledMemoriesSection?: string, options?: BuildMemorySystemPromptOptions): string;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { truncateEntrypoint } from "../store/indexFile.js";
|
|
2
2
|
import { ENTRYPOINT_NAME, MAX_ENTRYPOINT_LINES } from "../store/paths.js";
|
|
3
|
-
import { buildSearchingPastContextSection, FRONTMATTER_EXAMPLE, PERSISTENCE_SECTION, TRUSTING_RECALL, TYPES_SECTION, WHAT_NOT_TO_SAVE, WHEN_TO_ACCESS, } from "./sections.js";
|
|
3
|
+
import { buildSearchingPastContextSection, FRONTMATTER_EXAMPLE, PERSISTENCE_SECTION, TRUSTING_RECALL, TYPES_SECTION, WHAT_NOT_TO_SAVE, WHEN_TO_ACCESS, WHEN_TO_ACCESS_READ_ONLY, } from "./sections.js";
|
|
4
4
|
// First line of every system prompt segment this plugin injects. The messages transform uses it to
|
|
5
5
|
// find (and, on ignore-memory turns, strip) the plugin's own segment without guessing from headings.
|
|
6
6
|
export const AUTO_MEMORY_MARKER = "<!-- opencode-claude-memory -->";
|
|
@@ -26,29 +26,44 @@ export function buildMemorySystemPrompt(store, recalledMemoriesSection, options
|
|
|
26
26
|
"- Update memories that turn out to be outdated or wrong, recording what you found; delete one only when the user asks you to forget it or when a corrected or merged memory replaces it",
|
|
27
27
|
"- Do not write duplicate memories. First check if there is an existing memory you can update before writing a new one.",
|
|
28
28
|
].join("\n");
|
|
29
|
-
|
|
29
|
+
// Read-only (config `readOnly`): how to read the memory, nothing about writing it.
|
|
30
|
+
const readOnlyLines = [
|
|
30
31
|
AUTO_MEMORY_MARKER,
|
|
31
32
|
"# Auto Memory",
|
|
32
33
|
"",
|
|
33
|
-
`You have a persistent, file-based memory system at \`${store.memoryDir}
|
|
34
|
+
`You have a persistent, file-based memory system at \`${store.memoryDir}\`, shared with Claude Code. In this session it is read-only: never create, edit or delete anything in that directory, even when the user asks you to remember or forget something — tell them memory is read-only here instead.`,
|
|
34
35
|
"",
|
|
35
|
-
|
|
36
|
-
"",
|
|
37
|
-
"If the user explicitly asks you to remember something, save it immediately as whichever type fits best. If they ask you to forget something, find and remove the relevant entry.",
|
|
38
|
-
"",
|
|
39
|
-
TYPES_SECTION,
|
|
40
|
-
WHAT_NOT_TO_SAVE,
|
|
41
|
-
"",
|
|
42
|
-
howToSave,
|
|
43
|
-
"",
|
|
44
|
-
WHEN_TO_ACCESS,
|
|
36
|
+
WHEN_TO_ACCESS_READ_ONLY,
|
|
45
37
|
"",
|
|
46
38
|
TRUSTING_RECALL,
|
|
47
39
|
"",
|
|
48
|
-
PERSISTENCE_SECTION,
|
|
49
|
-
"",
|
|
50
40
|
...buildSearchingPastContextSection(store.memoryDir, store.projectDir),
|
|
51
41
|
];
|
|
42
|
+
const lines = store.readOnly
|
|
43
|
+
? readOnlyLines
|
|
44
|
+
: [
|
|
45
|
+
AUTO_MEMORY_MARKER,
|
|
46
|
+
"# Auto Memory",
|
|
47
|
+
"",
|
|
48
|
+
`You have a persistent, file-based memory system at \`${store.memoryDir}\`. This directory already exists — write to it directly (do not run mkdir or check for its existence).`,
|
|
49
|
+
"",
|
|
50
|
+
"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.",
|
|
51
|
+
"",
|
|
52
|
+
"If the user explicitly asks you to remember something, save it immediately as whichever type fits best. If they ask you to forget something, find and remove the relevant entry.",
|
|
53
|
+
"",
|
|
54
|
+
TYPES_SECTION,
|
|
55
|
+
WHAT_NOT_TO_SAVE,
|
|
56
|
+
"",
|
|
57
|
+
howToSave,
|
|
58
|
+
"",
|
|
59
|
+
WHEN_TO_ACCESS,
|
|
60
|
+
"",
|
|
61
|
+
TRUSTING_RECALL,
|
|
62
|
+
"",
|
|
63
|
+
PERSISTENCE_SECTION,
|
|
64
|
+
"",
|
|
65
|
+
...buildSearchingPastContextSection(store.memoryDir, store.projectDir),
|
|
66
|
+
];
|
|
52
67
|
if (includeIndex) {
|
|
53
68
|
const indexContent = store.readIndex();
|
|
54
69
|
if (indexContent.trim()) {
|
|
@@ -56,7 +71,9 @@ export function buildMemorySystemPrompt(store, recalledMemoriesSection, options
|
|
|
56
71
|
lines.push(`## ${ENTRYPOINT_NAME}`, "", truncated);
|
|
57
72
|
}
|
|
58
73
|
else {
|
|
59
|
-
lines.push(`## ${ENTRYPOINT_NAME}`, "",
|
|
74
|
+
lines.push(`## ${ENTRYPOINT_NAME}`, "", store.readOnly
|
|
75
|
+
? `${ENTRYPOINT_NAME} is currently empty.`
|
|
76
|
+
: `Your ${ENTRYPOINT_NAME} is currently empty. When you save new memories, they will appear here.`);
|
|
60
77
|
}
|
|
61
78
|
}
|
|
62
79
|
if (recalledMemoriesSection?.trim()) {
|
package/dist/recall/selector.js
CHANGED
|
@@ -16,6 +16,9 @@ export const SELECT_MEMORIES_SCHEMA = {
|
|
|
16
16
|
};
|
|
17
17
|
export const RECALL_SELECTOR_TITLE = "opencode-memory recall selector";
|
|
18
18
|
function selectedFrom(value) {
|
|
19
|
+
// A text answer often drops the wrapper object and gives the list alone.
|
|
20
|
+
if (Array.isArray(value))
|
|
21
|
+
return value.filter((item) => typeof item === "string");
|
|
19
22
|
if (!value || typeof value !== "object")
|
|
20
23
|
return undefined;
|
|
21
24
|
const selected = value.selected_memories;
|
|
@@ -22,6 +22,7 @@ export type ListOptions = {
|
|
|
22
22
|
};
|
|
23
23
|
export type MemoryStoreOptions = {
|
|
24
24
|
claudeConfigDir: string;
|
|
25
|
+
readOnly?: boolean;
|
|
25
26
|
now?: () => Date;
|
|
26
27
|
};
|
|
27
28
|
export declare class MemoryStore {
|
|
@@ -33,6 +34,7 @@ export declare class MemoryStore {
|
|
|
33
34
|
readonly memoryDir: string;
|
|
34
35
|
readonly entrypoint: string;
|
|
35
36
|
readonly stateDir: string;
|
|
37
|
+
readonly readOnly: boolean;
|
|
36
38
|
private readonly now;
|
|
37
39
|
constructor(memoryRoot: string, options: MemoryStoreOptions);
|
|
38
40
|
scan(): MemoryHeader[];
|
|
@@ -44,4 +46,5 @@ export declare class MemoryStore {
|
|
|
44
46
|
search(query: string): MemoryEntry[];
|
|
45
47
|
readIndex(): string;
|
|
46
48
|
private writeIndex;
|
|
49
|
+
private assertWritable;
|
|
47
50
|
}
|
|
@@ -18,19 +18,22 @@ export class MemoryStore {
|
|
|
18
18
|
// Plugin-private state (extraction watermarks, auto-dream gate) lives next to, not inside, the
|
|
19
19
|
// Claude Code project directory so Claude Code never sees it.
|
|
20
20
|
stateDir;
|
|
21
|
+
readOnly;
|
|
21
22
|
now;
|
|
22
23
|
constructor(memoryRoot, options) {
|
|
23
24
|
this.memoryRoot = memoryRoot;
|
|
24
25
|
this.gitRoot = findGitRoot(memoryRoot);
|
|
25
26
|
this.canonicalRoot = findCanonicalGitRoot(memoryRoot) ?? memoryRoot;
|
|
26
27
|
this.claudeConfigDir = options.claudeConfigDir;
|
|
28
|
+
this.readOnly = options.readOnly ?? false;
|
|
27
29
|
this.now = options.now ?? (() => new Date());
|
|
28
30
|
const projectKey = sanitizePath(this.canonicalRoot);
|
|
29
31
|
this.projectDir = join(this.claudeConfigDir, "projects", projectKey);
|
|
30
32
|
this.memoryDir = join(this.projectDir, "memory");
|
|
31
33
|
this.entrypoint = join(this.memoryDir, ENTRYPOINT_NAME);
|
|
32
34
|
this.stateDir = join(this.claudeConfigDir, "opencode-memory", projectKey);
|
|
33
|
-
|
|
35
|
+
if (!this.readOnly)
|
|
36
|
+
mkdirSync(this.memoryDir, { recursive: true });
|
|
34
37
|
}
|
|
35
38
|
scan() {
|
|
36
39
|
return scanMemoryFiles(this.memoryDir);
|
|
@@ -55,6 +58,7 @@ export class MemoryStore {
|
|
|
55
58
|
return readMemoryEntry(this.memoryDir, relativePath);
|
|
56
59
|
}
|
|
57
60
|
save(input) {
|
|
61
|
+
this.assertWritable();
|
|
58
62
|
const { relativePath, filePath } = resolveMemoryFilePath(this.memoryDir, input.fileName);
|
|
59
63
|
if (typeof input.name !== "string" || !input.name.trim()) {
|
|
60
64
|
throw new Error("Memory name is required");
|
|
@@ -97,6 +101,7 @@ export class MemoryStore {
|
|
|
97
101
|
// directory, as dsh-unified-memory does, so an auto-dream prune can never silently destroy
|
|
98
102
|
// another tool's work.
|
|
99
103
|
delete(fileName) {
|
|
104
|
+
this.assertWritable();
|
|
100
105
|
const { relativePath, filePath } = resolveMemoryFilePath(this.memoryDir, fileName);
|
|
101
106
|
const existing = readTextFile(filePath);
|
|
102
107
|
if (existing === null)
|
|
@@ -129,6 +134,12 @@ export class MemoryStore {
|
|
|
129
134
|
writeIndex(content) {
|
|
130
135
|
writeFileAtomicSync(this.entrypoint, content);
|
|
131
136
|
}
|
|
137
|
+
// Defence in depth: in read-only mode nothing registers a writing tool or runs a writing fork, so
|
|
138
|
+
// reaching this is a bug, and it must fail rather than touch Claude Code's files.
|
|
139
|
+
assertWritable() {
|
|
140
|
+
if (this.readOnly)
|
|
141
|
+
throw new Error("Memory is read-only (readOnly: true)");
|
|
142
|
+
}
|
|
132
143
|
}
|
|
133
144
|
// A memory's name and description as the scanner (and `read()`) reports them: missing fields
|
|
134
145
|
// count as their defaults.
|
package/dist/tools.js
CHANGED
|
@@ -51,8 +51,10 @@ function str(args, key) {
|
|
|
51
51
|
const value = args[key];
|
|
52
52
|
return typeof value === "string" ? value : "";
|
|
53
53
|
}
|
|
54
|
+
// The tools that change memory files; a read-only store (config `readOnly`) does not offer them.
|
|
55
|
+
const WRITING_TOOLS = ["memory_save", "memory_delete"];
|
|
54
56
|
export function buildMemoryToolSpecs(store, extraction) {
|
|
55
|
-
|
|
57
|
+
const specs = [
|
|
56
58
|
{
|
|
57
59
|
name: "memory_save",
|
|
58
60
|
description: "Save or update a memory for future conversations. " +
|
|
@@ -175,4 +177,5 @@ export function buildMemoryToolSpecs(store, extraction) {
|
|
|
175
177
|
},
|
|
176
178
|
},
|
|
177
179
|
];
|
|
180
|
+
return store.readOnly ? specs.filter((spec) => !WRITING_TOOLS.includes(spec.name)) : specs;
|
|
178
181
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-claude-memory",
|
|
3
|
-
"version": "2.1
|
|
3
|
+
"version": "2.2.1",
|
|
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",
|