agmem 0.1.1__py3-none-any.whl → 0.1.3__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.3.dist-info}/METADATA +157 -16
- agmem-0.1.3.dist-info/RECORD +105 -0
- memvcs/__init__.py +1 -1
- memvcs/cli.py +45 -31
- memvcs/commands/__init__.py +9 -9
- memvcs/commands/add.py +83 -76
- memvcs/commands/audit.py +59 -0
- 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 +11 -1
- memvcs/commands/commit.py +40 -39
- memvcs/commands/daemon.py +109 -76
- memvcs/commands/decay.py +77 -0
- memvcs/commands/diff.py +56 -57
- memvcs/commands/distill.py +90 -0
- memvcs/commands/federated.py +53 -0
- memvcs/commands/fsck.py +86 -61
- memvcs/commands/garden.py +40 -35
- memvcs/commands/gc.py +51 -0
- memvcs/commands/graph.py +41 -48
- memvcs/commands/init.py +16 -24
- memvcs/commands/log.py +25 -40
- memvcs/commands/merge.py +69 -27
- memvcs/commands/pack.py +129 -0
- memvcs/commands/prove.py +66 -0
- memvcs/commands/pull.py +31 -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/resolve.py +130 -0
- 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 +110 -0
- memvcs/commands/when.py +115 -0
- memvcs/core/access_index.py +167 -0
- memvcs/core/audit.py +124 -0
- memvcs/core/config_loader.py +3 -1
- memvcs/core/consistency.py +214 -0
- memvcs/core/crypto_verify.py +280 -0
- memvcs/core/decay.py +185 -0
- memvcs/core/diff.py +158 -143
- memvcs/core/distiller.py +277 -0
- memvcs/core/encryption.py +169 -0
- memvcs/core/federated.py +86 -0
- memvcs/core/gardener.py +176 -145
- memvcs/core/hooks.py +48 -14
- memvcs/core/ipfs_remote.py +39 -0
- memvcs/core/knowledge_graph.py +135 -138
- memvcs/core/llm/__init__.py +10 -0
- memvcs/core/llm/anthropic_provider.py +50 -0
- memvcs/core/llm/base.py +27 -0
- memvcs/core/llm/factory.py +30 -0
- memvcs/core/llm/openai_provider.py +36 -0
- memvcs/core/merge.py +260 -170
- memvcs/core/objects.py +110 -101
- memvcs/core/pack.py +92 -0
- memvcs/core/pii_scanner.py +147 -146
- memvcs/core/privacy_budget.py +63 -0
- memvcs/core/refs.py +132 -115
- memvcs/core/remote.py +38 -0
- memvcs/core/repository.py +254 -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 +121 -0
- memvcs/core/test_runner.py +101 -93
- memvcs/core/trust.py +103 -0
- memvcs/core/vector_store.py +56 -36
- memvcs/core/zk_proofs.py +26 -0
- 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.3.dist-info}/WHEEL +0 -0
- {agmem-0.1.1.dist-info → agmem-0.1.3.dist-info}/entry_points.txt +0 -0
- {agmem-0.1.1.dist-info → agmem-0.1.3.dist-info}/licenses/LICENSE +0 -0
- {agmem-0.1.1.dist-info → agmem-0.1.3.dist-info}/top_level.txt +0 -0
memvcs/commands/daemon.py
CHANGED
|
@@ -14,34 +14,34 @@ from ..commands.base import require_repo
|
|
|
14
14
|
|
|
15
15
|
class DaemonCommand:
|
|
16
16
|
"""Control the auto-sync daemon."""
|
|
17
|
-
|
|
18
|
-
name =
|
|
19
|
-
help =
|
|
20
|
-
|
|
17
|
+
|
|
18
|
+
name = "daemon"
|
|
19
|
+
help = "Start/stop the auto-sync daemon for automatic commits"
|
|
20
|
+
|
|
21
21
|
@staticmethod
|
|
22
22
|
def add_arguments(parser: argparse.ArgumentParser):
|
|
23
23
|
parser.add_argument(
|
|
24
|
-
|
|
25
|
-
choices=[
|
|
26
|
-
help=
|
|
24
|
+
"action",
|
|
25
|
+
choices=["start", "stop", "status", "run"],
|
|
26
|
+
help="Daemon action: start, stop, status, or run (foreground)",
|
|
27
27
|
)
|
|
28
28
|
parser.add_argument(
|
|
29
|
-
|
|
29
|
+
"--debounce",
|
|
30
30
|
type=int,
|
|
31
31
|
default=30,
|
|
32
|
-
help=
|
|
32
|
+
help="Seconds to wait after changes before committing (default: 30)",
|
|
33
33
|
)
|
|
34
|
+
parser.add_argument("--pidfile", help="PID file path (default: .mem/daemon.pid)")
|
|
34
35
|
parser.add_argument(
|
|
35
|
-
|
|
36
|
-
help='PID file path (default: .mem/daemon.pid)'
|
|
36
|
+
"--distill", action="store_true", help="Run distillation pipeline nightly (with daemon)"
|
|
37
37
|
)
|
|
38
|
-
|
|
38
|
+
|
|
39
39
|
@staticmethod
|
|
40
40
|
def execute(args) -> int:
|
|
41
41
|
repo, code = require_repo()
|
|
42
42
|
if code != 0:
|
|
43
43
|
return code
|
|
44
|
-
|
|
44
|
+
|
|
45
45
|
if args.pidfile:
|
|
46
46
|
pid_file = Path(args.pidfile).resolve()
|
|
47
47
|
try:
|
|
@@ -50,21 +50,21 @@ class DaemonCommand:
|
|
|
50
50
|
print("Error: --pidfile must be under repository root")
|
|
51
51
|
return 1
|
|
52
52
|
else:
|
|
53
|
-
pid_file = repo.root /
|
|
54
|
-
|
|
55
|
-
if args.action ==
|
|
56
|
-
return DaemonCommand._start(repo, pid_file, args.debounce)
|
|
57
|
-
elif args.action ==
|
|
53
|
+
pid_file = repo.root / ".mem" / "daemon.pid"
|
|
54
|
+
|
|
55
|
+
if args.action == "start":
|
|
56
|
+
return DaemonCommand._start(repo, pid_file, args.debounce, args.distill)
|
|
57
|
+
elif args.action == "stop":
|
|
58
58
|
return DaemonCommand._stop(pid_file)
|
|
59
|
-
elif args.action ==
|
|
59
|
+
elif args.action == "status":
|
|
60
60
|
return DaemonCommand._status(pid_file)
|
|
61
|
-
elif args.action ==
|
|
62
|
-
return DaemonCommand._run(repo, args.debounce)
|
|
63
|
-
|
|
61
|
+
elif args.action == "run":
|
|
62
|
+
return DaemonCommand._run(repo, args.debounce, pid_file=pid_file, distill=args.distill)
|
|
63
|
+
|
|
64
64
|
return 1
|
|
65
|
-
|
|
65
|
+
|
|
66
66
|
@staticmethod
|
|
67
|
-
def _start(repo, pid_file: Path, debounce: int) -> int:
|
|
67
|
+
def _start(repo, pid_file: Path, debounce: int, distill: bool = False) -> int:
|
|
68
68
|
"""Start daemon in background."""
|
|
69
69
|
# Check if already running
|
|
70
70
|
if pid_file.exists():
|
|
@@ -75,13 +75,13 @@ class DaemonCommand:
|
|
|
75
75
|
return 1
|
|
76
76
|
except OSError:
|
|
77
77
|
pid_file.unlink()
|
|
78
|
-
|
|
78
|
+
|
|
79
79
|
# Fork to background (Unix only)
|
|
80
|
-
if os.name !=
|
|
80
|
+
if os.name != "posix":
|
|
81
81
|
print("Background daemon not supported on this platform.")
|
|
82
82
|
print("Use 'agmem daemon run' to run in foreground.")
|
|
83
83
|
return 1
|
|
84
|
-
|
|
84
|
+
|
|
85
85
|
# First fork
|
|
86
86
|
try:
|
|
87
87
|
pid = os.fork()
|
|
@@ -92,11 +92,11 @@ class DaemonCommand:
|
|
|
92
92
|
except OSError as e:
|
|
93
93
|
print(f"Fork failed: {e}")
|
|
94
94
|
return 1
|
|
95
|
-
|
|
95
|
+
|
|
96
96
|
# Decouple from parent
|
|
97
97
|
os.setsid()
|
|
98
98
|
os.umask(0)
|
|
99
|
-
|
|
99
|
+
|
|
100
100
|
# Second fork
|
|
101
101
|
try:
|
|
102
102
|
pid = os.fork()
|
|
@@ -104,26 +104,26 @@ class DaemonCommand:
|
|
|
104
104
|
sys.exit(0)
|
|
105
105
|
except OSError:
|
|
106
106
|
sys.exit(1)
|
|
107
|
-
|
|
107
|
+
|
|
108
108
|
# Write PID file
|
|
109
109
|
pid_file.write_text(str(os.getpid()))
|
|
110
|
-
|
|
110
|
+
|
|
111
111
|
# Redirect stdio
|
|
112
112
|
sys.stdout.flush()
|
|
113
113
|
sys.stderr.flush()
|
|
114
|
-
|
|
115
|
-
# Run daemon
|
|
116
|
-
return DaemonCommand._run(repo, debounce, pid_file)
|
|
117
|
-
|
|
114
|
+
|
|
115
|
+
# Run daemon in background
|
|
116
|
+
return DaemonCommand._run(repo, debounce, pid_file, distill)
|
|
117
|
+
|
|
118
118
|
@staticmethod
|
|
119
119
|
def _stop(pid_file: Path) -> int:
|
|
120
120
|
"""Stop running daemon."""
|
|
121
121
|
if not pid_file.exists():
|
|
122
122
|
print("No daemon running")
|
|
123
123
|
return 0
|
|
124
|
-
|
|
124
|
+
|
|
125
125
|
pid = int(pid_file.read_text().strip())
|
|
126
|
-
|
|
126
|
+
|
|
127
127
|
try:
|
|
128
128
|
os.kill(pid, signal.SIGTERM)
|
|
129
129
|
print(f"Stopped daemon (PID: {pid})")
|
|
@@ -133,16 +133,16 @@ class DaemonCommand:
|
|
|
133
133
|
print(f"Could not stop daemon: {e}")
|
|
134
134
|
pid_file.unlink()
|
|
135
135
|
return 1
|
|
136
|
-
|
|
136
|
+
|
|
137
137
|
@staticmethod
|
|
138
138
|
def _status(pid_file: Path) -> int:
|
|
139
139
|
"""Show daemon status."""
|
|
140
140
|
if not pid_file.exists():
|
|
141
141
|
print("Daemon is not running")
|
|
142
142
|
return 0
|
|
143
|
-
|
|
143
|
+
|
|
144
144
|
pid = int(pid_file.read_text().strip())
|
|
145
|
-
|
|
145
|
+
|
|
146
146
|
try:
|
|
147
147
|
os.kill(pid, 0)
|
|
148
148
|
print(f"Daemon is running (PID: {pid})")
|
|
@@ -151,9 +151,9 @@ class DaemonCommand:
|
|
|
151
151
|
print("Daemon is not running (stale PID file)")
|
|
152
152
|
pid_file.unlink()
|
|
153
153
|
return 0
|
|
154
|
-
|
|
154
|
+
|
|
155
155
|
@staticmethod
|
|
156
|
-
def _run(repo, debounce: int, pid_file: Path = None) -> int:
|
|
156
|
+
def _run(repo, debounce: int, pid_file: Path = None, distill: bool = False) -> int:
|
|
157
157
|
"""Run daemon in foreground."""
|
|
158
158
|
try:
|
|
159
159
|
from watchdog.observers import Observer
|
|
@@ -161,84 +161,120 @@ class DaemonCommand:
|
|
|
161
161
|
except ImportError:
|
|
162
162
|
print("Daemon requires watchdog. Install with: pip install agmem[daemon]")
|
|
163
163
|
return 1
|
|
164
|
-
|
|
165
|
-
current_dir = repo.root /
|
|
166
|
-
|
|
164
|
+
|
|
165
|
+
current_dir = repo.root / "current"
|
|
166
|
+
|
|
167
167
|
if not current_dir.exists():
|
|
168
168
|
print("No current/ directory to watch")
|
|
169
169
|
return 1
|
|
170
|
-
|
|
170
|
+
|
|
171
171
|
class MemoryFileHandler(FileSystemEventHandler):
|
|
172
172
|
def __init__(self):
|
|
173
173
|
self.last_change = 0
|
|
174
174
|
self.pending = False
|
|
175
|
-
|
|
175
|
+
|
|
176
176
|
def on_any_event(self, event):
|
|
177
177
|
# Ignore .mem and hidden files
|
|
178
|
-
if
|
|
178
|
+
if ".mem" in event.src_path or "/." in event.src_path:
|
|
179
179
|
return
|
|
180
|
-
|
|
180
|
+
|
|
181
181
|
# Ignore directories
|
|
182
182
|
if event.is_directory:
|
|
183
183
|
return
|
|
184
|
-
|
|
184
|
+
|
|
185
185
|
self.last_change = time.time()
|
|
186
186
|
self.pending = True
|
|
187
|
-
|
|
187
|
+
|
|
188
188
|
handler = MemoryFileHandler()
|
|
189
189
|
observer = Observer()
|
|
190
190
|
observer.schedule(handler, str(current_dir), recursive=True)
|
|
191
191
|
observer.start()
|
|
192
|
-
|
|
192
|
+
|
|
193
193
|
print(f"Watching {current_dir} (debounce: {debounce}s)")
|
|
194
194
|
print("Press Ctrl+C to stop")
|
|
195
|
-
|
|
195
|
+
|
|
196
196
|
# Handle signals
|
|
197
197
|
running = True
|
|
198
|
-
|
|
198
|
+
|
|
199
199
|
def signal_handler(signum, frame):
|
|
200
200
|
nonlocal running
|
|
201
201
|
running = False
|
|
202
|
-
|
|
202
|
+
|
|
203
203
|
signal.signal(signal.SIGTERM, signal_handler)
|
|
204
204
|
signal.signal(signal.SIGINT, signal_handler)
|
|
205
|
-
|
|
205
|
+
|
|
206
|
+
# Health monitoring: periodic integrity check (configurable interval)
|
|
207
|
+
last_health_check = 0
|
|
208
|
+
health_check_interval = 3600 # 1 hour
|
|
209
|
+
|
|
206
210
|
try:
|
|
207
211
|
while running:
|
|
208
212
|
time.sleep(1)
|
|
209
|
-
|
|
213
|
+
|
|
214
|
+
# Periodic health check (Merkle/signature, optional)
|
|
215
|
+
if (
|
|
216
|
+
health_check_interval
|
|
217
|
+
and (time.time() - last_health_check) >= health_check_interval
|
|
218
|
+
):
|
|
219
|
+
try:
|
|
220
|
+
from ..core.crypto_verify import verify_commit, load_public_key
|
|
221
|
+
|
|
222
|
+
head = repo.refs.get_branch_commit(
|
|
223
|
+
repo.refs.get_current_branch() or "main"
|
|
224
|
+
) or (repo.refs.get_head() or {}).get("value")
|
|
225
|
+
if head:
|
|
226
|
+
ok, err = verify_commit(
|
|
227
|
+
repo.object_store,
|
|
228
|
+
head,
|
|
229
|
+
load_public_key(repo.mem_dir),
|
|
230
|
+
mem_dir=repo.mem_dir,
|
|
231
|
+
)
|
|
232
|
+
if not ok and err and "tampered" in (err or "").lower():
|
|
233
|
+
sys.stderr.write(f"Health check: {err}\n")
|
|
234
|
+
except Exception:
|
|
235
|
+
pass
|
|
236
|
+
last_health_check = time.time()
|
|
237
|
+
|
|
210
238
|
if handler.pending:
|
|
211
239
|
elapsed = time.time() - handler.last_change
|
|
212
240
|
if elapsed >= debounce:
|
|
213
241
|
# Auto-commit
|
|
214
242
|
DaemonCommand._auto_commit(repo)
|
|
243
|
+
if distill:
|
|
244
|
+
try:
|
|
245
|
+
from ..core.distiller import Distiller, DistillerConfig
|
|
246
|
+
|
|
247
|
+
d = Distiller(repo, DistillerConfig(create_safety_branch=False))
|
|
248
|
+
d.run()
|
|
249
|
+
except Exception:
|
|
250
|
+
pass
|
|
215
251
|
handler.pending = False
|
|
216
252
|
finally:
|
|
217
253
|
observer.stop()
|
|
218
254
|
observer.join()
|
|
219
|
-
|
|
255
|
+
|
|
220
256
|
if pid_file and pid_file.exists():
|
|
221
257
|
pid_file.unlink()
|
|
222
|
-
|
|
258
|
+
|
|
223
259
|
print("Daemon stopped")
|
|
224
260
|
return 0
|
|
225
|
-
|
|
261
|
+
|
|
226
262
|
@staticmethod
|
|
227
263
|
def _auto_commit(repo):
|
|
228
264
|
"""Perform automatic commit."""
|
|
229
265
|
from datetime import datetime
|
|
230
|
-
|
|
266
|
+
|
|
231
267
|
try:
|
|
232
268
|
# Check for changes
|
|
233
269
|
status = repo.get_status()
|
|
234
|
-
|
|
235
|
-
if not status.get(
|
|
270
|
+
|
|
271
|
+
if not status.get("modified") and not status.get("untracked"):
|
|
236
272
|
return
|
|
237
|
-
|
|
273
|
+
|
|
238
274
|
# Stage all changes in current/ (validate path stays under current/)
|
|
239
|
-
current_dir = repo.root /
|
|
240
|
-
for memory_file in current_dir.glob(
|
|
241
|
-
if memory_file.is_file() and
|
|
275
|
+
current_dir = repo.root / "current"
|
|
276
|
+
for memory_file in current_dir.glob("**/*"):
|
|
277
|
+
if memory_file.is_file() and ".mem" not in str(memory_file):
|
|
242
278
|
try:
|
|
243
279
|
rel_path = memory_file.relative_to(current_dir)
|
|
244
280
|
except ValueError:
|
|
@@ -250,18 +286,15 @@ class DaemonCommand:
|
|
|
250
286
|
repo.stage_file(rel_str)
|
|
251
287
|
except Exception:
|
|
252
288
|
pass
|
|
253
|
-
|
|
289
|
+
|
|
254
290
|
# Commit
|
|
255
|
-
timestamp = datetime.utcnow().strftime(
|
|
256
|
-
repo.commit(
|
|
257
|
-
|
|
258
|
-
{'auto_commit': True}
|
|
259
|
-
)
|
|
260
|
-
|
|
291
|
+
timestamp = datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S")
|
|
292
|
+
repo.commit(f"auto: update memory state ({timestamp})", {"auto_commit": True})
|
|
293
|
+
|
|
261
294
|
print(f"[{timestamp}] Auto-committed changes")
|
|
262
|
-
|
|
295
|
+
|
|
263
296
|
except Exception as e:
|
|
264
297
|
# Write conflict lock if there's an issue
|
|
265
|
-
conflict_file = repo.root /
|
|
298
|
+
conflict_file = repo.root / ".mem" / "CONFLICT.lock"
|
|
266
299
|
conflict_file.write_text(f"Auto-commit failed: {e}")
|
|
267
300
|
print(f"Warning: Auto-commit failed, wrote CONFLICT.lock: {e}")
|
memvcs/commands/decay.py
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""
|
|
2
|
+
agmem decay - Memory decay and forgetting.
|
|
3
|
+
|
|
4
|
+
Archives low-importance, rarely-accessed memories.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import argparse
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from ..commands.base import require_repo
|
|
11
|
+
from ..core.decay import DecayEngine, DecayConfig
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class DecayCommand:
|
|
15
|
+
"""Memory decay - archive low-importance memories."""
|
|
16
|
+
|
|
17
|
+
name = "decay"
|
|
18
|
+
help = "Archive low-importance, old episodic memories (decay/forgetting)"
|
|
19
|
+
|
|
20
|
+
@staticmethod
|
|
21
|
+
def add_arguments(parser: argparse.ArgumentParser):
|
|
22
|
+
parser.add_argument(
|
|
23
|
+
"--dry-run",
|
|
24
|
+
action="store_true",
|
|
25
|
+
help="Preview what would be archived without making changes",
|
|
26
|
+
)
|
|
27
|
+
parser.add_argument(
|
|
28
|
+
"--apply",
|
|
29
|
+
action="store_true",
|
|
30
|
+
help="Actually archive the memories",
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
@staticmethod
|
|
34
|
+
def execute(args) -> int:
|
|
35
|
+
repo, code = require_repo()
|
|
36
|
+
if code != 0:
|
|
37
|
+
return code
|
|
38
|
+
|
|
39
|
+
config = DecayConfig()
|
|
40
|
+
repo_config = repo.get_config()
|
|
41
|
+
decay_cfg = repo_config.get("decay", {})
|
|
42
|
+
if decay_cfg:
|
|
43
|
+
config.episodic_half_life_days = decay_cfg.get(
|
|
44
|
+
"episodic_half_life_days", config.episodic_half_life_days
|
|
45
|
+
)
|
|
46
|
+
config.semantic_min_importance = decay_cfg.get(
|
|
47
|
+
"semantic_min_importance", config.semantic_min_importance
|
|
48
|
+
)
|
|
49
|
+
config.access_count_threshold = decay_cfg.get(
|
|
50
|
+
"access_count_threshold", config.access_count_threshold
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
engine = DecayEngine(repo, config)
|
|
54
|
+
candidates = engine.get_decay_candidates()
|
|
55
|
+
|
|
56
|
+
if not candidates:
|
|
57
|
+
print("No memories eligible for decay.")
|
|
58
|
+
return 0
|
|
59
|
+
|
|
60
|
+
print(f"Found {len(candidates)} memory(ies) eligible for decay:")
|
|
61
|
+
for c in candidates[:20]:
|
|
62
|
+
print(f" - {c.path} (score: {c.decay_score:.2f}) - {c.reason}")
|
|
63
|
+
if len(candidates) > 20:
|
|
64
|
+
print(f" ... and {len(candidates) - 20} more")
|
|
65
|
+
|
|
66
|
+
if args.dry_run:
|
|
67
|
+
print("\nDry run - no changes made. Use --apply to archive.")
|
|
68
|
+
return 0
|
|
69
|
+
|
|
70
|
+
if not args.apply:
|
|
71
|
+
print("\nUse --apply to archive these memories.")
|
|
72
|
+
return 0
|
|
73
|
+
|
|
74
|
+
count = engine.apply_decay(candidates)
|
|
75
|
+
print(f"\nArchived {count} memory(ies) to .mem/forgetting/")
|
|
76
|
+
print("Use 'agmem resurrect <path>' to restore.")
|
|
77
|
+
return 0
|
memvcs/commands/diff.py
CHANGED
|
@@ -13,33 +13,35 @@ from ..core.repository import Repository
|
|
|
13
13
|
|
|
14
14
|
class DiffCommand:
|
|
15
15
|
"""Show differences between commits."""
|
|
16
|
-
|
|
17
|
-
name =
|
|
18
|
-
help =
|
|
19
|
-
|
|
16
|
+
|
|
17
|
+
name = "diff"
|
|
18
|
+
help = "Show changes between commits, commit and working tree, etc."
|
|
19
|
+
|
|
20
20
|
@staticmethod
|
|
21
21
|
def add_arguments(parser: argparse.ArgumentParser):
|
|
22
|
+
parser.add_argument("ref1", nargs="?", help="First commit/branch to compare")
|
|
23
|
+
parser.add_argument("ref2", nargs="?", help="Second commit/branch to compare")
|
|
22
24
|
parser.add_argument(
|
|
23
|
-
|
|
24
|
-
nargs='?',
|
|
25
|
-
help='First commit/branch to compare'
|
|
25
|
+
"--cached", "--staged", action="store_true", help="Show changes staged for commit"
|
|
26
26
|
)
|
|
27
27
|
parser.add_argument(
|
|
28
|
-
|
|
29
|
-
nargs='?',
|
|
30
|
-
help='Second commit/branch to compare'
|
|
28
|
+
"--stat", action="store_true", help="Show diffstat instead of full diff"
|
|
31
29
|
)
|
|
32
30
|
parser.add_argument(
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
31
|
+
"--from",
|
|
32
|
+
"--from-ref",
|
|
33
|
+
dest="from_ref",
|
|
34
|
+
metavar="REF",
|
|
35
|
+
help="Start ref for diff (supports ISO dates)",
|
|
36
36
|
)
|
|
37
37
|
parser.add_argument(
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
38
|
+
"--to",
|
|
39
|
+
"--to-ref",
|
|
40
|
+
dest="to_ref",
|
|
41
|
+
metavar="REF",
|
|
42
|
+
help="End ref for diff (supports ISO dates)",
|
|
41
43
|
)
|
|
42
|
-
|
|
44
|
+
|
|
43
45
|
@staticmethod
|
|
44
46
|
def execute(args) -> int:
|
|
45
47
|
repo, code = require_repo()
|
|
@@ -47,7 +49,7 @@ class DiffCommand:
|
|
|
47
49
|
return code
|
|
48
50
|
|
|
49
51
|
engine = DiffEngine(repo.object_store)
|
|
50
|
-
|
|
52
|
+
|
|
51
53
|
# Determine what to diff
|
|
52
54
|
if args.cached:
|
|
53
55
|
# Diff staged changes against HEAD
|
|
@@ -55,103 +57,100 @@ class DiffCommand:
|
|
|
55
57
|
if not head_commit:
|
|
56
58
|
print("No commits yet. Nothing to diff.")
|
|
57
59
|
return 0
|
|
58
|
-
|
|
60
|
+
|
|
59
61
|
staged_files = repo.staging.get_staged_files()
|
|
60
62
|
if not staged_files:
|
|
61
63
|
print("No staged changes.")
|
|
62
64
|
return 0
|
|
63
|
-
|
|
65
|
+
|
|
64
66
|
# Get staged content
|
|
65
67
|
working_files = {}
|
|
66
68
|
for path, sf in staged_files.items():
|
|
67
69
|
from ..core.objects import Blob
|
|
70
|
+
|
|
68
71
|
blob = Blob.load(repo.object_store, sf.blob_hash)
|
|
69
72
|
if blob:
|
|
70
73
|
working_files[path] = blob.content
|
|
71
|
-
|
|
72
|
-
tree_diff = engine.diff_working_dir(
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
)
|
|
76
|
-
|
|
77
|
-
print(engine.format_diff(tree_diff, 'HEAD', 'staged'))
|
|
74
|
+
|
|
75
|
+
tree_diff = engine.diff_working_dir(head_commit.store(repo.object_store), working_files)
|
|
76
|
+
|
|
77
|
+
print(engine.format_diff(tree_diff, "HEAD", "staged"))
|
|
78
78
|
return 0
|
|
79
|
-
|
|
80
|
-
# Diff between two refs
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
79
|
+
|
|
80
|
+
# Diff between two refs (--from/--to override ref1/ref2)
|
|
81
|
+
ref1 = getattr(args, "from_ref", None) or args.ref1
|
|
82
|
+
ref2 = getattr(args, "to_ref", None) or args.ref2
|
|
83
|
+
if ref1 and ref2:
|
|
84
|
+
commit1 = repo.resolve_ref(ref1)
|
|
85
|
+
commit2 = repo.resolve_ref(ref2)
|
|
86
|
+
|
|
85
87
|
if not commit1:
|
|
86
|
-
print(f"Error: Unknown revision: {
|
|
88
|
+
print(f"Error: Unknown revision: {ref1}")
|
|
87
89
|
return 1
|
|
88
90
|
if not commit2:
|
|
89
|
-
print(f"Error: Unknown revision: {
|
|
91
|
+
print(f"Error: Unknown revision: {ref2}")
|
|
90
92
|
return 1
|
|
91
|
-
|
|
93
|
+
|
|
92
94
|
tree_diff = engine.diff_commits(commit1, commit2)
|
|
93
|
-
|
|
95
|
+
|
|
94
96
|
if args.stat:
|
|
95
97
|
print(f" {tree_diff.added_count} file(s) added")
|
|
96
98
|
print(f" {tree_diff.deleted_count} file(s) deleted")
|
|
97
99
|
print(f" {tree_diff.modified_count} file(s) modified")
|
|
98
100
|
else:
|
|
99
|
-
print(engine.format_diff(tree_diff,
|
|
100
|
-
|
|
101
|
+
print(engine.format_diff(tree_diff, ref1, ref2))
|
|
102
|
+
|
|
101
103
|
return 0
|
|
102
|
-
|
|
104
|
+
|
|
103
105
|
# Diff working tree against a ref
|
|
104
106
|
if args.ref1:
|
|
105
107
|
commit_hash = repo.resolve_ref(args.ref1)
|
|
106
108
|
if not commit_hash:
|
|
107
109
|
print(f"Error: Unknown revision: {args.ref1}")
|
|
108
110
|
return 1
|
|
109
|
-
|
|
111
|
+
|
|
110
112
|
# Get working directory files
|
|
111
113
|
working_files = {}
|
|
112
114
|
for root, dirs, files in os.walk(repo.current_dir):
|
|
113
|
-
dirs[:] = [d for d in dirs if not d.startswith(
|
|
115
|
+
dirs[:] = [d for d in dirs if not d.startswith(".")]
|
|
114
116
|
for filename in files:
|
|
115
117
|
full_path = Path(root) / filename
|
|
116
118
|
rel_path = str(full_path.relative_to(repo.current_dir))
|
|
117
119
|
working_files[rel_path] = full_path.read_bytes()
|
|
118
|
-
|
|
120
|
+
|
|
119
121
|
tree_diff = engine.diff_working_dir(commit_hash, working_files)
|
|
120
|
-
|
|
122
|
+
|
|
121
123
|
if args.stat:
|
|
122
124
|
print(f" {tree_diff.added_count} file(s) added")
|
|
123
125
|
print(f" {tree_diff.deleted_count} file(s) deleted")
|
|
124
126
|
print(f" {tree_diff.modified_count} file(s) modified")
|
|
125
127
|
else:
|
|
126
|
-
print(engine.format_diff(tree_diff, args.ref1,
|
|
127
|
-
|
|
128
|
+
print(engine.format_diff(tree_diff, args.ref1, "working"))
|
|
129
|
+
|
|
128
130
|
return 0
|
|
129
|
-
|
|
131
|
+
|
|
130
132
|
# Default: diff working tree against HEAD
|
|
131
133
|
head_commit = repo.get_head_commit()
|
|
132
134
|
if not head_commit:
|
|
133
135
|
print("No commits yet. Nothing to diff.")
|
|
134
136
|
return 0
|
|
135
|
-
|
|
137
|
+
|
|
136
138
|
# Get working directory files
|
|
137
139
|
working_files = {}
|
|
138
140
|
for root, dirs, files in os.walk(repo.current_dir):
|
|
139
|
-
dirs[:] = [d for d in dirs if not d.startswith(
|
|
141
|
+
dirs[:] = [d for d in dirs if not d.startswith(".")]
|
|
140
142
|
for filename in files:
|
|
141
143
|
full_path = Path(root) / filename
|
|
142
144
|
rel_path = str(full_path.relative_to(repo.current_dir))
|
|
143
145
|
working_files[rel_path] = full_path.read_bytes()
|
|
144
|
-
|
|
145
|
-
tree_diff = engine.diff_working_dir(
|
|
146
|
-
|
|
147
|
-
working_files
|
|
148
|
-
)
|
|
149
|
-
|
|
146
|
+
|
|
147
|
+
tree_diff = engine.diff_working_dir(head_commit.store(repo.object_store), working_files)
|
|
148
|
+
|
|
150
149
|
if args.stat:
|
|
151
150
|
print(f" {tree_diff.added_count} file(s) added")
|
|
152
151
|
print(f" {tree_diff.deleted_count} file(s) deleted")
|
|
153
152
|
print(f" {tree_diff.modified_count} file(s) modified")
|
|
154
153
|
else:
|
|
155
|
-
print(engine.format_diff(tree_diff,
|
|
156
|
-
|
|
154
|
+
print(engine.format_diff(tree_diff, "HEAD", "working"))
|
|
155
|
+
|
|
157
156
|
return 0
|