opencode-branch-memory-manager 0.1.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.
@@ -0,0 +1,82 @@
1
+ {
2
+ "name": "opencode-branch-memory-plugin",
3
+ "version": "1.0.0",
4
+ "lockfileVersion": 3,
5
+ "requires": true,
6
+ "packages": {
7
+ "": {
8
+ "name": "opencode-branch-memory-plugin",
9
+ "version": "1.0.0",
10
+ "dependencies": {
11
+ "chokidar": "^4.0.0",
12
+ "zod": "^3.22.0"
13
+ },
14
+ "devDependencies": {
15
+ "@opencode-ai/plugin": "latest"
16
+ }
17
+ },
18
+ "node_modules/@opencode-ai/plugin": {
19
+ "version": "1.0.220",
20
+ "resolved": "https://registry.npmjs.org/@opencode-ai/plugin/-/plugin-1.0.220.tgz",
21
+ "integrity": "sha512-PlhuyWCjVqAgvM+4HO/syri+JXnypGp0owLkKjF7H285IU8V1geNY2RRSv17hAGp2C0gwwmZNyA63VEUbiJVCQ==",
22
+ "dev": true,
23
+ "dependencies": {
24
+ "@opencode-ai/sdk": "1.0.220",
25
+ "zod": "4.1.8"
26
+ }
27
+ },
28
+ "node_modules/@opencode-ai/plugin/node_modules/zod": {
29
+ "version": "4.1.8",
30
+ "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.8.tgz",
31
+ "integrity": "sha512-5R1P+WwQqmmMIEACyzSvo4JXHY5WiAFHRMg+zBZKgKS+Q1viRa0C1hmUKtHltoIFKtIdki3pRxkmpP74jnNYHQ==",
32
+ "dev": true,
33
+ "license": "MIT",
34
+ "funding": {
35
+ "url": "https://github.com/sponsors/colinhacks"
36
+ }
37
+ },
38
+ "node_modules/@opencode-ai/sdk": {
39
+ "version": "1.0.220",
40
+ "resolved": "https://registry.npmjs.org/@opencode-ai/sdk/-/sdk-1.0.220.tgz",
41
+ "integrity": "sha512-OllJJ+SjTSzbccGAat2uj5Vka56B2bxm+9SkFpFV0BXquaJA21KRdt1TgJOQnnzo6A/Zlwbx+ecmJ+Xai01JZA==",
42
+ "dev": true
43
+ },
44
+ "node_modules/chokidar": {
45
+ "version": "4.0.3",
46
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
47
+ "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
48
+ "license": "MIT",
49
+ "dependencies": {
50
+ "readdirp": "^4.0.1"
51
+ },
52
+ "engines": {
53
+ "node": ">= 14.16.0"
54
+ },
55
+ "funding": {
56
+ "url": "https://paulmillr.com/funding/"
57
+ }
58
+ },
59
+ "node_modules/readdirp": {
60
+ "version": "4.1.2",
61
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
62
+ "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
63
+ "license": "MIT",
64
+ "engines": {
65
+ "node": ">= 14.18.0"
66
+ },
67
+ "funding": {
68
+ "type": "individual",
69
+ "url": "https://paulmillr.com/funding/"
70
+ }
71
+ },
72
+ "node_modules/zod": {
73
+ "version": "3.25.76",
74
+ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
75
+ "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
76
+ "license": "MIT",
77
+ "funding": {
78
+ "url": "https://github.com/sponsors/colinhacks"
79
+ }
80
+ }
81
+ }
82
+ }
@@ -0,0 +1,149 @@
1
+ import type { Plugin } from '@opencode-ai/plugin'
2
+ import { ContextStorage, GitOperations, ContextCollector, ConfigManager } from '../branch-memory/index.js'
3
+
4
+ export const BranchMemoryPlugin: Plugin = async ({ project, client, $, directory, worktree }) => {
5
+ console.log('🧠 Branch Memory Plugin initializing...')
6
+
7
+ // Load configuration
8
+ ConfigManager.setProjectPath(directory)
9
+
10
+ // Check if we're in a git repository
11
+ const isGitRepo = await GitOperations.isGitRepo()
12
+ if (!isGitRepo) {
13
+ console.log('āš ļø Not in a git repository, branch memory disabled')
14
+ return {}
15
+ }
16
+
17
+ const storage = new ContextStorage(ConfigManager.getStorageDir(directory))
18
+ const collector = new ContextCollector(await ConfigManager.load())
19
+
20
+ // Track last auto-save time to avoid too frequent saves
21
+ let lastAutoSave = 0
22
+ const AUTO_SAVE_THROTTLE = 5000 // 5 seconds
23
+
24
+ // Auto-save function with throttling
25
+ const autoSave = async (reason: string) => {
26
+ const config = await ConfigManager.load()
27
+ if (config.autoSave.enabled) {
28
+ const now = Date.now()
29
+
30
+ if (now - lastAutoSave > AUTO_SAVE_THROTTLE) {
31
+ try {
32
+ const currentBranch = await GitOperations.getCurrentBranch()
33
+
34
+ if (currentBranch) {
35
+ const context = await collector.collectContext(
36
+ config.context.defaultInclude.includes('messages'),
37
+ config.context.defaultInclude.includes('todos'),
38
+ config.context.defaultInclude.includes('files'),
39
+ reason
40
+ )
41
+
42
+ await storage.saveContext(currentBranch, context)
43
+ lastAutoSave = now
44
+ console.log(`šŸ’¾ Auto-saved context for branch '${currentBranch}' (${reason})`)
45
+ }
46
+ } catch (error) {
47
+ console.error('Auto-save failed:', error)
48
+ }
49
+ }
50
+ }
51
+ }
52
+
53
+ // Track current branch for monitoring
54
+ let currentBranch: string | null = null
55
+
56
+ // Monitor git branch changes
57
+ const monitorBranch = async () => {
58
+ try {
59
+ const newBranch = await GitOperations.getCurrentBranch()
60
+
61
+ if (newBranch && newBranch !== currentBranch) {
62
+ const oldBranchName = currentBranch
63
+ currentBranch = newBranch
64
+ const config = await ConfigManager.load()
65
+
66
+ console.log(`šŸ”„ Branch changed: ${oldBranchName || '(none)'} → ${newBranch}`)
67
+
68
+ // Auto-save old branch context
69
+ if (oldBranchName && config.autoSave.onBranchChange) {
70
+ await storage.saveContext(oldBranchName, await collector.collectContext(
71
+ config.context.defaultInclude.includes('messages'),
72
+ config.context.defaultInclude.includes('todos'),
73
+ config.context.defaultInclude.includes('files'),
74
+ 'branch change'
75
+ ))
76
+ console.log(`šŸ’¾ Saved context for old branch '${oldBranchName}'`)
77
+ }
78
+
79
+ // Auto-load new branch context
80
+ if (config.contextLoading === 'auto') {
81
+ const branchContext = await storage.loadContext(newBranch)
82
+ if (branchContext) {
83
+ console.log(`šŸ“„ Found context for branch '${newBranch}'`)
84
+ console.log(' Use @branch-memory_load to restore it')
85
+ } else {
86
+ console.log(`ā„¹ļø No saved context for branch '${newBranch}'`)
87
+ }
88
+ } else if (config.contextLoading === 'ask') {
89
+ console.log(`ā„¹ļø Context available for branch '${newBranch}'`)
90
+ console.log(` Use @branch-memory_load to restore it`)
91
+ }
92
+ }
93
+ } catch (error) {
94
+ console.error('Error monitoring branch:', error)
95
+ }
96
+ }
97
+
98
+ // Start branch monitoring interval
99
+ const branchMonitorInterval = setInterval(monitorBranch, 2000)
100
+
101
+ return {
102
+ // Hook: Auto-load context when session is created
103
+ 'session.created': async (input: any, output: any) => {
104
+ console.log('šŸš€ Session created - checking for saved context...')
105
+ const config = await ConfigManager.load()
106
+ const branch = await GitOperations.getCurrentBranch()
107
+
108
+ if (branch && config.contextLoading === 'auto') {
109
+ const branchContext = await storage.loadContext(branch)
110
+ if (branchContext) {
111
+ console.log(`šŸ“„ Found context for branch '${branch}'`)
112
+ console.log(' Use @branch-memory_load to restore it')
113
+ }
114
+ }
115
+ },
116
+
117
+ // Hook: Auto-save before tool execution
118
+ 'tool.execute.before': async (input: any, output: any) => {
119
+ await autoSave('tool execution')
120
+ },
121
+
122
+ // Hook: Auto-save when session is updated (periodic checkpoints)
123
+ 'session.updated': async (input: any, output: any) => {
124
+ const config = await ConfigManager.load()
125
+ if (config.autoSave.enabled) {
126
+ const now = Date.now()
127
+ // Only auto-save periodically (every 60 seconds)
128
+ if (now - lastAutoSave > 60000) {
129
+ await autoSave('session update')
130
+ }
131
+ }
132
+ },
133
+
134
+ // Hook: Cleanup on plugin unload
135
+ unload: () => {
136
+ console.log('🧠 Branch Memory Plugin shutting down...')
137
+
138
+ // Clear the branch monitor interval
139
+ clearInterval(branchMonitorInterval)
140
+
141
+ // Save one last time before shutdown
142
+ autoSave('plugin unload').catch((error) => {
143
+ console.error('Final save failed:', error)
144
+ })
145
+
146
+ console.log('āœ… Plugin stopped')
147
+ },
148
+ }
149
+ }
@@ -0,0 +1,254 @@
1
+ import { tool, type ToolDefinition } from "@opencode-ai/plugin";
2
+ import {
3
+ ContextStorage,
4
+ GitOperations,
5
+ ContextCollector,
6
+ ConfigManager,
7
+ ContextInjector,
8
+ } from "../branch-memory/index.js";
9
+ import type { ToolContext } from "@opencode-ai/plugin";
10
+
11
+ /**
12
+ * Save current session context for current git branch with optional filters
13
+ */
14
+ export const save: ToolDefinition = tool({
15
+ description:
16
+ "Save current session context for current git branch with optional filters",
17
+ args: {
18
+ includeMessages: tool.schema
19
+ .boolean()
20
+ .optional()
21
+ .describe("Include conversation messages"),
22
+ includeTodos: tool.schema
23
+ .boolean()
24
+ .optional()
25
+ .describe("Include todo items"),
26
+ includeFiles: tool.schema
27
+ .boolean()
28
+ .optional()
29
+ .describe("Include file references"),
30
+ description: tool.schema
31
+ .string()
32
+ .optional()
33
+ .describe("Description of what you are saving"),
34
+ },
35
+ async execute(args, context: ToolContext) {
36
+ try {
37
+ ConfigManager.setProjectPath(process.cwd());
38
+ const config = await ConfigManager.load();
39
+
40
+ const currentBranch = await GitOperations.getCurrentBranch();
41
+
42
+ if (!currentBranch) {
43
+ return "āš ļø Not on a git branch, context not saved";
44
+ }
45
+
46
+ const collector = new ContextCollector(config);
47
+ const branchContext = await collector.collectContext(
48
+ args.includeMessages ??
49
+ config.context.defaultInclude.includes("messages"),
50
+ args.includeTodos ?? config.context.defaultInclude.includes("todos"),
51
+ args.includeFiles ?? config.context.defaultInclude.includes("files"),
52
+ args.description || "",
53
+ );
54
+
55
+ const storage = new ContextStorage(
56
+ ConfigManager.getStorageDir(process.cwd()),
57
+ );
58
+ await storage.saveContext(currentBranch, branchContext);
59
+
60
+ return `āœ… Saved context for branch '${currentBranch}'
61
+ ā”œā”€ Messages: ${branchContext.metadata.messageCount}
62
+ ā”œā”€ Todos: ${branchContext.metadata.todoCount}
63
+ ā”œā”€ Files: ${branchContext.metadata.fileCount}
64
+ └─ Size: ${(branchContext.metadata.size / 1024).toFixed(1)}KB`;
65
+ } catch (error) {
66
+ console.error("Error saving context:", error);
67
+ return `āŒ Failed to save context: ${error instanceof Error ? error.message : "Unknown error"}`;
68
+ }
69
+ },
70
+ });
71
+
72
+ /**
73
+ * Load branch-specific context into current session
74
+ */
75
+ export const load: ToolDefinition = tool({
76
+ description: "Load branch-specific context into current session",
77
+ args: {
78
+ branch: tool.schema
79
+ .string()
80
+ .optional()
81
+ .describe("Branch name (default: current branch)"),
82
+ },
83
+ async execute(args, context: ToolContext) {
84
+ try {
85
+ ConfigManager.setProjectPath(process.cwd());
86
+
87
+ const git = GitOperations;
88
+ const targetBranch = args.branch || (await git.getCurrentBranch());
89
+
90
+ if (!targetBranch) {
91
+ return "āš ļø Not on a git branch";
92
+ }
93
+
94
+ const storage = new ContextStorage(
95
+ ConfigManager.getStorageDir(process.cwd()),
96
+ );
97
+ const branchContext = await storage.loadContext(targetBranch);
98
+
99
+ if (!branchContext) {
100
+ return `āš ļø No context found for branch '${targetBranch}'`;
101
+ }
102
+
103
+ const injector = new ContextInjector(context);
104
+ await injector.injectContext(branchContext);
105
+
106
+ return `āœ… Loaded context for branch '${targetBranch}'
107
+ ā”œā”€ Saved: ${branchContext.savedAt.substring(0, 10)}...
108
+ ā”œā”€ Messages: ${branchContext.metadata.messageCount}
109
+ ā”œā”€ Todos: ${branchContext.metadata.todoCount}
110
+ └─ Files: ${branchContext.metadata.fileCount}`;
111
+ } catch (error) {
112
+ console.error("Error loading context:", error);
113
+ return `āŒ Failed to load context: ${error instanceof Error ? error.message : "Unknown error"}`;
114
+ }
115
+ },
116
+ });
117
+
118
+ /**
119
+ * Show branch memory status and available contexts
120
+ */
121
+ export const status: ToolDefinition = tool({
122
+ description: "Show branch memory status and available contexts",
123
+ args: {},
124
+ async execute(args, context: ToolContext) {
125
+ try {
126
+ ConfigManager.setProjectPath(process.cwd());
127
+
128
+ const git = GitOperations;
129
+ const currentBranch = await git.getCurrentBranch();
130
+ const storage = new ContextStorage(
131
+ ConfigManager.getStorageDir(process.cwd()),
132
+ );
133
+ const branches = await storage.listBranches();
134
+
135
+ let output = "\nšŸ“Š Branch Memory Status";
136
+ output += "\n═════════════════════\n";
137
+
138
+ if (currentBranch) {
139
+ output += `Current branch: ${currentBranch}\n\n`;
140
+
141
+ const branchContext = await storage.loadContext(currentBranch);
142
+ if (branchContext) {
143
+ output += `Current context:\n`;
144
+ output += ` šŸ“ Messages: ${branchContext.metadata.messageCount}\n`;
145
+ output += ` āœ… Todos: ${branchContext.metadata.todoCount}\n`;
146
+ output += ` šŸ“ Files: ${branchContext.metadata.fileCount}\n`;
147
+ output += ` šŸ’¾ Size: ${(branchContext.metadata.size / 1024).toFixed(1)}KB\n`;
148
+ output += ` ā° Saved: ${branchContext.savedAt}\n`;
149
+ if (branchContext.data.description) {
150
+ output += ` šŸ“„ Description: ${branchContext.data.description}\n`;
151
+ }
152
+ } else {
153
+ output += `Current branch has no saved context\n`;
154
+ }
155
+ } else {
156
+ output += `Not in a git repository\n`;
157
+ }
158
+
159
+ if (branches.length > 0) {
160
+ output += "\nAvailable contexts:\n";
161
+ for (const branch of branches) {
162
+ const meta = await storage.getMetadata(branch);
163
+ const marker = branch === currentBranch ? "→ " : " ";
164
+ output += `${marker}${branch} (${meta.size}, ${meta.modified.substring(0, 10)}...)\n`;
165
+ }
166
+ } else {
167
+ output += "\nNo saved contexts found\n";
168
+ }
169
+
170
+ output += "═════════════════════\n";
171
+
172
+ return output;
173
+ } catch (error) {
174
+ console.error("Error getting status:", error);
175
+ return `āŒ Failed to get status: ${error instanceof Error ? error.message : "Unknown error"}`;
176
+ }
177
+ },
178
+ });
179
+
180
+ /**
181
+ * Delete saved context for a branch
182
+ */
183
+ export const deleteContext: ToolDefinition = tool({
184
+ description: "Delete saved context for a branch",
185
+ args: {
186
+ branch: tool.schema.string().describe("Branch name to delete context for"),
187
+ },
188
+ async execute(args, context: ToolContext) {
189
+ try {
190
+ ConfigManager.setProjectPath(process.cwd());
191
+
192
+ const storage = new ContextStorage(
193
+ ConfigManager.getStorageDir(process.cwd()),
194
+ );
195
+ await storage.deleteContext(args.branch);
196
+
197
+ return `āœ… Deleted context for branch '${args.branch}'`;
198
+ } catch (error) {
199
+ console.error("Error deleting context:", error);
200
+ return `āŒ Failed to delete context: ${error instanceof Error ? error.message : "Unknown error"}`;
201
+ }
202
+ },
203
+ });
204
+
205
+ /**
206
+ * List all branches with saved contexts
207
+ */
208
+ export const list: ToolDefinition = tool({
209
+ description: "List all branches with saved contexts",
210
+ args: {
211
+ verbose: tool.schema
212
+ .boolean()
213
+ .optional()
214
+ .describe("Show detailed information"),
215
+ },
216
+ async execute(args, context: ToolContext) {
217
+ try {
218
+ ConfigManager.setProjectPath(process.cwd());
219
+
220
+ const storage = new ContextStorage(
221
+ ConfigManager.getStorageDir(process.cwd()),
222
+ );
223
+ const branches = await storage.listBranches();
224
+
225
+ if (branches.length === 0) {
226
+ return "No saved contexts found";
227
+ }
228
+
229
+ let output = "\nšŸ“‹ Branches with saved contexts\n";
230
+ output += "═════════════════════\n";
231
+
232
+ for (const branch of branches) {
233
+ const meta = await storage.getMetadata(branch);
234
+ output += `\n${branch}\n`;
235
+ output += ` šŸ’¾ Size: ${meta.size}\n`;
236
+ output += ` ā° Modified: ${meta.modified}\n`;
237
+
238
+ if (args.verbose) {
239
+ output += ` šŸ“ Messages: ${meta.messageCount}\n`;
240
+ output += ` āœ… Todos: ${meta.todoCount}\n`;
241
+ output += ` šŸ“ Files: ${meta.fileCount}\n`;
242
+ }
243
+ }
244
+
245
+ output += "\n═════════════════════\n";
246
+ output += `\nTotal: ${branches.length} branch(es)\n`;
247
+
248
+ return output;
249
+ } catch (error) {
250
+ console.error("Error listing contexts:", error);
251
+ return `āŒ Failed to list contexts: ${error instanceof Error ? error.message : "Unknown error"}`;
252
+ }
253
+ },
254
+ });
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 OpenCode Branch Memory Manager Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the
10
+ Software is furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.