opencode-branch-memory-manager 0.1.3 → 0.1.5

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
  /**
@@ -22,7 +22,10 @@ export class BranchMonitor {
22
22
  newBranch: string,
23
23
  ) => void,
24
24
  private config: PluginConfig,
25
- ) {}
25
+ ) {
26
+ // Register the callback so it gets called on branch changes
27
+ this.changeCallbacks.push(this.onBranchChange);
28
+ }
26
29
 
27
30
  /**
28
31
  * Start monitoring git branch changes
@@ -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: {