agmem 0.1.1__py3-none-any.whl → 0.1.2__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.
- {agmem-0.1.1.dist-info → agmem-0.1.2.dist-info}/METADATA +20 -3
- agmem-0.1.2.dist-info/RECORD +86 -0
- memvcs/__init__.py +1 -1
- memvcs/cli.py +35 -31
- memvcs/commands/__init__.py +9 -9
- memvcs/commands/add.py +77 -76
- memvcs/commands/blame.py +46 -53
- memvcs/commands/branch.py +13 -33
- memvcs/commands/checkout.py +27 -32
- memvcs/commands/clean.py +18 -23
- memvcs/commands/clone.py +4 -1
- memvcs/commands/commit.py +40 -39
- memvcs/commands/daemon.py +81 -76
- memvcs/commands/decay.py +77 -0
- memvcs/commands/diff.py +56 -57
- memvcs/commands/distill.py +74 -0
- memvcs/commands/fsck.py +55 -61
- memvcs/commands/garden.py +28 -37
- memvcs/commands/graph.py +41 -48
- memvcs/commands/init.py +16 -24
- memvcs/commands/log.py +25 -40
- memvcs/commands/merge.py +16 -28
- memvcs/commands/pack.py +129 -0
- memvcs/commands/pull.py +4 -1
- memvcs/commands/push.py +4 -2
- memvcs/commands/recall.py +145 -0
- memvcs/commands/reflog.py +13 -22
- memvcs/commands/remote.py +1 -0
- memvcs/commands/repair.py +66 -0
- memvcs/commands/reset.py +23 -33
- memvcs/commands/resurrect.py +82 -0
- memvcs/commands/search.py +3 -4
- memvcs/commands/serve.py +2 -1
- memvcs/commands/show.py +66 -36
- memvcs/commands/stash.py +34 -34
- memvcs/commands/status.py +27 -35
- memvcs/commands/tag.py +23 -47
- memvcs/commands/test.py +30 -44
- memvcs/commands/timeline.py +111 -0
- memvcs/commands/tree.py +26 -27
- memvcs/commands/verify.py +59 -0
- memvcs/commands/when.py +115 -0
- memvcs/core/access_index.py +167 -0
- memvcs/core/config_loader.py +3 -1
- memvcs/core/consistency.py +214 -0
- memvcs/core/decay.py +185 -0
- memvcs/core/diff.py +158 -143
- memvcs/core/distiller.py +277 -0
- memvcs/core/gardener.py +164 -132
- memvcs/core/hooks.py +48 -14
- memvcs/core/knowledge_graph.py +134 -138
- memvcs/core/merge.py +248 -171
- memvcs/core/objects.py +95 -96
- memvcs/core/pii_scanner.py +147 -146
- memvcs/core/refs.py +132 -115
- memvcs/core/repository.py +174 -164
- memvcs/core/schema.py +155 -113
- memvcs/core/staging.py +60 -65
- memvcs/core/storage/__init__.py +20 -18
- memvcs/core/storage/base.py +74 -70
- memvcs/core/storage/gcs.py +70 -68
- memvcs/core/storage/local.py +42 -40
- memvcs/core/storage/s3.py +105 -110
- memvcs/core/temporal_index.py +112 -0
- memvcs/core/test_runner.py +101 -93
- memvcs/core/vector_store.py +41 -35
- memvcs/integrations/mcp_server.py +1 -3
- memvcs/integrations/web_ui/server.py +25 -26
- memvcs/retrieval/__init__.py +22 -0
- memvcs/retrieval/base.py +54 -0
- memvcs/retrieval/pack.py +128 -0
- memvcs/retrieval/recaller.py +105 -0
- memvcs/retrieval/strategies.py +314 -0
- memvcs/utils/__init__.py +3 -3
- memvcs/utils/helpers.py +52 -52
- agmem-0.1.1.dist-info/RECORD +0 -67
- {agmem-0.1.1.dist-info → agmem-0.1.2.dist-info}/WHEEL +0 -0
- {agmem-0.1.1.dist-info → agmem-0.1.2.dist-info}/entry_points.txt +0 -0
- {agmem-0.1.1.dist-info → agmem-0.1.2.dist-info}/licenses/LICENSE +0 -0
- {agmem-0.1.1.dist-info → agmem-0.1.2.dist-info}/top_level.txt +0 -0
memvcs/core/storage/base.py
CHANGED
|
@@ -12,17 +12,20 @@ from pathlib import Path
|
|
|
12
12
|
|
|
13
13
|
class StorageError(Exception):
|
|
14
14
|
"""Base exception for storage operations."""
|
|
15
|
+
|
|
15
16
|
pass
|
|
16
17
|
|
|
17
18
|
|
|
18
19
|
class LockError(StorageError):
|
|
19
20
|
"""Exception raised when a lock cannot be acquired."""
|
|
21
|
+
|
|
20
22
|
pass
|
|
21
23
|
|
|
22
24
|
|
|
23
25
|
@dataclass
|
|
24
26
|
class FileInfo:
|
|
25
27
|
"""Information about a file in storage."""
|
|
28
|
+
|
|
26
29
|
path: str
|
|
27
30
|
size: int
|
|
28
31
|
modified: Optional[str] = None # ISO 8601 timestamp
|
|
@@ -32,184 +35,184 @@ class FileInfo:
|
|
|
32
35
|
class StorageAdapter(ABC):
|
|
33
36
|
"""
|
|
34
37
|
Abstract base class for storage adapters.
|
|
35
|
-
|
|
38
|
+
|
|
36
39
|
All storage backends (local filesystem, S3, GCS, etc.) must implement
|
|
37
40
|
this interface to provide consistent access to storage operations.
|
|
38
41
|
"""
|
|
39
|
-
|
|
42
|
+
|
|
40
43
|
@abstractmethod
|
|
41
44
|
def read_file(self, path: str) -> bytes:
|
|
42
45
|
"""
|
|
43
46
|
Read a file's contents.
|
|
44
|
-
|
|
47
|
+
|
|
45
48
|
Args:
|
|
46
49
|
path: Path to the file (relative to storage root)
|
|
47
|
-
|
|
50
|
+
|
|
48
51
|
Returns:
|
|
49
52
|
File contents as bytes
|
|
50
|
-
|
|
53
|
+
|
|
51
54
|
Raises:
|
|
52
55
|
StorageError: If file doesn't exist or can't be read
|
|
53
56
|
"""
|
|
54
57
|
pass
|
|
55
|
-
|
|
58
|
+
|
|
56
59
|
@abstractmethod
|
|
57
60
|
def write_file(self, path: str, data: bytes) -> None:
|
|
58
61
|
"""
|
|
59
62
|
Write data to a file.
|
|
60
|
-
|
|
63
|
+
|
|
61
64
|
Args:
|
|
62
65
|
path: Path to the file (relative to storage root)
|
|
63
66
|
data: Data to write
|
|
64
|
-
|
|
67
|
+
|
|
65
68
|
Raises:
|
|
66
69
|
StorageError: If file can't be written
|
|
67
70
|
"""
|
|
68
71
|
pass
|
|
69
|
-
|
|
72
|
+
|
|
70
73
|
@abstractmethod
|
|
71
74
|
def exists(self, path: str) -> bool:
|
|
72
75
|
"""
|
|
73
76
|
Check if a path exists.
|
|
74
|
-
|
|
77
|
+
|
|
75
78
|
Args:
|
|
76
79
|
path: Path to check
|
|
77
|
-
|
|
80
|
+
|
|
78
81
|
Returns:
|
|
79
82
|
True if path exists, False otherwise
|
|
80
83
|
"""
|
|
81
84
|
pass
|
|
82
|
-
|
|
85
|
+
|
|
83
86
|
@abstractmethod
|
|
84
87
|
def delete(self, path: str) -> bool:
|
|
85
88
|
"""
|
|
86
89
|
Delete a file.
|
|
87
|
-
|
|
90
|
+
|
|
88
91
|
Args:
|
|
89
92
|
path: Path to the file
|
|
90
|
-
|
|
93
|
+
|
|
91
94
|
Returns:
|
|
92
95
|
True if deleted, False if not found
|
|
93
96
|
"""
|
|
94
97
|
pass
|
|
95
|
-
|
|
98
|
+
|
|
96
99
|
@abstractmethod
|
|
97
100
|
def list_dir(self, path: str = "") -> List[FileInfo]:
|
|
98
101
|
"""
|
|
99
102
|
List contents of a directory.
|
|
100
|
-
|
|
103
|
+
|
|
101
104
|
Args:
|
|
102
105
|
path: Directory path (empty for root)
|
|
103
|
-
|
|
106
|
+
|
|
104
107
|
Returns:
|
|
105
108
|
List of FileInfo objects for directory contents
|
|
106
109
|
"""
|
|
107
110
|
pass
|
|
108
|
-
|
|
111
|
+
|
|
109
112
|
@abstractmethod
|
|
110
113
|
def makedirs(self, path: str) -> None:
|
|
111
114
|
"""
|
|
112
115
|
Create directory and any necessary parent directories.
|
|
113
|
-
|
|
116
|
+
|
|
114
117
|
Args:
|
|
115
118
|
path: Directory path to create
|
|
116
119
|
"""
|
|
117
120
|
pass
|
|
118
|
-
|
|
121
|
+
|
|
119
122
|
@abstractmethod
|
|
120
123
|
def is_dir(self, path: str) -> bool:
|
|
121
124
|
"""
|
|
122
125
|
Check if path is a directory.
|
|
123
|
-
|
|
126
|
+
|
|
124
127
|
Args:
|
|
125
128
|
path: Path to check
|
|
126
|
-
|
|
129
|
+
|
|
127
130
|
Returns:
|
|
128
131
|
True if path is a directory
|
|
129
132
|
"""
|
|
130
133
|
pass
|
|
131
|
-
|
|
134
|
+
|
|
132
135
|
# Lock management methods
|
|
133
|
-
|
|
136
|
+
|
|
134
137
|
@abstractmethod
|
|
135
138
|
def acquire_lock(self, lock_name: str, timeout: int = 30) -> bool:
|
|
136
139
|
"""
|
|
137
140
|
Acquire a distributed lock.
|
|
138
|
-
|
|
141
|
+
|
|
139
142
|
Args:
|
|
140
143
|
lock_name: Name of the lock to acquire
|
|
141
144
|
timeout: Maximum seconds to wait for lock
|
|
142
|
-
|
|
145
|
+
|
|
143
146
|
Returns:
|
|
144
147
|
True if lock acquired successfully
|
|
145
|
-
|
|
148
|
+
|
|
146
149
|
Raises:
|
|
147
150
|
LockError: If lock cannot be acquired within timeout
|
|
148
151
|
"""
|
|
149
152
|
pass
|
|
150
|
-
|
|
153
|
+
|
|
151
154
|
@abstractmethod
|
|
152
155
|
def release_lock(self, lock_name: str) -> None:
|
|
153
156
|
"""
|
|
154
157
|
Release a distributed lock.
|
|
155
|
-
|
|
158
|
+
|
|
156
159
|
Args:
|
|
157
160
|
lock_name: Name of the lock to release
|
|
158
161
|
"""
|
|
159
162
|
pass
|
|
160
|
-
|
|
163
|
+
|
|
161
164
|
@abstractmethod
|
|
162
165
|
def is_locked(self, lock_name: str) -> bool:
|
|
163
166
|
"""
|
|
164
167
|
Check if a lock is currently held.
|
|
165
|
-
|
|
168
|
+
|
|
166
169
|
Args:
|
|
167
170
|
lock_name: Name of the lock to check
|
|
168
|
-
|
|
171
|
+
|
|
169
172
|
Returns:
|
|
170
173
|
True if lock is held
|
|
171
174
|
"""
|
|
172
175
|
pass
|
|
173
|
-
|
|
176
|
+
|
|
174
177
|
# Convenience methods (can be overridden for efficiency)
|
|
175
|
-
|
|
176
|
-
def read_text(self, path: str, encoding: str =
|
|
178
|
+
|
|
179
|
+
def read_text(self, path: str, encoding: str = "utf-8") -> str:
|
|
177
180
|
"""Read file as text."""
|
|
178
181
|
return self.read_file(path).decode(encoding)
|
|
179
|
-
|
|
180
|
-
def write_text(self, path: str, text: str, encoding: str =
|
|
182
|
+
|
|
183
|
+
def write_text(self, path: str, text: str, encoding: str = "utf-8") -> None:
|
|
181
184
|
"""Write text to file."""
|
|
182
185
|
self.write_file(path, text.encode(encoding))
|
|
183
|
-
|
|
186
|
+
|
|
184
187
|
def walk(self, path: str = "") -> Iterator[tuple]:
|
|
185
188
|
"""
|
|
186
189
|
Walk through directory tree.
|
|
187
|
-
|
|
190
|
+
|
|
188
191
|
Yields:
|
|
189
192
|
Tuples of (dirpath, dirnames, filenames)
|
|
190
193
|
"""
|
|
191
194
|
contents = self.list_dir(path)
|
|
192
|
-
|
|
195
|
+
|
|
193
196
|
dirs = []
|
|
194
197
|
files = []
|
|
195
|
-
|
|
198
|
+
|
|
196
199
|
for item in contents:
|
|
197
200
|
if item.is_dir:
|
|
198
|
-
dirs.append(item.path.split(
|
|
201
|
+
dirs.append(item.path.split("/")[-1])
|
|
199
202
|
else:
|
|
200
|
-
files.append(item.path.split(
|
|
201
|
-
|
|
203
|
+
files.append(item.path.split("/")[-1])
|
|
204
|
+
|
|
202
205
|
yield (path, dirs, files)
|
|
203
|
-
|
|
206
|
+
|
|
204
207
|
for dirname in dirs:
|
|
205
208
|
subpath = f"{path}/{dirname}" if path else dirname
|
|
206
209
|
yield from self.walk(subpath)
|
|
207
|
-
|
|
210
|
+
|
|
208
211
|
def copy_file(self, src: str, dst: str) -> None:
|
|
209
212
|
"""Copy a file within storage."""
|
|
210
213
|
data = self.read_file(src)
|
|
211
214
|
self.write_file(dst, data)
|
|
212
|
-
|
|
215
|
+
|
|
213
216
|
def move_file(self, src: str, dst: str) -> None:
|
|
214
217
|
"""Move a file within storage."""
|
|
215
218
|
self.copy_file(src, dst)
|
|
@@ -219,14 +222,14 @@ class StorageAdapter(ABC):
|
|
|
219
222
|
class CachingStorageAdapter(StorageAdapter):
|
|
220
223
|
"""
|
|
221
224
|
Storage adapter that caches remote operations locally.
|
|
222
|
-
|
|
225
|
+
|
|
223
226
|
Used for cloud storage backends to minimize network requests.
|
|
224
227
|
"""
|
|
225
|
-
|
|
228
|
+
|
|
226
229
|
def __init__(self, remote: StorageAdapter, cache_dir: str):
|
|
227
230
|
"""
|
|
228
231
|
Initialize caching adapter.
|
|
229
|
-
|
|
232
|
+
|
|
230
233
|
Args:
|
|
231
234
|
remote: Remote storage adapter
|
|
232
235
|
cache_dir: Local directory for caching
|
|
@@ -235,35 +238,35 @@ class CachingStorageAdapter(StorageAdapter):
|
|
|
235
238
|
self.cache_dir = Path(cache_dir)
|
|
236
239
|
self.cache_dir.mkdir(parents=True, exist_ok=True)
|
|
237
240
|
self._dirty: set = set() # Paths that need to be pushed
|
|
238
|
-
|
|
241
|
+
|
|
239
242
|
def _cache_path(self, path: str) -> Path:
|
|
240
243
|
"""Get local cache path for a remote path."""
|
|
241
244
|
return self.cache_dir / path
|
|
242
|
-
|
|
245
|
+
|
|
243
246
|
def read_file(self, path: str) -> bytes:
|
|
244
247
|
"""Read from cache, fetching from remote if needed."""
|
|
245
248
|
cache_path = self._cache_path(path)
|
|
246
|
-
|
|
249
|
+
|
|
247
250
|
if not cache_path.exists():
|
|
248
251
|
# Fetch from remote
|
|
249
252
|
data = self.remote.read_file(path)
|
|
250
253
|
cache_path.parent.mkdir(parents=True, exist_ok=True)
|
|
251
254
|
cache_path.write_bytes(data)
|
|
252
|
-
|
|
255
|
+
|
|
253
256
|
return cache_path.read_bytes()
|
|
254
|
-
|
|
257
|
+
|
|
255
258
|
def write_file(self, path: str, data: bytes) -> None:
|
|
256
259
|
"""Write to cache and mark as dirty."""
|
|
257
260
|
cache_path = self._cache_path(path)
|
|
258
261
|
cache_path.parent.mkdir(parents=True, exist_ok=True)
|
|
259
262
|
cache_path.write_bytes(data)
|
|
260
263
|
self._dirty.add(path)
|
|
261
|
-
|
|
264
|
+
|
|
262
265
|
def exists(self, path: str) -> bool:
|
|
263
266
|
"""Check if path exists in cache or remote."""
|
|
264
267
|
cache_path = self._cache_path(path)
|
|
265
268
|
return cache_path.exists() or self.remote.exists(path)
|
|
266
|
-
|
|
269
|
+
|
|
267
270
|
def delete(self, path: str) -> bool:
|
|
268
271
|
"""Delete from cache and remote."""
|
|
269
272
|
cache_path = self._cache_path(path)
|
|
@@ -271,39 +274,39 @@ class CachingStorageAdapter(StorageAdapter):
|
|
|
271
274
|
cache_path.unlink()
|
|
272
275
|
self._dirty.discard(path)
|
|
273
276
|
return self.remote.delete(path)
|
|
274
|
-
|
|
277
|
+
|
|
275
278
|
def list_dir(self, path: str = "") -> List[FileInfo]:
|
|
276
279
|
"""List directory from remote."""
|
|
277
280
|
return self.remote.list_dir(path)
|
|
278
|
-
|
|
281
|
+
|
|
279
282
|
def makedirs(self, path: str) -> None:
|
|
280
283
|
"""Create directory in cache."""
|
|
281
284
|
cache_path = self._cache_path(path)
|
|
282
285
|
cache_path.mkdir(parents=True, exist_ok=True)
|
|
283
|
-
|
|
286
|
+
|
|
284
287
|
def is_dir(self, path: str) -> bool:
|
|
285
288
|
"""Check if path is directory."""
|
|
286
289
|
cache_path = self._cache_path(path)
|
|
287
290
|
if cache_path.exists():
|
|
288
291
|
return cache_path.is_dir()
|
|
289
292
|
return self.remote.is_dir(path)
|
|
290
|
-
|
|
293
|
+
|
|
291
294
|
def acquire_lock(self, lock_name: str, timeout: int = 30) -> bool:
|
|
292
295
|
"""Acquire lock on remote."""
|
|
293
296
|
return self.remote.acquire_lock(lock_name, timeout)
|
|
294
|
-
|
|
297
|
+
|
|
295
298
|
def release_lock(self, lock_name: str) -> None:
|
|
296
299
|
"""Release lock on remote."""
|
|
297
300
|
self.remote.release_lock(lock_name)
|
|
298
|
-
|
|
301
|
+
|
|
299
302
|
def is_locked(self, lock_name: str) -> bool:
|
|
300
303
|
"""Check if lock is held on remote."""
|
|
301
304
|
return self.remote.is_locked(lock_name)
|
|
302
|
-
|
|
305
|
+
|
|
303
306
|
def sync_to_remote(self) -> int:
|
|
304
307
|
"""
|
|
305
308
|
Push all dirty files to remote.
|
|
306
|
-
|
|
309
|
+
|
|
307
310
|
Returns:
|
|
308
311
|
Number of files synced
|
|
309
312
|
"""
|
|
@@ -315,14 +318,14 @@ class CachingStorageAdapter(StorageAdapter):
|
|
|
315
318
|
count += 1
|
|
316
319
|
self._dirty.discard(path)
|
|
317
320
|
return count
|
|
318
|
-
|
|
321
|
+
|
|
319
322
|
def sync_from_remote(self, paths: Optional[List[str]] = None) -> int:
|
|
320
323
|
"""
|
|
321
324
|
Pull files from remote to cache.
|
|
322
|
-
|
|
325
|
+
|
|
323
326
|
Args:
|
|
324
327
|
paths: Specific paths to sync, or None for all
|
|
325
|
-
|
|
328
|
+
|
|
326
329
|
Returns:
|
|
327
330
|
Number of files synced
|
|
328
331
|
"""
|
|
@@ -345,14 +348,15 @@ class CachingStorageAdapter(StorageAdapter):
|
|
|
345
348
|
cache_path.parent.mkdir(parents=True, exist_ok=True)
|
|
346
349
|
cache_path.write_bytes(data)
|
|
347
350
|
return len(paths)
|
|
348
|
-
|
|
351
|
+
|
|
349
352
|
def get_dirty_paths(self) -> List[str]:
|
|
350
353
|
"""Get list of paths that need to be pushed."""
|
|
351
354
|
return list(self._dirty)
|
|
352
|
-
|
|
355
|
+
|
|
353
356
|
def clear_cache(self) -> None:
|
|
354
357
|
"""Clear the local cache."""
|
|
355
358
|
import shutil
|
|
359
|
+
|
|
356
360
|
if self.cache_dir.exists():
|
|
357
361
|
shutil.rmtree(self.cache_dir)
|
|
358
362
|
self.cache_dir.mkdir(parents=True, exist_ok=True)
|