agmem 0.1.1__py3-none-any.whl

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 (67) hide show
  1. agmem-0.1.1.dist-info/METADATA +656 -0
  2. agmem-0.1.1.dist-info/RECORD +67 -0
  3. agmem-0.1.1.dist-info/WHEEL +5 -0
  4. agmem-0.1.1.dist-info/entry_points.txt +2 -0
  5. agmem-0.1.1.dist-info/licenses/LICENSE +21 -0
  6. agmem-0.1.1.dist-info/top_level.txt +1 -0
  7. memvcs/__init__.py +9 -0
  8. memvcs/cli.py +178 -0
  9. memvcs/commands/__init__.py +23 -0
  10. memvcs/commands/add.py +258 -0
  11. memvcs/commands/base.py +23 -0
  12. memvcs/commands/blame.py +169 -0
  13. memvcs/commands/branch.py +110 -0
  14. memvcs/commands/checkout.py +101 -0
  15. memvcs/commands/clean.py +76 -0
  16. memvcs/commands/clone.py +91 -0
  17. memvcs/commands/commit.py +174 -0
  18. memvcs/commands/daemon.py +267 -0
  19. memvcs/commands/diff.py +157 -0
  20. memvcs/commands/fsck.py +203 -0
  21. memvcs/commands/garden.py +107 -0
  22. memvcs/commands/graph.py +151 -0
  23. memvcs/commands/init.py +61 -0
  24. memvcs/commands/log.py +103 -0
  25. memvcs/commands/mcp.py +59 -0
  26. memvcs/commands/merge.py +88 -0
  27. memvcs/commands/pull.py +65 -0
  28. memvcs/commands/push.py +143 -0
  29. memvcs/commands/reflog.py +52 -0
  30. memvcs/commands/remote.py +51 -0
  31. memvcs/commands/reset.py +98 -0
  32. memvcs/commands/search.py +163 -0
  33. memvcs/commands/serve.py +54 -0
  34. memvcs/commands/show.py +125 -0
  35. memvcs/commands/stash.py +97 -0
  36. memvcs/commands/status.py +112 -0
  37. memvcs/commands/tag.py +117 -0
  38. memvcs/commands/test.py +132 -0
  39. memvcs/commands/tree.py +156 -0
  40. memvcs/core/__init__.py +21 -0
  41. memvcs/core/config_loader.py +245 -0
  42. memvcs/core/constants.py +12 -0
  43. memvcs/core/diff.py +380 -0
  44. memvcs/core/gardener.py +466 -0
  45. memvcs/core/hooks.py +151 -0
  46. memvcs/core/knowledge_graph.py +381 -0
  47. memvcs/core/merge.py +474 -0
  48. memvcs/core/objects.py +323 -0
  49. memvcs/core/pii_scanner.py +343 -0
  50. memvcs/core/refs.py +447 -0
  51. memvcs/core/remote.py +278 -0
  52. memvcs/core/repository.py +522 -0
  53. memvcs/core/schema.py +414 -0
  54. memvcs/core/staging.py +227 -0
  55. memvcs/core/storage/__init__.py +72 -0
  56. memvcs/core/storage/base.py +359 -0
  57. memvcs/core/storage/gcs.py +308 -0
  58. memvcs/core/storage/local.py +182 -0
  59. memvcs/core/storage/s3.py +369 -0
  60. memvcs/core/test_runner.py +371 -0
  61. memvcs/core/vector_store.py +313 -0
  62. memvcs/integrations/__init__.py +5 -0
  63. memvcs/integrations/mcp_server.py +267 -0
  64. memvcs/integrations/web_ui/__init__.py +1 -0
  65. memvcs/integrations/web_ui/server.py +352 -0
  66. memvcs/utils/__init__.py +9 -0
  67. memvcs/utils/helpers.py +178 -0
memvcs/core/diff.py ADDED
@@ -0,0 +1,380 @@
1
+ """
2
+ Diff functionality for agmem.
3
+
4
+ Compare commits, trees, and files.
5
+ """
6
+
7
+ from pathlib import Path
8
+ from typing import Optional, List, Dict, Any, Tuple
9
+ from dataclasses import dataclass
10
+ from enum import Enum
11
+
12
+ from .objects import ObjectStore, Commit, Tree, Blob
13
+
14
+
15
+ class DiffType(Enum):
16
+ """Types of differences."""
17
+ ADDED = "added"
18
+ DELETED = "deleted"
19
+ MODIFIED = "modified"
20
+ UNCHANGED = "unchanged"
21
+
22
+
23
+ @dataclass
24
+ class FileDiff:
25
+ """Difference for a single file."""
26
+ path: str
27
+ diff_type: DiffType
28
+ old_hash: Optional[str]
29
+ new_hash: Optional[str]
30
+ old_content: Optional[str]
31
+ new_content: Optional[str]
32
+ diff_lines: List[str]
33
+
34
+
35
+ @dataclass
36
+ class TreeDiff:
37
+ """Difference between two trees."""
38
+ files: List[FileDiff]
39
+ added_count: int
40
+ deleted_count: int
41
+ modified_count: int
42
+
43
+
44
+ class DiffEngine:
45
+ """Engine for computing differences."""
46
+
47
+ def __init__(self, object_store: ObjectStore):
48
+ self.object_store = object_store
49
+
50
+ def get_tree_files(self, tree_hash: str) -> Dict[str, str]:
51
+ """Get all files in a tree with their blob hashes."""
52
+ files = {}
53
+ tree = Tree.load(self.object_store, tree_hash)
54
+
55
+ if tree:
56
+ for entry in tree.entries:
57
+ path = entry.path + '/' + entry.name if entry.path else entry.name
58
+ files[path] = entry.hash
59
+
60
+ return files
61
+
62
+ def get_blob_content(self, hash_id: Optional[str]) -> Optional[str]:
63
+ """Get blob content as string."""
64
+ if not hash_id:
65
+ return None
66
+
67
+ blob = Blob.load(self.object_store, hash_id)
68
+ if blob:
69
+ return blob.content.decode('utf-8', errors='replace')
70
+ return None
71
+
72
+ def compute_line_diff(self, old_content: Optional[str], new_content: Optional[str]) -> List[str]:
73
+ """
74
+ Compute line-by-line diff between two contents.
75
+
76
+ Returns:
77
+ List of diff lines with +/- prefixes
78
+ """
79
+ old_lines = (old_content or '').splitlines(keepends=True)
80
+ new_lines = (new_content or '').splitlines(keepends=True)
81
+
82
+ # Simple diff algorithm (LCS-based would be better)
83
+ diff_lines = []
84
+
85
+ # Handle empty cases
86
+ if not old_lines or old_lines == ['']:
87
+ for line in new_lines:
88
+ diff_lines.append(f'+ {line.rstrip()}')
89
+ return diff_lines
90
+
91
+ if not new_lines or new_lines == ['']:
92
+ for line in old_lines:
93
+ diff_lines.append(f'- {line.rstrip()}')
94
+ return diff_lines
95
+
96
+ # Use unified diff style
97
+ max_lines = max(len(old_lines), len(new_lines))
98
+
99
+ i, j = 0, 0
100
+ while i < len(old_lines) or j < len(new_lines):
101
+ if i < len(old_lines) and j < len(new_lines):
102
+ old_line = old_lines[i].rstrip()
103
+ new_line = new_lines[j].rstrip()
104
+
105
+ if old_line == new_line:
106
+ diff_lines.append(f' {old_line}')
107
+ i += 1
108
+ j += 1
109
+ else:
110
+ # Find if this line exists later in new
111
+ found = False
112
+ for k in range(j + 1, min(j + 5, len(new_lines))):
113
+ if new_lines[k].rstrip() == old_line:
114
+ # Lines were added
115
+ for l in range(j, k):
116
+ diff_lines.append(f'+ {new_lines[l].rstrip()}')
117
+ j = k
118
+ found = True
119
+ break
120
+
121
+ if not found:
122
+ # Line was removed
123
+ diff_lines.append(f'- {old_line}')
124
+ i += 1
125
+ elif i < len(old_lines):
126
+ diff_lines.append(f'- {old_lines[i].rstrip()}')
127
+ i += 1
128
+ else:
129
+ diff_lines.append(f'+ {new_lines[j].rstrip()}')
130
+ j += 1
131
+
132
+ return diff_lines
133
+
134
+ def diff_trees(self, old_tree_hash: Optional[str], new_tree_hash: Optional[str]) -> TreeDiff:
135
+ """
136
+ Compute diff between two trees.
137
+
138
+ Args:
139
+ old_tree_hash: Hash of old tree (None for empty)
140
+ new_tree_hash: Hash of new tree (None for empty)
141
+
142
+ Returns:
143
+ TreeDiff with file differences
144
+ """
145
+ old_files = self.get_tree_files(old_tree_hash) if old_tree_hash else {}
146
+ new_files = self.get_tree_files(new_tree_hash) if new_tree_hash else {}
147
+
148
+ all_paths = set(old_files.keys()) | set(new_files.keys())
149
+
150
+ file_diffs = []
151
+ added = 0
152
+ deleted = 0
153
+ modified = 0
154
+
155
+ for path in sorted(all_paths):
156
+ old_hash = old_files.get(path)
157
+ new_hash = new_files.get(path)
158
+
159
+ if not old_hash and new_hash:
160
+ # Added
161
+ new_content = self.get_blob_content(new_hash)
162
+ diff_lines = self.compute_line_diff(None, new_content)
163
+
164
+ file_diffs.append(FileDiff(
165
+ path=path,
166
+ diff_type=DiffType.ADDED,
167
+ old_hash=None,
168
+ new_hash=new_hash,
169
+ old_content=None,
170
+ new_content=new_content,
171
+ diff_lines=diff_lines
172
+ ))
173
+ added += 1
174
+
175
+ elif old_hash and not new_hash:
176
+ # Deleted
177
+ old_content = self.get_blob_content(old_hash)
178
+ diff_lines = self.compute_line_diff(old_content, None)
179
+
180
+ file_diffs.append(FileDiff(
181
+ path=path,
182
+ diff_type=DiffType.DELETED,
183
+ old_hash=old_hash,
184
+ new_hash=None,
185
+ old_content=old_content,
186
+ new_content=None,
187
+ diff_lines=diff_lines
188
+ ))
189
+ deleted += 1
190
+
191
+ elif old_hash != new_hash:
192
+ # Modified
193
+ old_content = self.get_blob_content(old_hash)
194
+ new_content = self.get_blob_content(new_hash)
195
+ diff_lines = self.compute_line_diff(old_content, new_content)
196
+
197
+ file_diffs.append(FileDiff(
198
+ path=path,
199
+ diff_type=DiffType.MODIFIED,
200
+ old_hash=old_hash,
201
+ new_hash=new_hash,
202
+ old_content=old_content,
203
+ new_content=new_content,
204
+ diff_lines=diff_lines
205
+ ))
206
+ modified += 1
207
+
208
+ return TreeDiff(
209
+ files=file_diffs,
210
+ added_count=added,
211
+ deleted_count=deleted,
212
+ modified_count=modified
213
+ )
214
+
215
+ def diff_commits(self, old_commit_hash: Optional[str], new_commit_hash: Optional[str]) -> TreeDiff:
216
+ """
217
+ Compute diff between two commits.
218
+
219
+ Args:
220
+ old_commit_hash: Hash of old commit (None for empty)
221
+ new_commit_hash: Hash of new commit (None for empty)
222
+
223
+ Returns:
224
+ TreeDiff with file differences
225
+ """
226
+ old_tree_hash = None
227
+ new_tree_hash = None
228
+
229
+ if old_commit_hash:
230
+ old_commit = Commit.load(self.object_store, old_commit_hash)
231
+ if old_commit:
232
+ old_tree_hash = old_commit.tree
233
+
234
+ if new_commit_hash:
235
+ new_commit = Commit.load(self.object_store, new_commit_hash)
236
+ if new_commit:
237
+ new_tree_hash = new_commit.tree
238
+
239
+ return self.diff_trees(old_tree_hash, new_tree_hash)
240
+
241
+ def format_diff(self, tree_diff: TreeDiff, old_ref: str = 'a', new_ref: str = 'b') -> str:
242
+ """
243
+ Format tree diff as unified diff string.
244
+
245
+ Args:
246
+ tree_diff: TreeDiff to format
247
+ old_ref: Label for old version
248
+ new_ref: Label for new version
249
+
250
+ Returns:
251
+ Formatted diff string
252
+ """
253
+ lines = []
254
+
255
+ for file_diff in tree_diff.files:
256
+ if file_diff.diff_type == DiffType.UNCHANGED:
257
+ continue
258
+
259
+ # File header
260
+ lines.append(f"diff --agmem {old_ref}/{file_diff.path} {new_ref}/{file_diff.path}")
261
+
262
+ if file_diff.diff_type == DiffType.ADDED:
263
+ lines.append(f"new file mode 100644")
264
+ lines.append(f"index 0000000..{file_diff.new_hash[:7]}")
265
+ lines.append(f"--- /dev/null")
266
+ lines.append(f"+++ {new_ref}/{file_diff.path}")
267
+ elif file_diff.diff_type == DiffType.DELETED:
268
+ lines.append(f"deleted file mode 100644")
269
+ lines.append(f"index {file_diff.old_hash[:7]}..0000000")
270
+ lines.append(f"--- {old_ref}/{file_diff.path}")
271
+ lines.append(f"+++ /dev/null")
272
+ else: # MODIFIED
273
+ lines.append(f"index {file_diff.old_hash[:7]}..{file_diff.new_hash[:7]}")
274
+ lines.append(f"--- {old_ref}/{file_diff.path}")
275
+ lines.append(f"+++ {new_ref}/{file_diff.path}")
276
+
277
+ # Diff content
278
+ lines.append("@@ -1 +1 @@")
279
+ for diff_line in file_diff.diff_lines:
280
+ lines.append(diff_line)
281
+
282
+ lines.append("") # Empty line between files
283
+
284
+ # Summary
285
+ lines.append(f"# {tree_diff.added_count} file(s) added")
286
+ lines.append(f"# {tree_diff.deleted_count} file(s) deleted")
287
+ lines.append(f"# {tree_diff.modified_count} file(s) modified")
288
+
289
+ return '\n'.join(lines)
290
+
291
+ def diff_working_dir(self, commit_hash: str, working_files: Dict[str, bytes]) -> TreeDiff:
292
+ """
293
+ Compute diff between a commit and working directory.
294
+
295
+ Args:
296
+ commit_hash: Commit to compare against
297
+ working_files: Dict mapping paths to file contents
298
+
299
+ Returns:
300
+ TreeDiff with differences
301
+ """
302
+ # Get commit files
303
+ commit = Commit.load(self.object_store, commit_hash)
304
+ if not commit:
305
+ return TreeDiff(files=[], added_count=0, deleted_count=0, modified_count=0)
306
+
307
+ commit_files = self.get_tree_files(commit.tree)
308
+
309
+ file_diffs = []
310
+ added = 0
311
+ deleted = 0
312
+ modified = 0
313
+
314
+ all_paths = set(commit_files.keys()) | set(working_files.keys())
315
+
316
+ for path in sorted(all_paths):
317
+ commit_hash_id = commit_files.get(path)
318
+ working_content = working_files.get(path)
319
+
320
+ # Compute working file hash
321
+ working_hash = None
322
+ if working_content is not None:
323
+ blob = Blob(content=working_content)
324
+ working_hash = blob.store(self.object_store)
325
+
326
+ if not commit_hash_id and working_hash:
327
+ # Added
328
+ new_content = working_content.decode('utf-8', errors='replace') if working_content else None
329
+ diff_lines = self.compute_line_diff(None, new_content)
330
+
331
+ file_diffs.append(FileDiff(
332
+ path=path,
333
+ diff_type=DiffType.ADDED,
334
+ old_hash=None,
335
+ new_hash=working_hash,
336
+ old_content=None,
337
+ new_content=new_content,
338
+ diff_lines=diff_lines
339
+ ))
340
+ added += 1
341
+
342
+ elif commit_hash_id and not working_hash:
343
+ # Deleted
344
+ old_content = self.get_blob_content(commit_hash_id)
345
+ diff_lines = self.compute_line_diff(old_content, None)
346
+
347
+ file_diffs.append(FileDiff(
348
+ path=path,
349
+ diff_type=DiffType.DELETED,
350
+ old_hash=commit_hash_id,
351
+ new_hash=None,
352
+ old_content=old_content,
353
+ new_content=None,
354
+ diff_lines=diff_lines
355
+ ))
356
+ deleted += 1
357
+
358
+ elif commit_hash_id != working_hash:
359
+ # Modified
360
+ old_content = self.get_blob_content(commit_hash_id)
361
+ new_content = working_content.decode('utf-8', errors='replace') if working_content else None
362
+ diff_lines = self.compute_line_diff(old_content, new_content)
363
+
364
+ file_diffs.append(FileDiff(
365
+ path=path,
366
+ diff_type=DiffType.MODIFIED,
367
+ old_hash=commit_hash_id,
368
+ new_hash=working_hash,
369
+ old_content=old_content,
370
+ new_content=new_content,
371
+ diff_lines=diff_lines
372
+ ))
373
+ modified += 1
374
+
375
+ return TreeDiff(
376
+ files=file_diffs,
377
+ added_count=added,
378
+ deleted_count=deleted,
379
+ modified_count=modified
380
+ )