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/remote.py ADDED
@@ -0,0 +1,278 @@
1
+ """
2
+ Remote sync for agmem - file-based push/pull/clone.
3
+
4
+ Supports file:// URLs for local or mounted directories.
5
+ """
6
+
7
+ import json
8
+ import shutil
9
+ from pathlib import Path
10
+ from typing import Optional, Set
11
+ from urllib.parse import urlparse
12
+
13
+ from .objects import ObjectStore, Commit, Tree, Blob, _valid_object_hash
14
+ from .refs import RefsManager, _ref_path_under_root
15
+
16
+
17
+ def parse_remote_url(url: str) -> Path:
18
+ """Parse remote URL to local path. Supports file:// only. Rejects path traversal."""
19
+ parsed = urlparse(url)
20
+ if parsed.scheme == "file" or parsed.scheme == "":
21
+ path = parsed.path or parsed.netloc or url
22
+ resolved = Path(path).resolve()
23
+ if not resolved.exists():
24
+ raise ValueError(f"Remote path does not exist: {resolved}")
25
+ if not resolved.is_dir():
26
+ raise ValueError(f"Remote path is not a directory: {resolved}")
27
+ return resolved
28
+ raise ValueError(f"Unsupported remote URL scheme: {parsed.scheme}. Use file://")
29
+
30
+
31
+ def _collect_objects_from_commit(store: ObjectStore, commit_hash: str) -> Set[str]:
32
+ """Recursively collect all object hashes reachable from a commit."""
33
+ seen = set()
34
+ todo = [commit_hash]
35
+
36
+ while todo:
37
+ h = todo.pop()
38
+ if h in seen:
39
+ continue
40
+ seen.add(h)
41
+
42
+ # Try commit
43
+ content = store.retrieve(h, "commit")
44
+ if content:
45
+ data = json.loads(content)
46
+ todo.extend(data.get("parents", []))
47
+ if "tree" in data:
48
+ todo.append(data["tree"])
49
+ continue
50
+
51
+ # Try tree
52
+ content = store.retrieve(h, "tree")
53
+ if content:
54
+ data = json.loads(content)
55
+ for e in data.get("entries", []):
56
+ if "hash" in e:
57
+ todo.append(e["hash"])
58
+ continue
59
+
60
+ # Blob - no follow
61
+
62
+ return seen
63
+
64
+
65
+ def _list_local_objects(objects_dir: Path) -> Set[str]:
66
+ """List all object hashes in a .mem/objects directory."""
67
+ hashes = set()
68
+ for obj_type in ["blob", "tree", "commit"]:
69
+ type_dir = objects_dir / obj_type
70
+ if not type_dir.exists():
71
+ continue
72
+ for prefix_dir in type_dir.iterdir():
73
+ if prefix_dir.is_dir():
74
+ for suffix_file in prefix_dir.iterdir():
75
+ hash_id = prefix_dir.name + suffix_file.name
76
+ hashes.add(hash_id)
77
+ return hashes
78
+
79
+
80
+ def _get_object_path(objects_dir: Path, hash_id: str) -> Optional[Path]:
81
+ """Get path for an object. Returns path if found, else None. Validates hash_id."""
82
+ if not _valid_object_hash(hash_id):
83
+ return None
84
+ for otype in ["blob", "tree", "commit"]:
85
+ p = objects_dir / otype / hash_id[:2] / hash_id[2:]
86
+ if p.exists():
87
+ return p
88
+ return None
89
+
90
+
91
+ def _copy_object(src_dir: Path, dst_dir: Path, hash_id: str) -> bool:
92
+ """Copy a single object. Returns True if copied. Validates hash_id."""
93
+ if not _valid_object_hash(hash_id):
94
+ return False
95
+ src = _get_object_path(src_dir, hash_id)
96
+ if not src or not src.exists():
97
+ return False
98
+ # Infer type from path (e.g. .../blob/xx/yy)
99
+ obj_type = src.parent.parent.name
100
+ dst = dst_dir / obj_type / hash_id[:2] / hash_id[2:]
101
+ dst.parent.mkdir(parents=True, exist_ok=True)
102
+ if not dst.exists() or dst.stat().st_size != src.stat().st_size:
103
+ shutil.copy2(src, dst)
104
+ return True
105
+ return False
106
+
107
+
108
+ class Remote:
109
+ """Remote repository for push/pull operations."""
110
+
111
+ def __init__(self, repo_path: Path, name: str = "origin"):
112
+ self.repo_path = Path(repo_path)
113
+ self.mem_dir = self.repo_path / ".mem"
114
+ self.objects_dir = self.mem_dir / "objects"
115
+ self.name = name
116
+ self._config = self._load_config()
117
+
118
+ def _load_config(self) -> dict:
119
+ config_file = self.mem_dir / "config.json"
120
+ if config_file.exists():
121
+ return json.loads(config_file.read_text())
122
+ return {}
123
+
124
+ def _save_config(self, config: dict):
125
+ config_file = self.mem_dir / "config.json"
126
+ config_file.write_text(json.dumps(config, indent=2))
127
+
128
+ def get_remote_url(self) -> Optional[str]:
129
+ """Get remote URL for the given name."""
130
+ remotes = self._config.get("remotes", {})
131
+ return remotes.get(self.name, {}).get("url")
132
+
133
+ def set_remote_url(self, url: str):
134
+ """Set remote URL."""
135
+ if "remotes" not in self._config:
136
+ self._config["remotes"] = {}
137
+ if self.name not in self._config["remotes"]:
138
+ self._config["remotes"][self.name] = {}
139
+ self._config["remotes"][self.name]["url"] = url
140
+ self._save_config(self._config)
141
+
142
+ def push(self, branch: Optional[str] = None) -> str:
143
+ """
144
+ Push objects and refs to remote.
145
+ Returns status message.
146
+ """
147
+ url = self.get_remote_url()
148
+ if not url:
149
+ raise ValueError(f"Remote '{self.name}' has no URL configured")
150
+
151
+ remote_path = parse_remote_url(url)
152
+ remote_mem = remote_path / ".mem"
153
+ remote_objects = remote_mem / "objects"
154
+ remote_refs = remote_mem / "refs"
155
+
156
+ if not remote_path.exists():
157
+ raise ValueError(f"Remote path does not exist: {remote_path}")
158
+
159
+ remote_mem.mkdir(parents=True, exist_ok=True)
160
+ remote_objects.mkdir(parents=True, exist_ok=True)
161
+ (remote_refs / "heads").mkdir(parents=True, exist_ok=True)
162
+ (remote_refs / "tags").mkdir(parents=True, exist_ok=True)
163
+
164
+ refs = RefsManager(self.mem_dir)
165
+ store = ObjectStore(self.objects_dir)
166
+
167
+ # Collect objects to push
168
+ to_push = set()
169
+ for b in refs.list_branches():
170
+ if branch and b != branch:
171
+ continue
172
+ ch = refs.get_branch_commit(b)
173
+ if ch:
174
+ to_push.update(_collect_objects_from_commit(store, ch))
175
+ for t in refs.list_tags():
176
+ ch = refs.get_tag_commit(t)
177
+ if ch:
178
+ to_push.update(_collect_objects_from_commit(store, ch))
179
+
180
+ remote_has = _list_local_objects(remote_objects)
181
+ missing = to_push - remote_has
182
+
183
+ # Copy objects
184
+ copied = 0
185
+ for h in missing:
186
+ if _copy_object(self.objects_dir, remote_objects, h):
187
+ copied += 1
188
+
189
+ # Copy refs (validate names so remote path stays under refs/heads and refs/tags)
190
+ remote_heads = remote_refs / "heads"
191
+ remote_tags_dir = remote_refs / "tags"
192
+ for b in refs.list_branches():
193
+ if branch and b != branch:
194
+ continue
195
+ if not _ref_path_under_root(b, remote_heads):
196
+ continue
197
+ ch = refs.get_branch_commit(b)
198
+ if ch:
199
+ (remote_heads / b).parent.mkdir(parents=True, exist_ok=True)
200
+ (remote_heads / b).write_text(ch + "\n")
201
+ for t in refs.list_tags():
202
+ if not _ref_path_under_root(t, remote_tags_dir):
203
+ continue
204
+ ch = refs.get_tag_commit(t)
205
+ if ch:
206
+ (remote_tags_dir / t).parent.mkdir(parents=True, exist_ok=True)
207
+ (remote_tags_dir / t).write_text(ch + "\n")
208
+
209
+ return f"Pushed {copied} object(s) to {self.name}"
210
+
211
+ def fetch(self, branch: Optional[str] = None) -> str:
212
+ """
213
+ Fetch objects and refs from remote into local.
214
+ Returns status message.
215
+ """
216
+ url = self.get_remote_url()
217
+ if not url:
218
+ raise ValueError(f"Remote '{self.name}' has no URL configured")
219
+
220
+ remote_path = parse_remote_url(url)
221
+ remote_objects = remote_path / ".mem" / "objects"
222
+ remote_refs = remote_path / ".mem" / "refs"
223
+
224
+ if not remote_objects.exists():
225
+ raise ValueError(f"Remote is not an agmem repository: {remote_path}")
226
+
227
+ refs = RefsManager(self.mem_dir)
228
+ remote_store = ObjectStore(remote_objects)
229
+
230
+ # Collect remote refs to fetch (traverse remote's objects)
231
+ to_fetch = set()
232
+ heads_dir = remote_refs / "heads"
233
+ if heads_dir.exists():
234
+ for f in heads_dir.rglob("*"):
235
+ if f.is_file():
236
+ branch_name = str(f.relative_to(heads_dir))
237
+ if branch is not None and branch_name != branch:
238
+ continue
239
+ ch = f.read_text().strip()
240
+ if ch and _valid_object_hash(ch):
241
+ to_fetch.update(_collect_objects_from_commit(remote_store, ch))
242
+ tags_dir = remote_refs / "tags"
243
+ if tags_dir.exists() and branch is None:
244
+ for f in tags_dir.rglob("*"):
245
+ if f.is_file():
246
+ ch = f.read_text().strip()
247
+ if ch and _valid_object_hash(ch):
248
+ to_fetch.update(_collect_objects_from_commit(remote_store, ch))
249
+
250
+ local_has = _list_local_objects(self.objects_dir)
251
+ missing = to_fetch - local_has
252
+
253
+ copied = 0
254
+ for h in missing:
255
+ if _copy_object(remote_objects, self.objects_dir, h):
256
+ copied += 1
257
+
258
+ # Update remote-tracking refs (refs/remotes/<name>/<branch>), not local heads
259
+ if heads_dir.exists():
260
+ for f in heads_dir.rglob("*"):
261
+ if f.is_file():
262
+ branch_name = str(f.relative_to(heads_dir))
263
+ if not _ref_path_under_root(branch_name, refs.heads_dir):
264
+ continue
265
+ ch = f.read_text().strip()
266
+ if ch:
267
+ refs.set_remote_branch_commit(self.name, branch_name, ch)
268
+ if tags_dir.exists():
269
+ for f in tags_dir.rglob("*"):
270
+ if f.is_file():
271
+ tag_name = str(f.relative_to(tags_dir))
272
+ if not _ref_path_under_root(tag_name, refs.tags_dir):
273
+ continue
274
+ ch = f.read_text().strip()
275
+ if ch:
276
+ refs.create_tag(tag_name, ch)
277
+
278
+ return f"Fetched {copied} object(s) from {self.name}"