claude-autopm 1.30.0 → 1.31.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,316 @@
1
+ /**
2
+ * Conflict History Manager
3
+ *
4
+ * Logs conflict resolutions with timestamps and provides undo/replay functionality.
5
+ * Supports both in-memory and file-based storage.
6
+ *
7
+ * @example
8
+ * const ConflictHistory = require('./lib/conflict-history');
9
+ *
10
+ * const history = new ConflictHistory({
11
+ * storage: 'memory'
12
+ * });
13
+ *
14
+ * // Log a conflict resolution
15
+ * const logId = history.log(conflict, resolution);
16
+ *
17
+ * // Retrieve history with filters
18
+ * const recentConflicts = history.getHistory({ strategy: 'newest' });
19
+ *
20
+ * // Undo a resolution
21
+ * const undone = history.undo(logId);
22
+ *
23
+ * // Replay with different strategy
24
+ * const replayed = history.replay(logId, 'local');
25
+ */
26
+
27
+ const crypto = require('crypto');
28
+ const fs = require('fs');
29
+ const path = require('path');
30
+
31
+ class ConflictHistory {
32
+ /**
33
+ * Create a new ConflictHistory instance
34
+ *
35
+ * @param {Object} options - Configuration options
36
+ * @param {string} options.storage - Storage type ('memory' or 'file') (default: 'memory')
37
+ * @param {string} options.storagePath - Path to storage file (default: '.claude/.conflict-history.json')
38
+ */
39
+ constructor(options = {}) {
40
+ // Validate and sanitize storagePath to prevent path traversal
41
+ let storagePath = options.storagePath || '.claude/.conflict-history.json';
42
+
43
+ // Resolve path and check for path traversal attempts using a trusted base directory
44
+ const baseDir = process.cwd();
45
+ const resolvedPath = path.resolve(baseDir, storagePath);
46
+ if (!resolvedPath.startsWith(baseDir + path.sep)) {
47
+ throw new Error('storagePath must be within the current working directory (security: path traversal prevention)');
48
+ }
49
+
50
+ this.options = {
51
+ storage: options.storage || 'memory',
52
+ storagePath: resolvedPath
53
+ };
54
+
55
+ // In-memory storage
56
+ this.conflicts = new Map();
57
+
58
+ // Load from file if using file storage
59
+ if (this.options.storage === 'file') {
60
+ this._loadFromFile();
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Log a conflict resolution
66
+ *
67
+ * @param {Object} conflict - Conflict object
68
+ * @param {Object} resolution - Resolution object
69
+ * @param {string} resolution.strategy - Strategy used
70
+ * @param {string} resolution.chosenContent - Resolved content
71
+ * @param {Date} resolution.timestamp - Resolution timestamp (optional)
72
+ * @returns {string} Unique log ID
73
+ */
74
+ log(conflict, resolution) {
75
+ // Generate unique ID
76
+ const logId = this._generateId();
77
+
78
+ // Add timestamp if not provided
79
+ const timestamp = resolution.timestamp || new Date();
80
+
81
+ // Store conflict and resolution
82
+ const entry = {
83
+ id: logId,
84
+ conflict: { ...conflict },
85
+ resolution: {
86
+ ...resolution,
87
+ timestamp
88
+ },
89
+ createdAt: new Date()
90
+ };
91
+
92
+ this.conflicts.set(logId, entry);
93
+
94
+ // Persist to file if using file storage
95
+ if (this.options.storage === 'file') {
96
+ this._saveToFile();
97
+ }
98
+
99
+ return logId;
100
+ }
101
+
102
+ /**
103
+ * Retrieve conflict history with optional filters
104
+ *
105
+ * @param {Object} filters - Filter criteria
106
+ * @param {string} filters.strategy - Filter by resolution strategy
107
+ * @param {string} filters.filePath - Filter by file path
108
+ * @param {Date} filters.after - Filter by date (after)
109
+ * @param {Date} filters.before - Filter by date (before)
110
+ * @returns {Array<Object>} Filtered conflict history entries
111
+ */
112
+ getHistory(filters = {}) {
113
+ let results = Array.from(this.conflicts.values());
114
+
115
+ // Apply filters
116
+ if (filters.strategy) {
117
+ results = results.filter(entry =>
118
+ entry.resolution.strategy === filters.strategy
119
+ );
120
+ }
121
+
122
+ if (filters.filePath) {
123
+ results = results.filter(entry =>
124
+ entry.conflict.filePath === filters.filePath
125
+ );
126
+ }
127
+
128
+ if (filters.after) {
129
+ results = results.filter(entry =>
130
+ entry.createdAt >= filters.after
131
+ );
132
+ }
133
+
134
+ if (filters.before) {
135
+ results = results.filter(entry =>
136
+ entry.createdAt <= filters.before
137
+ );
138
+ }
139
+
140
+ // Sort by creation date (newest first)
141
+ results.sort((a, b) => b.createdAt - a.createdAt);
142
+
143
+ return results;
144
+ }
145
+
146
+ /**
147
+ * Undo a conflict resolution
148
+ *
149
+ * Retrieves the original conflict state before resolution.
150
+ *
151
+ * @param {string} logId - Log ID to undo
152
+ * @returns {Object} Original conflict and resolution
153
+ */
154
+ undo(logId) {
155
+ const entry = this.conflicts.get(logId);
156
+
157
+ if (!entry) {
158
+ throw new Error(`Conflict log not found: ${logId}`);
159
+ }
160
+
161
+ return {
162
+ conflict: entry.conflict,
163
+ resolution: entry.resolution
164
+ };
165
+ }
166
+
167
+ /**
168
+ * Replay a conflict resolution with a different strategy
169
+ *
170
+ * @param {string} logId - Log ID to replay
171
+ * @param {string} newStrategy - New resolution strategy
172
+ * @returns {Object} Replayed resolution result
173
+ */
174
+ replay(logId, newStrategy) {
175
+ const entry = this.conflicts.get(logId);
176
+
177
+ if (!entry) {
178
+ throw new Error(`Conflict log not found: ${logId}`);
179
+ }
180
+
181
+ // Determine new content based on strategy
182
+ let newContent;
183
+ switch (newStrategy) {
184
+ case 'local':
185
+ newContent = entry.conflict.localContent;
186
+ break;
187
+
188
+ case 'remote':
189
+ newContent = entry.conflict.remoteContent;
190
+ break;
191
+
192
+ case 'newest':
193
+ // Use timestamp comparison
194
+ if (entry.conflict.localTimestamp && entry.conflict.remoteTimestamp) {
195
+ const localTime = new Date(entry.conflict.localTimestamp).getTime();
196
+ const remoteTime = new Date(entry.conflict.remoteTimestamp).getTime();
197
+ newContent = remoteTime > localTime ?
198
+ entry.conflict.remoteContent :
199
+ entry.conflict.localContent;
200
+ } else {
201
+ newContent = entry.conflict.localContent;
202
+ }
203
+ break;
204
+
205
+ default:
206
+ throw new Error(`Unknown strategy: ${newStrategy}`);
207
+ }
208
+
209
+ return {
210
+ originalResolution: entry.resolution,
211
+ newResolution: {
212
+ strategy: newStrategy,
213
+ chosenContent: newContent,
214
+ timestamp: new Date()
215
+ }
216
+ };
217
+ }
218
+
219
+ /**
220
+ * Clear all conflict history
221
+ */
222
+ clear() {
223
+ this.conflicts.clear();
224
+
225
+ if (this.options.storage === 'file') {
226
+ this._saveToFile();
227
+ }
228
+ }
229
+
230
+ /**
231
+ * Get total number of logged conflicts
232
+ *
233
+ * @returns {number} Total conflicts
234
+ */
235
+ count() {
236
+ return this.conflicts.size;
237
+ }
238
+
239
+ /**
240
+ * Generate unique ID for conflict log
241
+ *
242
+ * @private
243
+ * @returns {string} Unique ID
244
+ */
245
+ _generateId() {
246
+ return crypto.randomBytes(16).toString('hex');
247
+ }
248
+
249
+ /**
250
+ * Load conflict history from file
251
+ *
252
+ * @private
253
+ */
254
+ _loadFromFile() {
255
+ try {
256
+ if (fs.existsSync(this.options.storagePath)) {
257
+ const data = fs.readFileSync(this.options.storagePath, 'utf8');
258
+
259
+ // Validate non-empty file
260
+ if (!data.trim()) {
261
+ this.conflicts.clear();
262
+ return;
263
+ }
264
+
265
+ const parsed = JSON.parse(data);
266
+
267
+ // Validate structure
268
+ if (!Array.isArray(parsed)) {
269
+ throw new Error('Invalid conflict history format: expected array');
270
+ }
271
+
272
+ this.conflicts.clear();
273
+ for (const entry of parsed) {
274
+ // Validate entry structure
275
+ if (!entry.id || !entry.conflict || !entry.resolution) {
276
+ console.warn(`Skipping invalid entry: ${JSON.stringify(entry).substring(0, 100)}`);
277
+ continue;
278
+ }
279
+
280
+ // Restore Date objects
281
+ entry.createdAt = new Date(entry.createdAt);
282
+ entry.resolution.timestamp = new Date(entry.resolution.timestamp);
283
+
284
+ this.conflicts.set(entry.id, entry);
285
+ }
286
+ }
287
+ } catch (error) {
288
+ // Re-throw with context for better debugging
289
+ throw new Error(`Failed to load conflict history from ${this.options.storagePath}: ${error.message}`);
290
+ }
291
+ }
292
+
293
+ /**
294
+ * Save conflict history to file
295
+ *
296
+ * @private
297
+ */
298
+ _saveToFile() {
299
+ try {
300
+ // Ensure directory exists
301
+ const dir = path.dirname(this.options.storagePath);
302
+ if (!fs.existsSync(dir)) {
303
+ fs.mkdirSync(dir, { recursive: true });
304
+ }
305
+
306
+ // Convert Map to Array for JSON serialization
307
+ const data = Array.from(this.conflicts.values());
308
+
309
+ fs.writeFileSync(this.options.storagePath, JSON.stringify(data, null, 2), 'utf8');
310
+ } catch (error) {
311
+ console.error('Failed to save conflict history:', error.message);
312
+ }
313
+ }
314
+ }
315
+
316
+ module.exports = ConflictHistory;
@@ -0,0 +1,330 @@
1
+ /**
2
+ * Conflict Resolver for GitHub Sync Operations
3
+ *
4
+ * Handles three-way merge conflicts when syncing markdown files between
5
+ * local filesystem and GitHub. Supports multiple resolution strategies:
6
+ * - newest: Keep version with newest timestamp
7
+ * - local: Always prefer local version
8
+ * - remote: Always prefer remote version
9
+ * - rules-based: Apply custom resolution rules
10
+ * - interactive: Prompt user for resolution
11
+ *
12
+ * @example
13
+ * const ConflictResolver = require('./lib/conflict-resolver');
14
+ *
15
+ * const resolver = new ConflictResolver({
16
+ * strategy: 'newest',
17
+ * contextLines: 3
18
+ * });
19
+ *
20
+ * const result = resolver.threeWayMerge(localContent, remoteContent, baseContent);
21
+ *
22
+ * if (result.hasConflicts) {
23
+ * for (const conflict of result.conflicts) {
24
+ * const resolved = resolver.resolveConflict(conflict, 'newest');
25
+ * }
26
+ * }
27
+ */
28
+
29
+ class ConflictResolver {
30
+ /**
31
+ * Create a new ConflictResolver instance
32
+ *
33
+ * @param {Object} options - Configuration options
34
+ * @param {string} options.strategy - Default resolution strategy (default: 'manual')
35
+ * @param {number} options.contextLines - Number of context lines around conflicts (default: 3)
36
+ * @param {string} options.markerPrefix - Prefix for conflict markers (default: 'LOCAL/REMOTE')
37
+ * @param {boolean} options.normalizeLineEndings - Normalize line endings during merge (default: true)
38
+ */
39
+ constructor(options = {}) {
40
+ // Validate configuration
41
+ if (options.contextLines !== undefined) {
42
+ if (typeof options.contextLines !== 'number' || options.contextLines < 0) {
43
+ throw new Error('contextLines must be a non-negative number');
44
+ }
45
+ }
46
+
47
+ this.options = {
48
+ strategy: options.strategy || 'manual',
49
+ contextLines: options.contextLines !== undefined ? options.contextLines : 3,
50
+ markerPrefix: options.markerPrefix || 'LOCAL',
51
+ normalizeLineEndings: options.normalizeLineEndings !== false
52
+ };
53
+ }
54
+
55
+ /**
56
+ * Perform three-way merge on text content
57
+ *
58
+ * Compares local, remote, and base versions to detect and merge changes.
59
+ * Uses a line-by-line diff algorithm to identify conflicts.
60
+ *
61
+ * IMPORTANT LIMITATIONS:
62
+ * - This is a simplified line-based merge that does NOT detect:
63
+ * • Moved code blocks (treats as delete + add = conflict)
64
+ * • Reordered functions (multiple conflicts)
65
+ * • Semantic equivalence (different formatting, same logic)
66
+ * - For complex refactoring, use manual conflict resolution strategy
67
+ * - Does not use Longest Common Subsequence (LCS) algorithm
68
+ * - No semantic/AST-based merging for code
69
+ *
70
+ * PERFORMANCE NOTES:
71
+ * - Files >1MB may cause memory pressure (splits into line arrays)
72
+ * - For very large files, consider splitting into sections
73
+ * - Tested up to 1MB files (~1000 lines)
74
+ *
75
+ * @param {string} local - Local version content
76
+ * @param {string} remote - Remote version content
77
+ * @param {string} base - Base version content (last synced)
78
+ * @returns {Object} Merge result with conflicts and merged content
79
+ * @returns {string} result.merged - Merged content with conflict markers
80
+ * @returns {Array} result.conflicts - Array of detected conflicts
81
+ * @returns {boolean} result.hasConflicts - True if conflicts were detected
82
+ */
83
+ threeWayMerge(local, remote, base) {
84
+ // Validate inputs
85
+ if (local === null || local === undefined ||
86
+ remote === null || remote === undefined ||
87
+ base === null || base === undefined) {
88
+ throw new Error('All parameters (local, remote, base) are required and cannot be null or undefined');
89
+ }
90
+
91
+ // Normalize line endings if enabled
92
+ if (this.options.normalizeLineEndings) {
93
+ local = this._normalizeLineEndings(local);
94
+ remote = this._normalizeLineEndings(remote);
95
+ base = this._normalizeLineEndings(base);
96
+ }
97
+
98
+ // Split into lines for comparison
99
+ const localLines = local.split('\n');
100
+ const remoteLines = remote.split('\n');
101
+ const baseLines = base.split('\n');
102
+
103
+ // Perform three-way merge
104
+ const mergeResult = this._performMerge(localLines, remoteLines, baseLines);
105
+
106
+ return {
107
+ merged: mergeResult.merged.join('\n'),
108
+ conflicts: mergeResult.conflicts,
109
+ hasConflicts: mergeResult.conflicts.length > 0
110
+ };
111
+ }
112
+
113
+ /**
114
+ * Perform the actual merge algorithm
115
+ *
116
+ * Uses a simplified three-way merge that handles most common cases:
117
+ * - Non-overlapping changes are merged
118
+ * - Identical changes are merged
119
+ * - Conflicting changes are marked
120
+ *
121
+ * @private
122
+ * @param {Array<string>} localLines - Local lines
123
+ * @param {Array<string>} remoteLines - Remote lines
124
+ * @param {Array<string>} baseLines - Base lines
125
+ * @returns {Object} Merge result
126
+ */
127
+ _performMerge(localLines, remoteLines, baseLines) {
128
+ const merged = [];
129
+ const conflicts = [];
130
+
131
+ const maxLen = Math.max(localLines.length, remoteLines.length, baseLines.length);
132
+
133
+ for (let i = 0; i < maxLen; i++) {
134
+ const baseLine = baseLines[i];
135
+ const localLine = localLines[i];
136
+ const remoteLine = remoteLines[i];
137
+
138
+ // Case 1: All three are identical (or all undefined)
139
+ if (baseLine === localLine && baseLine === remoteLine) {
140
+ if (baseLine !== undefined) {
141
+ merged.push(baseLine);
142
+ }
143
+ continue;
144
+ }
145
+
146
+ // Case 2: Base and local match, remote different - accept remote change
147
+ if (baseLine === localLine && remoteLine !== baseLine) {
148
+ if (remoteLine !== undefined) {
149
+ merged.push(remoteLine);
150
+ }
151
+ continue;
152
+ }
153
+
154
+ // Case 3: Base and remote match, local different - accept local change
155
+ if (baseLine === remoteLine && localLine !== baseLine) {
156
+ if (localLine !== undefined) {
157
+ merged.push(localLine);
158
+ }
159
+ continue;
160
+ }
161
+
162
+ // Case 4: Local and remote match (but differ from base) - accept the change
163
+ if (localLine === remoteLine) {
164
+ if (localLine !== undefined) {
165
+ merged.push(localLine);
166
+ }
167
+ continue;
168
+ }
169
+
170
+ // Case 5: All three differ - CONFLICT
171
+ conflicts.push({
172
+ line: merged.length + 1,
173
+ localContent: localLine || '',
174
+ remoteContent: remoteLine || '',
175
+ baseContent: baseLine || '',
176
+ section: this._detectSection(i, localLines)
177
+ });
178
+
179
+ merged.push('<<<<<<< LOCAL');
180
+ merged.push(localLine || '');
181
+ merged.push('=======');
182
+ merged.push(remoteLine || '');
183
+ merged.push('>>>>>>> REMOTE');
184
+ }
185
+
186
+ return { merged, conflicts };
187
+ }
188
+
189
+ /**
190
+ * Detect which section of the file a line belongs to
191
+ *
192
+ * @private
193
+ * @param {number} lineIndex - Line index
194
+ * @param {Array<string>} lines - File lines
195
+ * @returns {string} Section name ('frontmatter', 'body', etc.)
196
+ */
197
+ _detectSection(lineIndex, lines) {
198
+ // Detect frontmatter (YAML between --- markers)
199
+ if (lineIndex < 10) {
200
+ const firstLines = lines.slice(0, Math.min(10, lines.length));
201
+ if (firstLines[0] === '---') {
202
+ const endIndex = firstLines.findIndex((line, idx) => idx > 0 && line === '---');
203
+ if (endIndex > 0 && lineIndex <= endIndex) {
204
+ return 'frontmatter';
205
+ }
206
+ }
207
+ }
208
+
209
+ return 'body';
210
+ }
211
+
212
+ /**
213
+ * Normalize line endings to LF
214
+ *
215
+ * @private
216
+ * @param {string} text - Text to normalize
217
+ * @returns {string} Normalized text
218
+ */
219
+ _normalizeLineEndings(text) {
220
+ return text.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
221
+ }
222
+
223
+ /**
224
+ * Resolve a conflict using the specified strategy
225
+ *
226
+ * @param {Object} conflict - Conflict object from threeWayMerge
227
+ * @param {string} strategy - Resolution strategy ('newest', 'local', 'remote', 'rules-based')
228
+ * @param {Object} rules - Optional rules for rules-based strategy
229
+ * @returns {string} Resolved content
230
+ */
231
+ resolveConflict(conflict, strategy, rules = {}) {
232
+ switch (strategy) {
233
+ case 'newest':
234
+ return this._resolveNewest(conflict);
235
+
236
+ case 'local':
237
+ return conflict.localContent;
238
+
239
+ case 'remote':
240
+ return conflict.remoteContent;
241
+
242
+ case 'rules-based':
243
+ return this._resolveRulesBased(conflict, rules);
244
+
245
+ case 'manual':
246
+ // Return conflict markers for manual resolution
247
+ return `<<<<<<< LOCAL\n${conflict.localContent}\n=======\n${conflict.remoteContent}\n>>>>>>> REMOTE`;
248
+
249
+ default:
250
+ throw new Error(`Unknown resolution strategy: ${strategy}`);
251
+ }
252
+ }
253
+
254
+ /**
255
+ * Resolve conflict using newest timestamp
256
+ *
257
+ * @private
258
+ * @param {Object} conflict - Conflict with timestamp metadata
259
+ * @returns {string} Content from newest version
260
+ */
261
+ _resolveNewest(conflict) {
262
+ if (!conflict.localTimestamp || !conflict.remoteTimestamp) {
263
+ // If no timestamps, prefer local by default
264
+ return conflict.localContent;
265
+ }
266
+
267
+ const localTime = new Date(conflict.localTimestamp).getTime();
268
+ const remoteTime = new Date(conflict.remoteTimestamp).getTime();
269
+
270
+ // Validate timestamps (Invalid Date returns NaN)
271
+ if (isNaN(localTime) || isNaN(remoteTime)) {
272
+ throw new Error(
273
+ `Invalid timestamps detected in conflict resolution. ` +
274
+ `localTimestamp: "${conflict.localTimestamp}", remoteTimestamp: "${conflict.remoteTimestamp}". ` +
275
+ `Expected format: a valid date string (e.g., ISO 8601).`
276
+ );
277
+ }
278
+ return remoteTime > localTime ? conflict.remoteContent : conflict.localContent;
279
+ }
280
+
281
+ /**
282
+ * Resolve conflict using custom rules
283
+ *
284
+ * @private
285
+ * @param {Object} conflict - Conflict object
286
+ * @param {Object} rules - Resolution rules
287
+ * @returns {string} Resolved content
288
+ */
289
+ _resolveRulesBased(conflict, rules) {
290
+ const section = conflict.section || 'body';
291
+
292
+ // Check for section-specific rules
293
+ if (rules[section]) {
294
+ const sectionRules = rules[section];
295
+
296
+ // Extract field name from content (e.g., "priority: high")
297
+ const localMatch = conflict.localContent.match(/^(\w+):\s*(.+)$/);
298
+ const remoteMatch = conflict.remoteContent.match(/^(\w+):\s*(.+)$/);
299
+
300
+ if (localMatch && remoteMatch && localMatch[1] === remoteMatch[1]) {
301
+ const field = localMatch[1];
302
+ const localValue = localMatch[2];
303
+ const remoteValue = remoteMatch[2];
304
+
305
+ // Apply field-specific rule
306
+ if (sectionRules[field] === 'prefer-highest') {
307
+ // For priority fields, prefer higher priority
308
+ const priorities = ['low', 'medium', 'high', 'critical'];
309
+ const localPriority = priorities.indexOf(localValue);
310
+ const remotePriority = priorities.indexOf(remoteValue);
311
+
312
+ return localPriority > remotePriority ? conflict.localContent : conflict.remoteContent;
313
+ }
314
+
315
+ if (sectionRules[field] === 'prefer-local') {
316
+ return conflict.localContent;
317
+ }
318
+
319
+ if (sectionRules[field] === 'prefer-remote') {
320
+ return conflict.remoteContent;
321
+ }
322
+ }
323
+ }
324
+
325
+ // Default: prefer local if no matching rule
326
+ return conflict.localContent;
327
+ }
328
+ }
329
+
330
+ module.exports = ConflictResolver;