opencode-branch-memory-manager 0.1.3 → 0.1.4

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.
@@ -6,9 +6,11 @@ import { GitOperations } from "./git.js";
6
6
  */
7
7
  export class ContextCollector {
8
8
  private config: PluginConfig;
9
+ private client?: any;
9
10
 
10
- constructor(config: PluginConfig) {
11
+ constructor(config: PluginConfig, client?: any) {
11
12
  this.config = config;
13
+ this.client = client;
12
14
  }
13
15
 
14
16
  /**
@@ -73,9 +75,28 @@ export class ContextCollector {
73
75
  * @returns Array of messages
74
76
  */
75
77
  private async collectMessages(): Promise<BranchContext["data"]["messages"]> {
76
- // Placeholder for SDK integration
77
- // When OpenCode SDK is available, this will fetch recent messages
78
- return [];
78
+ if (!this.client) {
79
+ console.warn('Client not available, skipping message collection');
80
+ return [];
81
+ }
82
+
83
+ try {
84
+ // Use OpenCode SDK to fetch session messages
85
+ // Note: The actual API may differ - this is based on common patterns
86
+ const messages = await this.client.session?.getMessages?.() || [];
87
+
88
+ // Limit to maxMessages from config
89
+ const limited = messages.slice(-this.config.context.maxMessages);
90
+
91
+ return limited.map((msg: any) => ({
92
+ role: msg.role || 'user',
93
+ content: msg.content || '',
94
+ timestamp: msg.timestamp || new Date().toISOString()
95
+ }));
96
+ } catch (error) {
97
+ console.error('Failed to collect messages:', error);
98
+ return [];
99
+ }
79
100
  }
80
101
 
81
102
  /**
@@ -83,8 +104,27 @@ export class ContextCollector {
83
104
  * @returns Array of todos
84
105
  */
85
106
  private async collectTodos(): Promise<BranchContext["data"]["todos"]> {
86
- // Placeholder for SDK integration
87
- // When OpenCode SDK is available, this will fetch todo items
88
- return [];
107
+ if (!this.client) {
108
+ console.warn('Client not available, skipping todo collection');
109
+ return [];
110
+ }
111
+
112
+ try {
113
+ // Use OpenCode SDK to fetch session todos
114
+ // Note: The actual API may differ - this is based on common patterns
115
+ const todos = await this.client.session?.getTodos?.() || [];
116
+
117
+ // Limit to maxTodos from config
118
+ const limited = todos.slice(0, this.config.context.maxTodos);
119
+
120
+ return limited.map((todo: any) => ({
121
+ id: todo.id || String(Date.now()),
122
+ content: todo.content || '',
123
+ status: todo.status || 'pending'
124
+ }));
125
+ } catch (error) {
126
+ console.error('Failed to collect todos:', error);
127
+ return [];
128
+ }
89
129
  }
90
130
  }
@@ -11,7 +11,9 @@ const DEFAULT_CONFIG: PluginConfig = {
11
11
  enabled: true,
12
12
  onMessageChange: true,
13
13
  onBranchChange: true,
14
- onToolExecute: true
14
+ onToolExecute: true,
15
+ throttleMs: 5000,
16
+ periodicIntervalMs: 60000
15
17
  },
16
18
  contextLoading: 'auto',
17
19
  context: {
@@ -34,28 +36,28 @@ const DEFAULT_CONFIG: PluginConfig = {
34
36
  * Configuration manager for branch memory plugin
35
37
  */
36
38
  export class ConfigManager {
37
- private static configPath: string
38
- private static projectPath: string
39
-
39
+ private configPath: string
40
+ private projectPath: string
41
+
40
42
  /**
41
- * Set the project path for configuration
43
+ * Create a new ConfigManager instance
42
44
  * @param projectPath - The root directory of the project
43
45
  */
44
- static setProjectPath(projectPath: string): void {
46
+ constructor(projectPath: string) {
45
47
  this.projectPath = projectPath
46
48
  this.configPath = path.join(projectPath, '.opencode', 'config', 'branch-memory.json')
47
49
  }
48
-
50
+
49
51
  /**
50
52
  * Load configuration from project directory, falling back to defaults
51
53
  * @returns Configuration object
52
54
  */
53
- static async load(): Promise<PluginConfig> {
55
+ async load(): Promise<PluginConfig> {
54
56
  if (existsSync(this.configPath)) {
55
57
  try {
56
58
  const content = await fs.readFile(this.configPath, 'utf8')
57
59
  const userConfig = JSON.parse(content) as Partial<PluginConfig>
58
-
60
+
59
61
  // Deep merge user config with defaults
60
62
  return this.deepMerge(DEFAULT_CONFIG, userConfig) as PluginConfig
61
63
  } catch (error) {
@@ -65,31 +67,30 @@ export class ConfigManager {
65
67
  }
66
68
  return { ...DEFAULT_CONFIG }
67
69
  }
68
-
70
+
69
71
  /**
70
72
  * Get default configuration
71
73
  * @returns Default configuration object
72
74
  */
73
- static getDefault(): PluginConfig {
75
+ getDefault(): PluginConfig {
74
76
  return JSON.parse(JSON.stringify(DEFAULT_CONFIG)) as PluginConfig
75
77
  }
76
-
78
+
77
79
  /**
78
80
  * Get storage directory path
79
- * @param projectPath - The root directory of the project
80
81
  * @returns Path to storage directory
81
82
  */
82
- static getStorageDir(projectPath: string): string {
83
- return path.join(projectPath, '.opencode', 'branch-memory')
83
+ getStorageDir(): string {
84
+ return path.join(this.projectPath, '.opencode', 'branch-memory')
84
85
  }
85
-
86
+
86
87
  /**
87
88
  * Save configuration to project directory
88
89
  * @param config - Configuration object to save
89
90
  */
90
- static async save(config: PluginConfig): Promise<void> {
91
+ async save(config: PluginConfig): Promise<void> {
91
92
  const configDir = path.dirname(this.configPath)
92
-
93
+
93
94
  try {
94
95
  await fs.mkdir(configDir, { recursive: true })
95
96
  await fs.writeFile(this.configPath, JSON.stringify(config, null, 2), 'utf8')
@@ -98,20 +99,20 @@ export class ConfigManager {
98
99
  throw error
99
100
  }
100
101
  }
101
-
102
+
102
103
  /**
103
104
  * Deep merge two objects
104
105
  * @param target - Target object (defaults)
105
106
  * @param source - Source object (user config)
106
107
  * @returns Merged object
107
108
  */
108
- private static deepMerge<T>(target: T, source: Partial<T>): T {
109
+ private deepMerge<T>(target: T, source: Partial<T>): T {
109
110
  const result = { ...target }
110
-
111
+
111
112
  for (const key in source) {
112
113
  const sourceValue = source[key]
113
114
  const targetValue = result[key]
114
-
115
+
115
116
  if (sourceValue !== undefined) {
116
117
  if (typeof sourceValue === 'object' && sourceValue !== null && !Array.isArray(sourceValue) &&
117
118
  typeof targetValue === 'object' && targetValue !== null && !Array.isArray(targetValue)) {
@@ -121,7 +122,7 @@ export class ConfigManager {
121
122
  }
122
123
  }
123
124
  }
124
-
125
+
125
126
  return result
126
127
  }
127
128
  }
@@ -95,7 +95,7 @@ export class GitOperations {
95
95
  }
96
96
 
97
97
  static async getAllBranches(): Promise<string[]> {
98
- const result = await this.runGitCommand('branch', "--format='%(refname:short)'")
98
+ const result = await this.runGitCommand('branch', '--format=%(refname:short)')
99
99
 
100
100
  if (result.exitCode !== 0) {
101
101
  return []
@@ -16,15 +16,43 @@ export class ContextInjector {
16
16
  * @param branchContext - Branch context to inject
17
17
  */
18
18
  async injectContext(branchContext: BranchContext): Promise<void> {
19
- const summary = this.formatContextSummary(branchContext)
20
-
21
- // Inject context without triggering AI response
22
- // This would use the OpenCode SDK client.session.prompt() with noReply: true
23
- // For now, log the injection
24
- console.log('\nšŸ“„ Context injected for branch:', branchContext.branch)
25
- console.log('─'.repeat(50))
26
- console.log(summary)
27
- console.log('─'.repeat(50))
19
+ if (!this.context || !this.hasContextData(branchContext)) {
20
+ return;
21
+ }
22
+
23
+ const summary = this.formatContextSummary(branchContext);
24
+
25
+ try {
26
+ // Inject context as a system message that doesn't trigger response
27
+ // Using the OpenCode SDK client to add context to the session
28
+ const client = (this.context as any).client;
29
+
30
+ if (client?.session?.addMessage) {
31
+ await client.session.addMessage({
32
+ role: 'system',
33
+ content: summary,
34
+ silent: true // Don't trigger AI response
35
+ });
36
+ console.log('āœ… Context injected for branch:', branchContext.branch);
37
+ } else if (client?.session?.prompt) {
38
+ // Fallback: use prompt with noReply flag
39
+ await client.session.prompt(summary, { noReply: true });
40
+ console.log('āœ… Context injected for branch:', branchContext.branch);
41
+ } else {
42
+ // Fallback to console output if SDK methods not available
43
+ console.log('\nšŸ“„ Context Summary (SDK not available):');
44
+ console.log('─'.repeat(50));
45
+ console.log(summary);
46
+ console.log('─'.repeat(50));
47
+ }
48
+ } catch (error) {
49
+ console.error('Failed to inject context:', error);
50
+ // Fall back to console output
51
+ console.log('\nšŸ“„ Context Summary (injection failed):');
52
+ console.log('─'.repeat(50));
53
+ console.log(summary);
54
+ console.log('─'.repeat(50));
55
+ }
28
56
  }
29
57
 
30
58
  /**
@@ -1,16 +1,18 @@
1
1
  import * as fs from 'fs/promises'
2
2
  import * as path from 'path'
3
3
  import { existsSync } from 'fs'
4
- import type { BranchContext } from './types.js'
4
+ import type { BranchContext, PluginConfig } from './types.js'
5
5
 
6
6
  /**
7
7
  * Context storage manager for branch-specific contexts
8
8
  */
9
9
  export class ContextStorage {
10
10
  private storageDir: string
11
-
12
- constructor(storageDir: string) {
11
+ private maxBackups: number
12
+
13
+ constructor(storageDir: string, config?: PluginConfig) {
13
14
  this.storageDir = storageDir
15
+ this.maxBackups = config?.storage?.maxBackups ?? 5
14
16
  }
15
17
 
16
18
  /**
@@ -162,7 +164,7 @@ export class ContextStorage {
162
164
  return { name: f, timestamp: match ? parseInt(match[1], 10) : 0 }
163
165
  })
164
166
  .sort((a, b) => b.timestamp - a.timestamp)
165
- .slice(5) // Keep last 5
167
+ .slice(this.maxBackups) // Keep last maxBackups
166
168
 
167
169
  for (const backup of backups) {
168
170
  await fs.unlink(path.join(this.storageDir, backup.name)).catch(() => {})
@@ -49,6 +49,8 @@ export interface PluginConfig {
49
49
  onMessageChange: boolean
50
50
  onBranchChange: boolean
51
51
  onToolExecute: boolean
52
+ throttleMs: number
53
+ periodicIntervalMs: number
52
54
  }
53
55
  contextLoading: 'auto' | 'ask' | 'manual'
54
56
  context: {
@@ -12336,8 +12336,10 @@ import { existsSync } from "fs";
12336
12336
 
12337
12337
  class ContextStorage {
12338
12338
  storageDir;
12339
- constructor(storageDir) {
12339
+ maxBackups;
12340
+ constructor(storageDir, config2) {
12340
12341
  this.storageDir = storageDir;
12342
+ this.maxBackups = config2?.storage?.maxBackups ?? 5;
12341
12343
  }
12342
12344
  getBranchFile(branch) {
12343
12345
  return path.join(this.storageDir, `${this.sanitizeBranchName(branch)}.json`);
@@ -12415,7 +12417,7 @@ class ContextStorage {
12415
12417
  const backups = files.filter((f) => f.startsWith(`${safeBranch}.backup.`) && f.endsWith(".json")).map((f) => {
12416
12418
  const match = f.match(/\.backup\.(\d+)\.json$/);
12417
12419
  return { name: f, timestamp: match ? parseInt(match[1], 10) : 0 };
12418
- }).sort((a, b) => b.timestamp - a.timestamp).slice(5);
12420
+ }).sort((a, b) => b.timestamp - a.timestamp).slice(this.maxBackups);
12419
12421
  for (const backup of backups) {
12420
12422
  await fs.unlink(path.join(this.storageDir, backup.name)).catch(() => {});
12421
12423
  }
@@ -12595,7 +12597,7 @@ class GitOperations {
12595
12597
  `).filter((f) => f.length > 0);
12596
12598
  }
12597
12599
  static async getAllBranches() {
12598
- const result = await this.runGitCommand("branch", "--format='%(refname:short)'");
12600
+ const result = await this.runGitCommand("branch", "--format=%(refname:short)");
12599
12601
  if (result.exitCode !== 0) {
12600
12602
  return [];
12601
12603
  }
@@ -12649,8 +12651,10 @@ class GitOperations {
12649
12651
  // .opencode/branch-memory/collector.ts
12650
12652
  class ContextCollector {
12651
12653
  config;
12652
- constructor(config2) {
12654
+ client;
12655
+ constructor(config2, client) {
12653
12656
  this.config = config2;
12657
+ this.client = client;
12654
12658
  }
12655
12659
  async collectContext(includeMessages = true, includeTodos = true, includeFiles = true, description = "") {
12656
12660
  const currentBranch = await GitOperations.getCurrentBranch();
@@ -12685,10 +12689,40 @@ class ContextCollector {
12685
12689
  return context;
12686
12690
  }
12687
12691
  async collectMessages() {
12688
- return [];
12692
+ if (!this.client) {
12693
+ console.warn("Client not available, skipping message collection");
12694
+ return [];
12695
+ }
12696
+ try {
12697
+ const messages = await this.client.session?.getMessages?.() || [];
12698
+ const limited = messages.slice(-this.config.context.maxMessages);
12699
+ return limited.map((msg) => ({
12700
+ role: msg.role || "user",
12701
+ content: msg.content || "",
12702
+ timestamp: msg.timestamp || new Date().toISOString()
12703
+ }));
12704
+ } catch (error45) {
12705
+ console.error("Failed to collect messages:", error45);
12706
+ return [];
12707
+ }
12689
12708
  }
12690
12709
  async collectTodos() {
12691
- return [];
12710
+ if (!this.client) {
12711
+ console.warn("Client not available, skipping todo collection");
12712
+ return [];
12713
+ }
12714
+ try {
12715
+ const todos = await this.client.session?.getTodos?.() || [];
12716
+ const limited = todos.slice(0, this.config.context.maxTodos);
12717
+ return limited.map((todo) => ({
12718
+ id: todo.id || String(Date.now()),
12719
+ content: todo.content || "",
12720
+ status: todo.status || "pending"
12721
+ }));
12722
+ } catch (error45) {
12723
+ console.error("Failed to collect todos:", error45);
12724
+ return [];
12725
+ }
12692
12726
  }
12693
12727
  }
12694
12728
  // .opencode/branch-memory/injector.ts
@@ -12698,12 +12732,37 @@ class ContextInjector {
12698
12732
  this.context = context;
12699
12733
  }
12700
12734
  async injectContext(branchContext) {
12735
+ if (!this.context || !this.hasContextData(branchContext)) {
12736
+ return;
12737
+ }
12701
12738
  const summary = this.formatContextSummary(branchContext);
12702
- console.log(`
12703
- \uD83D\uDCE5 Context injected for branch:`, branchContext.branch);
12704
- console.log("─".repeat(50));
12705
- console.log(summary);
12706
- console.log("─".repeat(50));
12739
+ try {
12740
+ const client = this.context.client;
12741
+ if (client?.session?.addMessage) {
12742
+ await client.session.addMessage({
12743
+ role: "system",
12744
+ content: summary,
12745
+ silent: true
12746
+ });
12747
+ console.log("āœ… Context injected for branch:", branchContext.branch);
12748
+ } else if (client?.session?.prompt) {
12749
+ await client.session.prompt(summary, { noReply: true });
12750
+ console.log("āœ… Context injected for branch:", branchContext.branch);
12751
+ } else {
12752
+ console.log(`
12753
+ \uD83D\uDCE5 Context Summary (SDK not available):`);
12754
+ console.log("─".repeat(50));
12755
+ console.log(summary);
12756
+ console.log("─".repeat(50));
12757
+ }
12758
+ } catch (error45) {
12759
+ console.error("Failed to inject context:", error45);
12760
+ console.log(`
12761
+ \uD83D\uDCE5 Context Summary (injection failed):`);
12762
+ console.log("─".repeat(50));
12763
+ console.log(summary);
12764
+ console.log("─".repeat(50));
12765
+ }
12707
12766
  }
12708
12767
  async askUserAboutContextLoading(branchContext) {
12709
12768
  const summary = this.formatContextSummary(branchContext);
@@ -12762,7 +12821,9 @@ var DEFAULT_CONFIG = {
12762
12821
  enabled: true,
12763
12822
  onMessageChange: true,
12764
12823
  onBranchChange: true,
12765
- onToolExecute: true
12824
+ onToolExecute: true,
12825
+ throttleMs: 5000,
12826
+ periodicIntervalMs: 60000
12766
12827
  },
12767
12828
  contextLoading: "auto",
12768
12829
  context: {
@@ -12782,13 +12843,13 @@ var DEFAULT_CONFIG = {
12782
12843
  };
12783
12844
 
12784
12845
  class ConfigManager {
12785
- static configPath;
12786
- static projectPath;
12787
- static setProjectPath(projectPath) {
12846
+ configPath;
12847
+ projectPath;
12848
+ constructor(projectPath) {
12788
12849
  this.projectPath = projectPath;
12789
12850
  this.configPath = path2.join(projectPath, ".opencode", "config", "branch-memory.json");
12790
12851
  }
12791
- static async load() {
12852
+ async load() {
12792
12853
  if (existsSync2(this.configPath)) {
12793
12854
  try {
12794
12855
  const content = await fs2.readFile(this.configPath, "utf8");
@@ -12801,13 +12862,13 @@ class ConfigManager {
12801
12862
  }
12802
12863
  return { ...DEFAULT_CONFIG };
12803
12864
  }
12804
- static getDefault() {
12865
+ getDefault() {
12805
12866
  return JSON.parse(JSON.stringify(DEFAULT_CONFIG));
12806
12867
  }
12807
- static getStorageDir(projectPath) {
12808
- return path2.join(projectPath, ".opencode", "branch-memory");
12868
+ getStorageDir() {
12869
+ return path2.join(this.projectPath, ".opencode", "branch-memory");
12809
12870
  }
12810
- static async save(config2) {
12871
+ async save(config2) {
12811
12872
  const configDir = path2.dirname(this.configPath);
12812
12873
  try {
12813
12874
  await fs2.mkdir(configDir, { recursive: true });
@@ -12817,7 +12878,7 @@ class ConfigManager {
12817
12878
  throw error45;
12818
12879
  }
12819
12880
  }
12820
- static deepMerge(target, source) {
12881
+ deepMerge(target, source) {
12821
12882
  const result = { ...target };
12822
12883
  for (const key in source) {
12823
12884
  const sourceValue = source[key];
@@ -12834,6 +12895,12 @@ class ConfigManager {
12834
12895
  }
12835
12896
  }
12836
12897
  // .opencode/tool/branch-memory.ts
12898
+ async function initializeContext() {
12899
+ const configManager = new ConfigManager(process.cwd());
12900
+ const config2 = await configManager.load();
12901
+ const storage = new ContextStorage(configManager.getStorageDir(), config2);
12902
+ return { configManager, config: config2, storage };
12903
+ }
12837
12904
  var save = tool({
12838
12905
  description: "Save current session context for current git branch with optional filters",
12839
12906
  args: {
@@ -12844,15 +12911,13 @@ var save = tool({
12844
12911
  },
12845
12912
  async execute(args, context) {
12846
12913
  try {
12847
- ConfigManager.setProjectPath(process.cwd());
12848
- const config2 = await ConfigManager.load();
12914
+ const { config: config2, storage } = await initializeContext();
12849
12915
  const currentBranch = await GitOperations.getCurrentBranch();
12850
12916
  if (!currentBranch) {
12851
12917
  return "āš ļø Not on a git branch, context not saved";
12852
12918
  }
12853
12919
  const collector = new ContextCollector(config2);
12854
12920
  const branchContext = await collector.collectContext(args.includeMessages ?? config2.context.defaultInclude.includes("messages"), args.includeTodos ?? config2.context.defaultInclude.includes("todos"), args.includeFiles ?? config2.context.defaultInclude.includes("files"), args.description || "");
12855
- const storage = new ContextStorage(ConfigManager.getStorageDir(process.cwd()));
12856
12921
  await storage.saveContext(currentBranch, branchContext);
12857
12922
  return `āœ… Saved context for branch '${currentBranch}'
12858
12923
  ā”œā”€ Messages: ${branchContext.metadata.messageCount}
@@ -12872,13 +12937,12 @@ var load = tool({
12872
12937
  },
12873
12938
  async execute(args, context) {
12874
12939
  try {
12875
- ConfigManager.setProjectPath(process.cwd());
12940
+ const { storage } = await initializeContext();
12876
12941
  const git = GitOperations;
12877
12942
  const targetBranch = args.branch || await git.getCurrentBranch();
12878
12943
  if (!targetBranch) {
12879
12944
  return "āš ļø Not on a git branch";
12880
12945
  }
12881
- const storage = new ContextStorage(ConfigManager.getStorageDir(process.cwd()));
12882
12946
  const branchContext = await storage.loadContext(targetBranch);
12883
12947
  if (!branchContext) {
12884
12948
  return `āš ļø No context found for branch '${targetBranch}'`;
@@ -12901,10 +12965,9 @@ var status = tool({
12901
12965
  args: {},
12902
12966
  async execute(args, context) {
12903
12967
  try {
12904
- ConfigManager.setProjectPath(process.cwd());
12968
+ const { storage } = await initializeContext();
12905
12969
  const git = GitOperations;
12906
12970
  const currentBranch = await git.getCurrentBranch();
12907
- const storage = new ContextStorage(ConfigManager.getStorageDir(process.cwd()));
12908
12971
  const branches = await storage.listBranches();
12909
12972
  let output = `
12910
12973
  \uD83D\uDCCA Branch Memory Status`;
@@ -12972,8 +13035,7 @@ var deleteContext = tool({
12972
13035
  },
12973
13036
  async execute(args, context) {
12974
13037
  try {
12975
- ConfigManager.setProjectPath(process.cwd());
12976
- const storage = new ContextStorage(ConfigManager.getStorageDir(process.cwd()));
13038
+ const { storage } = await initializeContext();
12977
13039
  await storage.deleteContext(args.branch);
12978
13040
  return `āœ… Deleted context for branch '${args.branch}'`;
12979
13041
  } catch (error45) {
@@ -12989,8 +13051,7 @@ var list = tool({
12989
13051
  },
12990
13052
  async execute(args, context) {
12991
13053
  try {
12992
- ConfigManager.setProjectPath(process.cwd());
12993
- const storage = new ContextStorage(ConfigManager.getStorageDir(process.cwd()));
13054
+ const { storage } = await initializeContext();
12994
13055
  const branches = await storage.listBranches();
12995
13056
  if (branches.length === 0) {
12996
13057
  return "No saved contexts found";
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "branch-memory-tools",
3
+ "owner": {
4
+ "name": "Davidcreador",
5
+ "email": "contact@davidcreador.com"
6
+ },
7
+ "metadata": {
8
+ "description": "OpenCode plugins for branch memory management",
9
+ "version": "1.0.0"
10
+ },
11
+ "plugins": [
12
+ {
13
+ "name": "branch-memory-manager",
14
+ "source": ".",
15
+ "description": "Automatically manages branch-specific context with auto-save and auto-load. Never lose your development context when switching git branches.",
16
+ "version": "0.1.3",
17
+ "author": {
18
+ "name": "Davidcreador"
19
+ },
20
+ "license": "MIT",
21
+ "keywords": [
22
+ "branch",
23
+ "context",
24
+ "git",
25
+ "auto-save",
26
+ "memory",
27
+ "workflow"
28
+ ]
29
+ }
30
+ ]
31
+ }
@@ -1,11 +1,11 @@
1
1
  import type { Plugin } from '@opencode-ai/plugin'
2
- import { ContextStorage, GitOperations, ContextCollector, ConfigManager } from '../branch-memory/index.js'
2
+ import { ContextStorage, GitOperations, ContextCollector, ConfigManager, BranchMonitor } from '../branch-memory/index.js'
3
3
 
4
4
  export const BranchMemoryPlugin: Plugin = async ({ project, client, $, directory, worktree }) => {
5
5
  console.log('🧠 Branch Memory Plugin initializing...')
6
6
 
7
7
  // Load configuration
8
- ConfigManager.setProjectPath(directory)
8
+ const configManager = new ConfigManager(directory)
9
9
 
10
10
  // Check if we're in a git repository
11
11
  const isGitRepo = await GitOperations.isGitRepo()
@@ -14,28 +14,28 @@ export const BranchMemoryPlugin: Plugin = async ({ project, client, $, directory
14
14
  return {}
15
15
  }
16
16
 
17
- const storage = new ContextStorage(ConfigManager.getStorageDir(directory))
18
- const collector = new ContextCollector(await ConfigManager.load())
17
+ const config = await configManager.load()
18
+ const storage = new ContextStorage(configManager.getStorageDir(), config)
19
+ const collector = new ContextCollector(config, client)
19
20
 
20
21
  // Track last auto-save time to avoid too frequent saves
21
22
  let lastAutoSave = 0
22
- const AUTO_SAVE_THROTTLE = 5000 // 5 seconds
23
23
 
24
24
  // Auto-save function with throttling
25
25
  const autoSave = async (reason: string) => {
26
- const config = await ConfigManager.load()
27
- if (config.autoSave.enabled) {
26
+ const currentConfig = await configManager.load()
27
+ if (currentConfig.autoSave.enabled) {
28
28
  const now = Date.now()
29
29
 
30
- if (now - lastAutoSave > AUTO_SAVE_THROTTLE) {
30
+ if (now - lastAutoSave > currentConfig.autoSave.throttleMs) {
31
31
  try {
32
32
  const currentBranch = await GitOperations.getCurrentBranch()
33
33
 
34
34
  if (currentBranch) {
35
35
  const context = await collector.collectContext(
36
- config.context.defaultInclude.includes('messages'),
37
- config.context.defaultInclude.includes('todos'),
38
- config.context.defaultInclude.includes('files'),
36
+ currentConfig.context.defaultInclude.includes('messages'),
37
+ currentConfig.context.defaultInclude.includes('todos'),
38
+ currentConfig.context.defaultInclude.includes('files'),
39
39
  reason
40
40
  )
41
41
 
@@ -50,62 +50,53 @@ export const BranchMemoryPlugin: Plugin = async ({ project, client, $, directory
50
50
  }
51
51
  }
52
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
- }
53
+ // Initialize branch monitor with callback
54
+ const branchMonitor = new BranchMonitor(
55
+ async (oldBranch, newBranch) => {
56
+ console.log(`šŸ”„ Branch changed: ${oldBranch || '(none)'} → ${newBranch}`)
57
+
58
+ const currentConfig = await configManager.load()
59
+
60
+ // Auto-save old branch context
61
+ if (oldBranch && currentConfig.autoSave.onBranchChange) {
62
+ const context = await collector.collectContext(
63
+ currentConfig.context.defaultInclude.includes('messages'),
64
+ currentConfig.context.defaultInclude.includes('todos'),
65
+ currentConfig.context.defaultInclude.includes('files'),
66
+ 'branch change'
67
+ )
68
+ await storage.saveContext(oldBranch, context)
69
+ console.log(`šŸ’¾ Saved context for old branch '${oldBranch}'`)
70
+ }
78
71
 
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`)
72
+ // Auto-load new branch context
73
+ if (currentConfig.contextLoading === 'auto') {
74
+ const branchContext = await storage.loadContext(newBranch)
75
+ if (branchContext) {
76
+ console.log(`šŸ“„ Found context for branch '${newBranch}'`)
77
+ console.log(' Use @branch-memory_load to restore it')
78
+ } else {
79
+ console.log(`ā„¹ļø No saved context for branch '${newBranch}'`)
91
80
  }
81
+ } else if (currentConfig.contextLoading === 'ask') {
82
+ console.log(`ā„¹ļø Context available for branch '${newBranch}'`)
83
+ console.log(` Use @branch-memory_load to restore it`)
92
84
  }
93
- } catch (error) {
94
- console.error('Error monitoring branch:', error)
95
- }
96
- }
85
+ },
86
+ config
87
+ )
97
88
 
98
- // Start branch monitoring interval
99
- const branchMonitorInterval = setInterval(monitorBranch, 2000)
89
+ // Start branch monitoring
90
+ await branchMonitor.start()
100
91
 
101
92
  return {
102
93
  // Hook: Auto-load context when session is created
103
94
  'session.created': async (input: any, output: any) => {
104
95
  console.log('šŸš€ Session created - checking for saved context...')
105
- const config = await ConfigManager.load()
96
+ const currentConfig = await configManager.load()
106
97
  const branch = await GitOperations.getCurrentBranch()
107
98
 
108
- if (branch && config.contextLoading === 'auto') {
99
+ if (branch && currentConfig.contextLoading === 'auto') {
109
100
  const branchContext = await storage.loadContext(branch)
110
101
  if (branchContext) {
111
102
  console.log(`šŸ“„ Found context for branch '${branch}'`)
@@ -121,11 +112,11 @@ export const BranchMemoryPlugin: Plugin = async ({ project, client, $, directory
121
112
 
122
113
  // Hook: Auto-save when session is updated (periodic checkpoints)
123
114
  'session.updated': async (input: any, output: any) => {
124
- const config = await ConfigManager.load()
125
- if (config.autoSave.enabled) {
115
+ const currentConfig = await configManager.load()
116
+ if (currentConfig.autoSave.enabled) {
126
117
  const now = Date.now()
127
- // Only auto-save periodically (every 60 seconds)
128
- if (now - lastAutoSave > 60000) {
118
+ // Only auto-save periodically
119
+ if (now - lastAutoSave > currentConfig.autoSave.periodicIntervalMs) {
129
120
  await autoSave('session update')
130
121
  }
131
122
  }
@@ -135,8 +126,8 @@ export const BranchMemoryPlugin: Plugin = async ({ project, client, $, directory
135
126
  unload: () => {
136
127
  console.log('🧠 Branch Memory Plugin shutting down...')
137
128
 
138
- // Clear the branch monitor interval
139
- clearInterval(branchMonitorInterval)
129
+ // Stop branch monitoring
130
+ branchMonitor.stop()
140
131
 
141
132
  // Save one last time before shutdown
142
133
  autoSave('plugin unload').catch((error) => {
@@ -7,6 +7,22 @@ import {
7
7
  ContextInjector,
8
8
  } from "../branch-memory/index.js";
9
9
  import type { ToolContext } from "@opencode-ai/plugin";
10
+ import type { PluginConfig } from "../branch-memory/types.js";
11
+
12
+ /**
13
+ * Shared initialization helper for all tools
14
+ * Reduces code duplication across tool implementations
15
+ */
16
+ async function initializeContext(): Promise<{
17
+ configManager: ConfigManager;
18
+ config: PluginConfig;
19
+ storage: ContextStorage;
20
+ }> {
21
+ const configManager = new ConfigManager(process.cwd());
22
+ const config = await configManager.load();
23
+ const storage = new ContextStorage(configManager.getStorageDir(), config);
24
+ return { configManager, config, storage };
25
+ }
10
26
 
11
27
  /**
12
28
  * Save current session context for current git branch with optional filters
@@ -34,8 +50,7 @@ export const save: ToolDefinition = tool({
34
50
  },
35
51
  async execute(args, context: ToolContext) {
36
52
  try {
37
- ConfigManager.setProjectPath(process.cwd());
38
- const config = await ConfigManager.load();
53
+ const { config, storage } = await initializeContext();
39
54
 
40
55
  const currentBranch = await GitOperations.getCurrentBranch();
41
56
 
@@ -52,9 +67,6 @@ export const save: ToolDefinition = tool({
52
67
  args.description || "",
53
68
  );
54
69
 
55
- const storage = new ContextStorage(
56
- ConfigManager.getStorageDir(process.cwd()),
57
- );
58
70
  await storage.saveContext(currentBranch, branchContext);
59
71
 
60
72
  return `āœ… Saved context for branch '${currentBranch}'
@@ -82,7 +94,7 @@ export const load: ToolDefinition = tool({
82
94
  },
83
95
  async execute(args, context: ToolContext) {
84
96
  try {
85
- ConfigManager.setProjectPath(process.cwd());
97
+ const { storage } = await initializeContext();
86
98
 
87
99
  const git = GitOperations;
88
100
  const targetBranch = args.branch || (await git.getCurrentBranch());
@@ -91,9 +103,6 @@ export const load: ToolDefinition = tool({
91
103
  return "āš ļø Not on a git branch";
92
104
  }
93
105
 
94
- const storage = new ContextStorage(
95
- ConfigManager.getStorageDir(process.cwd()),
96
- );
97
106
  const branchContext = await storage.loadContext(targetBranch);
98
107
 
99
108
  if (!branchContext) {
@@ -123,13 +132,10 @@ export const status: ToolDefinition = tool({
123
132
  args: {},
124
133
  async execute(args, context: ToolContext) {
125
134
  try {
126
- ConfigManager.setProjectPath(process.cwd());
135
+ const { storage } = await initializeContext();
127
136
 
128
137
  const git = GitOperations;
129
138
  const currentBranch = await git.getCurrentBranch();
130
- const storage = new ContextStorage(
131
- ConfigManager.getStorageDir(process.cwd()),
132
- );
133
139
  const branches = await storage.listBranches();
134
140
 
135
141
  let output = "\nšŸ“Š Branch Memory Status";
@@ -187,11 +193,8 @@ export const deleteContext: ToolDefinition = tool({
187
193
  },
188
194
  async execute(args, context: ToolContext) {
189
195
  try {
190
- ConfigManager.setProjectPath(process.cwd());
196
+ const { storage } = await initializeContext();
191
197
 
192
- const storage = new ContextStorage(
193
- ConfigManager.getStorageDir(process.cwd()),
194
- );
195
198
  await storage.deleteContext(args.branch);
196
199
 
197
200
  return `āœ… Deleted context for branch '${args.branch}'`;
@@ -215,11 +218,8 @@ export const list: ToolDefinition = tool({
215
218
  },
216
219
  async execute(args, context: ToolContext) {
217
220
  try {
218
- ConfigManager.setProjectPath(process.cwd());
221
+ const { storage } = await initializeContext();
219
222
 
220
- const storage = new ContextStorage(
221
- ConfigManager.getStorageDir(process.cwd()),
222
- );
223
223
  const branches = await storage.listBranches();
224
224
 
225
225
  if (branches.length === 0) {
package/README.md CHANGED
@@ -18,41 +18,74 @@ Automatically manages branch-specific context for OpenCode so you never lose you
18
18
 
19
19
  ### Installation
20
20
 
21
- #### Option 1: npm Package (Recommended)
21
+ The plugin supports **three installation methods** to fit different workflows:
22
22
 
23
- **macOS/Linux:**
24
- ```bash
25
- bunx install @davecodes/opencode-branch-memory-manager
26
- ```
27
-
28
- **Windows (PowerShell):**
29
- ```powershell
30
- bunx install @davecodes/opencode-branch-memory-manager
31
- ```
23
+ #### Method 1: NPM Package (Recommended for Individual Users)
32
24
 
33
- The plugin and tools will be automatically loaded when you run `opencode`. Add to your `opencode.json`:
25
+ Add the plugin to your `opencode.json`:
34
26
 
35
- ```json
27
+ ```json
36
28
  {
37
29
  "plugin": ["opencode-branch-memory-manager"]
38
30
  }
39
31
  ```
40
32
 
33
+ OpenCode will automatically install the package from npm when you run `opencode`.
34
+
35
+ **Manual installation:**
36
+ ```bash
37
+ bunx install opencode-branch-memory-manager
38
+ ```
39
+
41
40
  **Note:** This will add the plugin without removing any existing plugins you have.
42
41
 
43
- #### Option 2: Local Installation
42
+ #### Method 2: Direct Git Clone (For Development/Testing)
43
+
44
+ Clone the repository directly into your project:
44
45
 
45
- **macOS/Linux:**
46
46
  ```bash
47
- curl -fsSL https://raw.githubusercontent.com/Davidcreador/opencode-branch-memory-manager/main/install.sh | bash
47
+ # Navigate to your project's .opencode directory
48
+ cd .opencode
49
+
50
+ # Clone the plugin
51
+ git clone https://github.com/Davidcreador/opencode-branch-memory-manager.git
52
+
53
+ # Install dependencies and build
54
+ cd opencode-branch-memory-manager
55
+ bun install
56
+ bun run build:local
48
57
  ```
49
58
 
50
- **Windows (PowerShell):**
51
- ```powershell
52
- irm https://raw.githubusercontent.com/Davidcreador/opencode-branch-memory-manager/main/install.ps1 | iex
59
+ The plugin will be automatically loaded when you run `opencode` from your project.
60
+
61
+ #### Method 3: Marketplace (For Teams/Organizations)
62
+
63
+ For team or organizational distribution, you can set up a marketplace:
64
+
65
+ **1. Add the marketplace to your `.opencode/settings.json`:**
66
+ ```json
67
+ {
68
+ "extraKnownMarketplaces": {
69
+ "your-org": {
70
+ "source": {
71
+ "source": "github",
72
+ "repo": "your-org/opencode-plugins"
73
+ }
74
+ }
75
+ }
76
+ }
77
+ ```
78
+
79
+ **2. Enable the plugin:**
80
+ ```json
81
+ {
82
+ "enabledPlugins": {
83
+ "branch-memory-manager@your-org": true
84
+ }
85
+ }
53
86
  ```
54
87
 
55
- The plugin and tools will be automatically loaded when you run `opencode`. No configuration needed.
88
+ The plugin will be installed from your organization's marketplace.
56
89
 
57
90
  ### Getting Started
58
91
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-branch-memory-manager",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "Automatically manages branch-specific context for OpenCode with auto-save, auto-load, and git workflow integration",
5
5
  "author": "Davidcreador",
6
6
  "license": "MIT",
@@ -40,6 +40,17 @@
40
40
  "auto-save",
41
41
  "workspace"
42
42
  ],
43
+ "opencode": {
44
+ "type": "plugin",
45
+ "entrypoint": ".opencode/plugin/branch-memory-plugin.ts",
46
+ "tools": [
47
+ "@branch-memory_save",
48
+ "@branch-memory_load",
49
+ "@branch-memory_status",
50
+ "@branch-memory_list",
51
+ "@branch-memory_deleteContext"
52
+ ]
53
+ },
43
54
  "dependencies": {
44
55
  "chokidar": "^4.0.0",
45
56
  "zod": "^3.22.0"