opencode-claude-memory 2.0.2 → 2.1.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.
Files changed (59) hide show
  1. package/README.md +67 -10
  2. package/dist/agents.d.ts +8 -10
  3. package/dist/agents.js +4 -43
  4. package/dist/extraction/ExtractionCoordinator.d.ts +10 -12
  5. package/dist/extraction/ExtractionCoordinator.js +66 -50
  6. package/dist/extraction/autodream.d.ts +2 -5
  7. package/dist/extraction/autodream.js +3 -7
  8. package/dist/host/types.d.ts +52 -0
  9. package/dist/host/types.js +6 -0
  10. package/dist/host/v1/agents.d.ts +11 -0
  11. package/dist/host/v1/agents.js +48 -0
  12. package/dist/host/v1/coordinators.d.ts +12 -0
  13. package/dist/host/v1/coordinators.js +43 -0
  14. package/dist/{extraction/forkSession.d.ts → host/v1/fork.d.ts} +3 -2
  15. package/dist/{extraction/forkSession.js → host/v1/fork.js} +2 -2
  16. package/dist/host/v1/host.d.ts +9 -0
  17. package/dist/host/v1/host.js +79 -0
  18. package/dist/{hooks → host/v1}/ignore.d.ts +2 -3
  19. package/dist/host/v1/ignore.js +29 -0
  20. package/dist/host/v1/log.d.ts +4 -0
  21. package/dist/host/v1/log.js +20 -0
  22. package/dist/{hooks → host/v1}/messages.d.ts +3 -2
  23. package/dist/{hooks → host/v1}/messages.js +3 -17
  24. package/dist/host/v1/plugin.d.ts +2 -0
  25. package/dist/host/v1/plugin.js +58 -0
  26. package/dist/host/v1/selector.d.ts +18 -0
  27. package/dist/host/v1/selector.js +12 -0
  28. package/dist/host/v1/tools.d.ts +5 -0
  29. package/dist/host/v1/tools.js +17 -0
  30. package/dist/host/v2/agents.d.ts +15 -0
  31. package/dist/host/v2/agents.js +83 -0
  32. package/dist/host/v2/events.d.ts +40 -0
  33. package/dist/host/v2/events.js +137 -0
  34. package/dist/host/v2/fork.d.ts +76 -0
  35. package/dist/host/v2/fork.js +136 -0
  36. package/dist/host/v2/host.d.ts +35 -0
  37. package/dist/host/v2/host.js +86 -0
  38. package/dist/host/v2/log.d.ts +4 -0
  39. package/dist/host/v2/log.js +40 -0
  40. package/dist/host/v2/plugin.d.ts +26 -0
  41. package/dist/host/v2/plugin.js +149 -0
  42. package/dist/host/v2/transcript.d.ts +3 -0
  43. package/dist/host/v2/transcript.js +50 -0
  44. package/dist/index.d.ts +7 -2
  45. package/dist/index.js +5 -55
  46. package/dist/recall/RecallCoordinator.d.ts +15 -10
  47. package/dist/recall/RecallCoordinator.js +26 -35
  48. package/dist/recall/selector.d.ts +14 -19
  49. package/dist/recall/selector.js +37 -47
  50. package/dist/recall/turn.d.ts +4 -0
  51. package/dist/recall/turn.js +49 -0
  52. package/dist/tools.d.ts +16 -3
  53. package/dist/tools.js +59 -41
  54. package/dist/util/log.d.ts +0 -2
  55. package/dist/util/log.js +2 -18
  56. package/package.json +16 -4
  57. package/dist/hooks/ignore.js +0 -53
  58. /package/dist/{sdk.d.ts → host/v1/sdk.d.ts} +0 -0
  59. /package/dist/{sdk.js → host/v1/sdk.js} +0 -0
@@ -0,0 +1,49 @@
1
+ // Host-independent text rules for a user turn: the "ignore memory" / "use memory again" phrases and
2
+ // the keys of memories already surfaced in a system prompt. The host adapters apply them to their
3
+ // own message shapes.
4
+ import { RECALLED_MEMORIES_HEADING } from "../prompt/systemPrompt.js";
5
+ // "Ignore memory" handling. Claude Code's semantics are session-scoped: once the user asks to
6
+ // ignore memory, it stays ignored until they explicitly ask for it back.
7
+ export function detectIgnoreMemory(query) {
8
+ if (!query)
9
+ return false;
10
+ const normalized = query.toLowerCase();
11
+ return (/(ignore|don't use|do not use|without|skip)\s+(the\s+|your\s+)?memory/.test(normalized) ||
12
+ /memory\s+(should be|must be)?\s*ignored/.test(normalized));
13
+ }
14
+ export function detectResumeMemory(query) {
15
+ if (!query)
16
+ return false;
17
+ const normalized = query.toLowerCase();
18
+ return (/(use|enable|resume|restore|bring back|turn on)\s+(the\s+|your\s+)?memory(\s+again)?/.test(normalized) ||
19
+ /memory\s+(back\s+)?on\b/.test(normalized) ||
20
+ /stop ignoring\s+(the\s+|your\s+)?memory/.test(normalized));
21
+ }
22
+ // Replays user queries in order and returns whether memory is ignored at the end. Used to rebuild
23
+ // session state the coordinator no longer holds (process restart, cache eviction), so an "ignore
24
+ // memory" said earlier in the session keeps applying without the user repeating it.
25
+ export function deriveIgnoredFromQueries(queries) {
26
+ let ignored = false;
27
+ for (const query of queries) {
28
+ if (detectIgnoreMemory(query))
29
+ ignored = true;
30
+ else if (ignored && detectResumeMemory(query))
31
+ ignored = false;
32
+ }
33
+ return ignored;
34
+ }
35
+ // Parses "### <name> (<type>)" headers from the ## Recalled Memories section
36
+ // of system prompts. After compaction old system messages disappear, so
37
+ // the returned set naturally shrinks — no manual reset needed.
38
+ export function extractSurfacedMemoryKeys(systemText) {
39
+ const keys = new Set();
40
+ const recalledSection = systemText.indexOf(RECALLED_MEMORIES_HEADING);
41
+ if (recalledSection === -1)
42
+ return keys;
43
+ const headerPattern = /^### (.+?) \((\w+)\)/gm;
44
+ const section = systemText.slice(recalledSection);
45
+ for (let match = headerPattern.exec(section); match !== null; match = headerPattern.exec(section)) {
46
+ keys.add(`${match[1]}|${match[2]}`);
47
+ }
48
+ return keys;
49
+ }
package/dist/tools.d.ts CHANGED
@@ -1,9 +1,22 @@
1
- import { type Hooks } from "@opencode-ai/plugin";
1
+ import { z } from "zod/v4";
2
+ import type { MemoryToolName } from "./agents.js";
2
3
  import type { ExtractionCoordinator } from "./extraction/ExtractionCoordinator.js";
3
4
  import type { MemoryStore, SaveMemoryResult } from "./store/MemoryStore.js";
4
5
  export declare function formatMemorySaveResult(outcome: SaveMemoryResult, savedThisRun?: readonly string[]): string;
5
6
  export declare function memorySaveTitle(type: string, name: string): string | undefined;
6
7
  export declare function memoryListTitle(count: number): string;
7
8
  export declare function memorySearchTitle(query: string, count: number): string;
8
- export type MemoryTools = NonNullable<Hooks["tool"]>;
9
- export declare function buildMemoryTools(store: MemoryStore, extraction: Pick<ExtractionCoordinator, "recordSave">): MemoryTools;
9
+ export type MemoryToolResult = {
10
+ title?: string;
11
+ output: string;
12
+ };
13
+ export type MemoryToolContext = {
14
+ sessionID?: string;
15
+ };
16
+ export type MemoryToolSpec = {
17
+ name: MemoryToolName;
18
+ description: string;
19
+ args: z.ZodRawShape;
20
+ execute(args: Record<string, unknown>, ctx: MemoryToolContext): Promise<MemoryToolResult>;
21
+ };
22
+ export declare function buildMemoryToolSpecs(store: MemoryStore, extraction: Pick<ExtractionCoordinator, "recordSave">): MemoryToolSpec[];
package/dist/tools.js CHANGED
@@ -1,4 +1,9 @@
1
- import { tool } from "@opencode-ai/plugin";
1
+ // The five memory tools, defined once. Each host adapter wraps the specs into its own tool format
2
+ // (V1: `Hooks["tool"]`, V2: `ToolEditor.add`); results carry their own titles.
3
+ //
4
+ // Argument schemas use `zod/v4`: both hosts accept them (V1 duck-types Zod v4 schemas, V2 takes any
5
+ // Standard Schema), so the plugin needs no runtime import from either SDK.
6
+ import { z } from "zod/v4";
2
7
  import { MEMORY_TYPES } from "./store/frontmatter.js";
3
8
  // Tool result for memory_save. Inside an extraction fork, `savedThisRun` (file names already saved
4
9
  // by this fork, in order) is appended as an explicit done-signal so the model does not lose track on
@@ -42,9 +47,14 @@ export function memorySearchTitle(query, count) {
42
47
  return `"${query}" · ${plural(count, "match", "matches")}`;
43
48
  }
44
49
  const FILE_NAME_HINT = 'with or without the .md extension; sub-directories are allowed, e.g. "team/conventions"';
45
- export function buildMemoryTools(store, extraction) {
46
- return {
47
- memory_save: tool({
50
+ function str(args, key) {
51
+ const value = args[key];
52
+ return typeof value === "string" ? value : "";
53
+ }
54
+ export function buildMemoryToolSpecs(store, extraction) {
55
+ return [
56
+ {
57
+ name: "memory_save",
48
58
  description: "Save or update a memory for future conversations. " +
49
59
  "Each memory is stored as a markdown file with frontmatter. " +
50
60
  "Use this when the user explicitly asks you to remember something, " +
@@ -52,52 +62,56 @@ export function buildMemoryTools(store, extraction) {
52
62
  "(user preferences, feedback, project context, external references). " +
53
63
  "Check existing memories first with memory_list or memory_search to avoid duplicates.",
54
64
  args: {
55
- file_name: tool.schema
65
+ file_name: z
56
66
  .string()
57
67
  .describe('File name for the memory (without .md extension). Use snake_case, e.g. "user_role", "feedback_testing_style", "project_auth_rewrite"; a sub-directory prefix such as "team/conventions" is allowed'),
58
- name: tool.schema.string().describe("Human-readable name for this memory"),
59
- description: tool.schema
68
+ name: z.string().describe("Human-readable name for this memory"),
69
+ description: z
60
70
  .string()
61
71
  .describe("One-line description — used to decide relevance in future conversations, so be specific"),
62
- type: tool.schema
72
+ type: z
63
73
  .enum(MEMORY_TYPES)
64
74
  .describe("Memory type: user (about the person), feedback (guidance on approach), project (ongoing work context), reference (pointers to external systems)"),
65
- content: tool.schema
75
+ content: z
66
76
  .string()
67
77
  .describe("Memory content. For feedback/project types, structure as: rule/fact, then **Why:** and **How to apply:** lines"),
68
78
  },
69
79
  async execute(args, ctx) {
80
+ const type = str(args, "type");
70
81
  const outcome = store.save({
71
- fileName: args.file_name,
72
- name: args.name,
73
- description: args.description,
74
- type: args.type,
75
- content: args.content,
82
+ fileName: str(args, "file_name"),
83
+ name: str(args, "name"),
84
+ description: str(args, "description"),
85
+ type,
86
+ content: str(args, "content"),
76
87
  });
77
- const savedThisRun = extraction.recordSave(ctx?.sessionID, outcome.fileName);
88
+ const savedThisRun = extraction.recordSave(ctx.sessionID, outcome.fileName);
78
89
  return {
79
- title: memorySaveTitle(args.type, args.name),
90
+ title: memorySaveTitle(type, str(args, "name")),
80
91
  output: formatMemorySaveResult(outcome, savedThisRun),
81
92
  };
82
93
  },
83
- }),
84
- memory_delete: tool({
94
+ },
95
+ {
96
+ name: "memory_delete",
85
97
  description: "Delete a memory that is outdated, wrong, or no longer relevant. Also removes it from the index.",
86
98
  args: {
87
- file_name: tool.schema.string().describe(`File name of the memory to delete (${FILE_NAME_HINT})`),
99
+ file_name: z.string().describe(`File name of the memory to delete (${FILE_NAME_HINT})`),
88
100
  },
89
101
  async execute(args) {
90
- const { deleted, trashedTo } = store.delete(args.file_name);
91
- let output = `Memory "${args.file_name}" not found.`;
102
+ const fileName = str(args, "file_name");
103
+ const { deleted, trashedTo } = store.delete(fileName);
104
+ let output = `Memory "${fileName}" not found.`;
92
105
  if (deleted) {
93
106
  output = trashedTo
94
- ? `Memory "${args.file_name}" deleted (a copy was kept at ${trashedTo}).`
95
- : `Memory "${args.file_name}" deleted.`;
107
+ ? `Memory "${fileName}" deleted (a copy was kept at ${trashedTo}).`
108
+ : `Memory "${fileName}" deleted.`;
96
109
  }
97
- return { title: args.file_name, output };
110
+ return { title: fileName, output };
98
111
  },
99
- }),
100
- memory_list: tool({
112
+ },
113
+ {
114
+ name: "memory_list",
101
115
  description: "List all saved memories with their names, types, and descriptions. " +
102
116
  "Use this to check what memories exist before saving a new one (to avoid duplicates) " +
103
117
  "or when you need to recall what's been stored.",
@@ -110,36 +124,40 @@ export function buildMemoryTools(store, extraction) {
110
124
  const lines = entries.map((e) => `- **${e.name}** (${e.type}) [${e.filename}]: ${e.description}`);
111
125
  return { title, output: `${entries.length} memories found:\n${lines.join("\n")}` };
112
126
  },
113
- }),
114
- memory_search: tool({
127
+ },
128
+ {
129
+ name: "memory_search",
115
130
  description: "Search memories by keyword. Searches across names, descriptions, and content. " +
116
131
  "Use this to find relevant memories before answering questions or when the user references past conversations.",
117
132
  args: {
118
- query: tool.schema.string().describe("Search query — searches across name, description, and content"),
133
+ query: z.string().describe("Search query — searches across name, description, and content"),
119
134
  },
120
135
  async execute(args) {
121
- const results = store.search(args.query);
122
- const title = memorySearchTitle(args.query, results.length);
136
+ const query = str(args, "query");
137
+ const results = store.search(query);
138
+ const title = memorySearchTitle(query, results.length);
123
139
  if (results.length === 0)
124
- return { title, output: `No memories matching "${args.query}".` };
140
+ return { title, output: `No memories matching "${query}".` };
125
141
  const lines = results.map((e) => `- **${e.name}** (${e.type}) [${e.filename}]: ${e.description}\n Content: ${e.body.slice(0, 200)}${e.body.length > 200 ? "..." : ""}`);
126
- return { title, output: `${results.length} matches for "${args.query}":\n${lines.join("\n")}` };
142
+ return { title, output: `${results.length} matches for "${query}":\n${lines.join("\n")}` };
127
143
  },
128
- }),
129
- memory_read: tool({
144
+ },
145
+ {
146
+ name: "memory_read",
130
147
  description: "Read the full content of a specific memory file.",
131
148
  args: {
132
- file_name: tool.schema.string().describe(`File name of the memory to read (${FILE_NAME_HINT})`),
149
+ file_name: z.string().describe(`File name of the memory to read (${FILE_NAME_HINT})`),
133
150
  },
134
151
  async execute(args) {
135
- const entry = store.read(args.file_name);
152
+ const fileName = str(args, "file_name");
153
+ const entry = store.read(fileName);
136
154
  if (!entry)
137
- return { title: args.file_name, output: `Memory "${args.file_name}" not found.` };
155
+ return { title: fileName, output: `Memory "${fileName}" not found.` };
138
156
  return {
139
- title: args.file_name,
157
+ title: fileName,
140
158
  output: `# ${entry.name}\n**Type:** ${entry.type}\n**Description:** ${entry.description}\n\n${entry.body}`,
141
159
  };
142
160
  },
143
- }),
144
- };
161
+ },
162
+ ];
145
163
  }
@@ -1,6 +1,4 @@
1
- import type { OpencodeClient } from "../sdk.js";
2
1
  export declare const LOG_SERVICE = "opencode-claude-memory";
3
2
  export type LogLevel = "debug" | "info" | "warn" | "error";
4
3
  export type Logger = (level: LogLevel, message: string, extra?: Record<string, unknown>) => void;
5
4
  export declare function getErrorMessage(error: unknown): string;
6
- export declare function createLogger(client: OpencodeClient | undefined, directory: string): Logger;
package/dist/util/log.js CHANGED
@@ -1,3 +1,5 @@
1
+ // Host-independent logging types. Each host adapter supplies its own sink (V1: the OpenCode service
2
+ // log, V2: a file in the plugin state directory); stderr is never used because it ends up in the UI.
1
3
  export const LOG_SERVICE = "opencode-claude-memory";
2
4
  export function getErrorMessage(error) {
3
5
  if (error instanceof Error)
@@ -9,21 +11,3 @@ export function getErrorMessage(error) {
9
11
  }
10
12
  return String(error);
11
13
  }
12
- // Logging goes through the OpenCode service log only. stderr is rendered into the chat UI, so a
13
- // failing background task must never write there. Every call is best-effort and never throws.
14
- export function createLogger(client, directory) {
15
- return (level, message, extra) => {
16
- const log = client?.app?.log;
17
- if (typeof log !== "function")
18
- return;
19
- try {
20
- void Promise.resolve(log.call(client?.app, {
21
- body: { service: LOG_SERVICE, level, message, extra },
22
- query: { directory },
23
- })).catch(() => { });
24
- }
25
- catch {
26
- // best-effort
27
- }
28
- };
29
- }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-claude-memory",
3
- "version": "2.0.2",
3
+ "version": "2.1.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",
@@ -16,7 +16,7 @@
16
16
  "dist"
17
17
  ],
18
18
  "engines": {
19
- "opencode": ">=1.18.0"
19
+ "opencode": ">=1.18.29"
20
20
  },
21
21
  "scripts": {
22
22
  "build": "tsc -p tsconfig.build.json",
@@ -58,14 +58,26 @@
58
58
  ],
59
59
  "license": "MIT",
60
60
  "peerDependencies": {
61
- "@opencode-ai/plugin": ">=1.18.0"
61
+ "@opencode-ai/plugin": ">=1.18.29",
62
+ "@opencode/plugin": ">=2.0.0"
63
+ },
64
+ "peerDependenciesMeta": {
65
+ "@opencode-ai/plugin": {
66
+ "optional": true
67
+ },
68
+ "@opencode/plugin": {
69
+ "optional": true
70
+ }
62
71
  },
63
72
  "dependencies": {
64
- "zod": "^3.24.0"
73
+ "zod": "^3.25.0"
65
74
  },
66
75
  "devDependencies": {
67
76
  "@biomejs/biome": "^2",
68
77
  "@opencode-ai/plugin": "^1.18.16",
78
+ "@opencode/client": "2.0.22",
79
+ "@opencode/plugin": "2.0.22",
80
+ "@opencode/schema": "2.0.22",
69
81
  "bun-types": "^1.3.11",
70
82
  "conventional-changelog-conventionalcommits": "^9",
71
83
  "typescript": "^6.0.2"
@@ -1,53 +0,0 @@
1
- // "Ignore memory" handling. Claude Code's semantics are session-scoped: once the user asks to
2
- // ignore memory, it stays ignored until they explicitly ask for it back.
3
- import { AUTO_MEMORY_MARKER } from "../prompt/systemPrompt.js";
4
- import { extractUserQuery, roleOf } from "./messages.js";
5
- export function detectIgnoreMemory(query) {
6
- if (!query)
7
- return false;
8
- const normalized = query.toLowerCase();
9
- return (/(ignore|don't use|do not use|without|skip)\s+(the\s+|your\s+)?memory/.test(normalized) ||
10
- /memory\s+(should be|must be)?\s*ignored/.test(normalized));
11
- }
12
- export function detectResumeMemory(query) {
13
- if (!query)
14
- return false;
15
- const normalized = query.toLowerCase();
16
- return (/(use|enable|resume|restore|bring back|turn on)\s+(the\s+|your\s+)?memory(\s+again)?/.test(normalized) ||
17
- /memory\s+(back\s+)?on\b/.test(normalized) ||
18
- /stop ignoring\s+(the\s+|your\s+)?memory/.test(normalized));
19
- }
20
- // Replays every user message in order and returns whether memory is ignored at the end. Used to
21
- // rebuild session state that the coordinator no longer holds (process restart, cache eviction), so
22
- // an "ignore memory" said earlier in the session keeps applying without the user repeating it.
23
- export function deriveIgnoredFromHistory(messages) {
24
- let ignored = false;
25
- for (const message of messages) {
26
- if (roleOf(message) !== "user")
27
- continue;
28
- const query = extractUserQuery(message);
29
- if (detectIgnoreMemory(query))
30
- ignored = true;
31
- else if (ignored && detectResumeMemory(query))
32
- ignored = false;
33
- }
34
- return ignored;
35
- }
36
- export function isAutoMemoryPart(part) {
37
- if (!part || typeof part !== "object")
38
- return false;
39
- const text = part.text;
40
- return typeof text === "string" && text.trimStart().startsWith(AUTO_MEMORY_MARKER);
41
- }
42
- // Drops the plugin's own system segment from system-role messages; messages left without parts
43
- // are removed entirely.
44
- export function stripAutoMemoryParts(messages) {
45
- return messages
46
- .map((message) => {
47
- if (roleOf(message) !== "system" || !Array.isArray(message.parts))
48
- return message;
49
- const parts = message.parts.filter((part) => !isAutoMemoryPart(part));
50
- return parts.length === message.parts.length ? message : { ...message, parts };
51
- })
52
- .filter((message) => !Array.isArray(message.parts) || message.parts.length > 0);
53
- }
File without changes
File without changes