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/add.py
CHANGED
|
@@ -10,84 +10,82 @@ from ..core.repository import Repository
|
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
# Default allowed file extensions for memory files
|
|
13
|
-
DEFAULT_ALLOWED_EXTENSIONS = {
|
|
13
|
+
DEFAULT_ALLOWED_EXTENSIONS = {".md", ".txt", ".json", ".yaml", ".yml"}
|
|
14
14
|
|
|
15
15
|
# Binary file signatures (magic bytes) to detect binary files
|
|
16
16
|
BINARY_SIGNATURES = [
|
|
17
|
-
b
|
|
18
|
-
b
|
|
19
|
-
b
|
|
20
|
-
b
|
|
21
|
-
b
|
|
22
|
-
b
|
|
23
|
-
b
|
|
24
|
-
b
|
|
25
|
-
b
|
|
17
|
+
b"\x89PNG", # PNG
|
|
18
|
+
b"\xff\xd8\xff", # JPEG
|
|
19
|
+
b"GIF8", # GIF
|
|
20
|
+
b"%PDF", # PDF
|
|
21
|
+
b"PK\x03\x04", # ZIP
|
|
22
|
+
b"\x1f\x8b", # GZIP
|
|
23
|
+
b"BM", # BMP
|
|
24
|
+
b"\x00\x00\x01\x00", # ICO
|
|
25
|
+
b"RIFF", # WAV, AVI, etc.
|
|
26
26
|
]
|
|
27
27
|
|
|
28
28
|
|
|
29
29
|
class AddCommand:
|
|
30
30
|
"""Add files to the staging area."""
|
|
31
|
-
|
|
32
|
-
name =
|
|
33
|
-
help =
|
|
34
|
-
|
|
31
|
+
|
|
32
|
+
name = "add"
|
|
33
|
+
help = "Add memory files to staging area"
|
|
34
|
+
|
|
35
35
|
@staticmethod
|
|
36
36
|
def add_arguments(parser: argparse.ArgumentParser):
|
|
37
|
+
parser.add_argument("paths", nargs="+", help="Files or directories to stage")
|
|
37
38
|
parser.add_argument(
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
parser.add_argument(
|
|
43
|
-
'--all', '-A',
|
|
44
|
-
action='store_true',
|
|
45
|
-
help='Stage all changes (including modifications and deletions)'
|
|
39
|
+
"--all",
|
|
40
|
+
"-A",
|
|
41
|
+
action="store_true",
|
|
42
|
+
help="Stage all changes (including modifications and deletions)",
|
|
46
43
|
)
|
|
47
44
|
parser.add_argument(
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
45
|
+
"--force",
|
|
46
|
+
"-f",
|
|
47
|
+
action="store_true",
|
|
48
|
+
help="Force add even if file type is not recommended",
|
|
51
49
|
)
|
|
52
50
|
parser.add_argument(
|
|
53
|
-
|
|
54
|
-
action=
|
|
55
|
-
help=
|
|
51
|
+
"--allow-binary",
|
|
52
|
+
action="store_true",
|
|
53
|
+
help="Allow staging binary files (not recommended)",
|
|
56
54
|
)
|
|
57
|
-
|
|
55
|
+
|
|
58
56
|
@staticmethod
|
|
59
57
|
def _is_binary_file(filepath: Path) -> bool:
|
|
60
58
|
"""Check if a file is binary by looking at magic bytes."""
|
|
61
59
|
try:
|
|
62
|
-
with open(filepath,
|
|
60
|
+
with open(filepath, "rb") as f:
|
|
63
61
|
header = f.read(16)
|
|
64
|
-
|
|
62
|
+
|
|
65
63
|
for signature in BINARY_SIGNATURES:
|
|
66
64
|
if header.startswith(signature):
|
|
67
65
|
return True
|
|
68
|
-
|
|
66
|
+
|
|
69
67
|
# Also check for null bytes (common in binary files)
|
|
70
|
-
if b
|
|
68
|
+
if b"\x00" in header:
|
|
71
69
|
return True
|
|
72
|
-
|
|
70
|
+
|
|
73
71
|
return False
|
|
74
72
|
except Exception:
|
|
75
73
|
return False
|
|
76
|
-
|
|
74
|
+
|
|
77
75
|
@staticmethod
|
|
78
76
|
def _is_allowed_extension(filepath: Path, config: dict) -> bool:
|
|
79
77
|
"""Check if file extension is in allowed list."""
|
|
80
|
-
allowed = config.get(
|
|
78
|
+
allowed = config.get("allowed_extensions", list(DEFAULT_ALLOWED_EXTENSIONS))
|
|
81
79
|
allowed_set = set(allowed)
|
|
82
|
-
|
|
80
|
+
|
|
83
81
|
ext = filepath.suffix.lower()
|
|
84
82
|
return ext in allowed_set or not ext # Allow files without extension
|
|
85
|
-
|
|
83
|
+
|
|
86
84
|
@staticmethod
|
|
87
85
|
def _validate_file(filepath: Path, config: dict, force: bool, allow_binary: bool) -> tuple:
|
|
88
86
|
"""
|
|
89
87
|
Validate a file for staging.
|
|
90
|
-
|
|
88
|
+
|
|
91
89
|
Returns:
|
|
92
90
|
Tuple of (is_valid, warning_message)
|
|
93
91
|
"""
|
|
@@ -96,13 +94,16 @@ class AddCommand:
|
|
|
96
94
|
if allow_binary:
|
|
97
95
|
return True, f"Warning: {filepath} appears to be binary"
|
|
98
96
|
else:
|
|
99
|
-
return
|
|
100
|
-
|
|
97
|
+
return (
|
|
98
|
+
False,
|
|
99
|
+
f"Rejected: {filepath} is a binary file. Use --allow-binary to override.",
|
|
100
|
+
)
|
|
101
|
+
|
|
101
102
|
# Check extension
|
|
102
103
|
if not AddCommand._is_allowed_extension(filepath, config):
|
|
103
|
-
ext = filepath.suffix or
|
|
104
|
-
allowed = config.get(
|
|
105
|
-
|
|
104
|
+
ext = filepath.suffix or "(no extension)"
|
|
105
|
+
allowed = config.get("allowed_extensions", list(DEFAULT_ALLOWED_EXTENSIONS))
|
|
106
|
+
|
|
106
107
|
if force:
|
|
107
108
|
return True, f"Warning: {filepath} has extension '{ext}' which may not be optimal"
|
|
108
109
|
else:
|
|
@@ -111,9 +112,9 @@ class AddCommand:
|
|
|
111
112
|
f" Recommended: {', '.join(sorted(allowed))}\n"
|
|
112
113
|
f" Use --force to override."
|
|
113
114
|
)
|
|
114
|
-
|
|
115
|
+
|
|
115
116
|
return True, None
|
|
116
|
-
|
|
117
|
+
|
|
117
118
|
@staticmethod
|
|
118
119
|
def execute(args) -> int:
|
|
119
120
|
repo, code = require_repo()
|
|
@@ -123,19 +124,19 @@ class AddCommand:
|
|
|
123
124
|
staged_count = 0
|
|
124
125
|
rejected_count = 0
|
|
125
126
|
config = repo.get_config()
|
|
126
|
-
|
|
127
|
+
|
|
127
128
|
for path_str in args.paths:
|
|
128
129
|
path = Path(path_str)
|
|
129
|
-
|
|
130
|
+
|
|
130
131
|
# Handle '.' to stage all
|
|
131
|
-
if path_str ==
|
|
132
|
+
if path_str == ".":
|
|
132
133
|
staged, rejected = AddCommand._stage_directory_with_validation(
|
|
133
134
|
repo, None, config, args.force, args.allow_binary
|
|
134
135
|
)
|
|
135
136
|
staged_count += staged
|
|
136
137
|
rejected_count += rejected
|
|
137
138
|
continue
|
|
138
|
-
|
|
139
|
+
|
|
139
140
|
# Resolve path relative to current/
|
|
140
141
|
if path.is_absolute():
|
|
141
142
|
try:
|
|
@@ -156,85 +157,91 @@ class AddCommand:
|
|
|
156
157
|
else:
|
|
157
158
|
print(f"Error: Path not found: {path}")
|
|
158
159
|
continue
|
|
159
|
-
|
|
160
|
+
|
|
160
161
|
full_path = repo.current_dir / rel_path
|
|
161
|
-
|
|
162
|
+
|
|
162
163
|
if not full_path.exists():
|
|
163
164
|
print(f"Error: Path not found: {path}")
|
|
164
165
|
continue
|
|
165
|
-
|
|
166
|
+
|
|
166
167
|
if full_path.is_file():
|
|
167
168
|
# Validate file
|
|
168
169
|
is_valid, message = AddCommand._validate_file(
|
|
169
170
|
full_path, config, args.force, args.allow_binary
|
|
170
171
|
)
|
|
171
|
-
|
|
172
|
+
|
|
172
173
|
if not is_valid:
|
|
173
174
|
print(message)
|
|
174
175
|
rejected_count += 1
|
|
175
176
|
continue
|
|
176
|
-
|
|
177
|
+
|
|
177
178
|
if message: # Warning
|
|
178
179
|
print(message)
|
|
179
|
-
|
|
180
|
+
|
|
180
181
|
try:
|
|
181
182
|
blob_hash = repo.stage_file(str(rel_path))
|
|
182
183
|
print(f" staged: {rel_path}")
|
|
183
184
|
staged_count += 1
|
|
184
185
|
except Exception as e:
|
|
185
186
|
print(f"Error staging {rel_path}: {e}")
|
|
186
|
-
|
|
187
|
+
|
|
187
188
|
elif full_path.is_dir():
|
|
188
189
|
staged, rejected = AddCommand._stage_directory_with_validation(
|
|
189
190
|
repo, str(rel_path), config, args.force, args.allow_binary
|
|
190
191
|
)
|
|
191
192
|
staged_count += staged
|
|
192
193
|
rejected_count += rejected
|
|
193
|
-
|
|
194
|
+
|
|
194
195
|
if staged_count > 0 or rejected_count > 0:
|
|
195
196
|
print(f"\nStaged {staged_count} file(s)")
|
|
196
197
|
if rejected_count > 0:
|
|
197
198
|
print(f"Rejected {rejected_count} file(s) - use --force to override")
|
|
198
199
|
if staged_count > 0:
|
|
199
200
|
print("Run 'agmem commit -m \"message\"' to save snapshot")
|
|
201
|
+
try:
|
|
202
|
+
from ..core.audit import append_audit
|
|
203
|
+
|
|
204
|
+
append_audit(repo.mem_dir, "add", {"staged_count": staged_count})
|
|
205
|
+
except Exception:
|
|
206
|
+
pass
|
|
200
207
|
else:
|
|
201
208
|
print("No files staged")
|
|
202
|
-
|
|
209
|
+
|
|
203
210
|
return 0
|
|
204
|
-
|
|
211
|
+
|
|
205
212
|
@staticmethod
|
|
206
|
-
def _stage_directory_with_validation(
|
|
213
|
+
def _stage_directory_with_validation(
|
|
214
|
+
repo, subdir: str, config: dict, force: bool, allow_binary: bool
|
|
215
|
+
) -> tuple:
|
|
207
216
|
"""
|
|
208
217
|
Stage a directory with file validation.
|
|
209
|
-
|
|
218
|
+
|
|
210
219
|
Returns:
|
|
211
220
|
Tuple of (staged_count, rejected_count)
|
|
212
221
|
"""
|
|
213
222
|
staged_count = 0
|
|
214
223
|
rejected_count = 0
|
|
215
|
-
|
|
224
|
+
|
|
216
225
|
if subdir:
|
|
217
226
|
dir_path = repo.current_dir / subdir
|
|
218
227
|
else:
|
|
219
228
|
dir_path = repo.current_dir
|
|
220
|
-
|
|
229
|
+
|
|
221
230
|
if not dir_path.exists():
|
|
222
231
|
return 0, 0
|
|
223
|
-
|
|
224
|
-
for file_path in dir_path.rglob(
|
|
232
|
+
|
|
233
|
+
for file_path in dir_path.rglob("*"):
|
|
225
234
|
if not file_path.is_file():
|
|
226
235
|
continue
|
|
227
|
-
|
|
236
|
+
|
|
228
237
|
# Skip hidden files and .mem directory
|
|
229
238
|
rel_to_current = file_path.relative_to(repo.current_dir)
|
|
230
|
-
if any(part.startswith(
|
|
239
|
+
if any(part.startswith(".") for part in rel_to_current.parts):
|
|
231
240
|
continue
|
|
232
|
-
|
|
241
|
+
|
|
233
242
|
# Validate file
|
|
234
|
-
is_valid, message = AddCommand._validate_file(
|
|
235
|
-
|
|
236
|
-
)
|
|
237
|
-
|
|
243
|
+
is_valid, message = AddCommand._validate_file(file_path, config, force, allow_binary)
|
|
244
|
+
|
|
238
245
|
if not is_valid:
|
|
239
246
|
if not force:
|
|
240
247
|
# Only print first few rejections to avoid spam
|
|
@@ -244,15 +251,15 @@ class AddCommand:
|
|
|
244
251
|
print(" ... (more files rejected)")
|
|
245
252
|
rejected_count += 1
|
|
246
253
|
continue
|
|
247
|
-
|
|
254
|
+
|
|
248
255
|
if message: # Warning
|
|
249
256
|
print(message)
|
|
250
|
-
|
|
257
|
+
|
|
251
258
|
try:
|
|
252
259
|
repo.stage_file(str(rel_to_current))
|
|
253
260
|
print(f" staged: {rel_to_current}")
|
|
254
261
|
staged_count += 1
|
|
255
262
|
except Exception as e:
|
|
256
263
|
print(f"Error staging {rel_to_current}: {e}")
|
|
257
|
-
|
|
264
|
+
|
|
258
265
|
return staged_count, rejected_count
|
memvcs/commands/audit.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""
|
|
2
|
+
agmem audit - Tamper-evident audit trail.
|
|
3
|
+
|
|
4
|
+
Read and verify the append-only audit log.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import argparse
|
|
8
|
+
|
|
9
|
+
from ..commands.base import require_repo
|
|
10
|
+
from ..core.audit import read_audit, verify_audit
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class AuditCommand:
|
|
14
|
+
"""Show and verify the tamper-evident audit log."""
|
|
15
|
+
|
|
16
|
+
name = "audit"
|
|
17
|
+
help = "Show and verify the tamper-evident audit log"
|
|
18
|
+
|
|
19
|
+
@staticmethod
|
|
20
|
+
def add_arguments(parser: argparse.ArgumentParser):
|
|
21
|
+
parser.add_argument(
|
|
22
|
+
"-n",
|
|
23
|
+
"--max",
|
|
24
|
+
type=int,
|
|
25
|
+
default=50,
|
|
26
|
+
metavar="N",
|
|
27
|
+
help="Show at most N entries (default 50)",
|
|
28
|
+
)
|
|
29
|
+
parser.add_argument(
|
|
30
|
+
"--verify",
|
|
31
|
+
action="store_true",
|
|
32
|
+
help="Verify the audit chain and report first tampering point",
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
@staticmethod
|
|
36
|
+
def execute(args) -> int:
|
|
37
|
+
repo, code = require_repo()
|
|
38
|
+
if code != 0:
|
|
39
|
+
return code
|
|
40
|
+
|
|
41
|
+
if args.verify:
|
|
42
|
+
valid, first_bad = verify_audit(repo.mem_dir)
|
|
43
|
+
if valid:
|
|
44
|
+
print("Audit log chain is valid.")
|
|
45
|
+
return 0
|
|
46
|
+
print(f"Audit log chain invalid at entry index {first_bad}.")
|
|
47
|
+
return 1
|
|
48
|
+
|
|
49
|
+
entries = read_audit(repo.mem_dir, max_entries=args.max)
|
|
50
|
+
if not entries:
|
|
51
|
+
print("No audit entries.")
|
|
52
|
+
return 0
|
|
53
|
+
for e in entries:
|
|
54
|
+
ts = e.get("timestamp", "")
|
|
55
|
+
op = e.get("operation", "")
|
|
56
|
+
details = e.get("details", {})
|
|
57
|
+
detail_str = " ".join(f"{k}={v}" for k, v in sorted(details.items()) if v is not None)
|
|
58
|
+
print(f"{ts} {op} {detail_str}")
|
|
59
|
+
return 0
|
memvcs/commands/blame.py
CHANGED
|
@@ -12,53 +12,46 @@ from ..core.objects import Commit, Tree, Blob
|
|
|
12
12
|
|
|
13
13
|
class BlameCommand:
|
|
14
14
|
"""Show author and commit for each line of a file, or trace semantic facts."""
|
|
15
|
-
|
|
16
|
-
name =
|
|
17
|
-
help =
|
|
18
|
-
|
|
15
|
+
|
|
16
|
+
name = "blame"
|
|
17
|
+
help = "Show who changed each line of a memory file, or trace semantic facts"
|
|
18
|
+
|
|
19
19
|
@staticmethod
|
|
20
20
|
def add_arguments(parser: argparse.ArgumentParser):
|
|
21
|
+
parser.add_argument("file", nargs="?", help="File to blame (path relative to current/)")
|
|
21
22
|
parser.add_argument(
|
|
22
|
-
|
|
23
|
-
nargs='?',
|
|
24
|
-
help='File to blame (path relative to current/)'
|
|
25
|
-
)
|
|
26
|
-
parser.add_argument(
|
|
27
|
-
'ref',
|
|
28
|
-
nargs='?',
|
|
29
|
-
default='HEAD',
|
|
30
|
-
help='Commit to blame at (default: HEAD)'
|
|
23
|
+
"ref", nargs="?", default="HEAD", help="Commit to blame at (default: HEAD)"
|
|
31
24
|
)
|
|
32
25
|
parser.add_argument(
|
|
33
|
-
|
|
34
|
-
help='Semantic query to trace (e.g., "Why does agent think X?")'
|
|
26
|
+
"--query", "-q", help='Semantic query to trace (e.g., "Why does agent think X?")'
|
|
35
27
|
)
|
|
36
28
|
parser.add_argument(
|
|
37
|
-
|
|
29
|
+
"--limit",
|
|
30
|
+
"-n",
|
|
38
31
|
type=int,
|
|
39
32
|
default=5,
|
|
40
|
-
help=
|
|
33
|
+
help="Number of results to show for semantic blame (default: 5)",
|
|
41
34
|
)
|
|
42
|
-
|
|
35
|
+
|
|
43
36
|
@staticmethod
|
|
44
37
|
def execute(args) -> int:
|
|
45
38
|
repo, code = require_repo()
|
|
46
39
|
if code != 0:
|
|
47
40
|
return code
|
|
48
|
-
|
|
41
|
+
|
|
49
42
|
# Semantic blame mode
|
|
50
43
|
if args.query:
|
|
51
44
|
return BlameCommand._semantic_blame(repo, args.query, args.limit)
|
|
52
|
-
|
|
45
|
+
|
|
53
46
|
# File blame mode
|
|
54
47
|
if not args.file:
|
|
55
48
|
print("Error: Either --query or a file path is required.")
|
|
56
49
|
print("Usage: agmem blame <file> [ref]")
|
|
57
|
-
print(
|
|
50
|
+
print(' agmem blame --query "Why does agent think X?"')
|
|
58
51
|
return 1
|
|
59
|
-
|
|
52
|
+
|
|
60
53
|
return BlameCommand._file_blame(repo, args.file, args.ref)
|
|
61
|
-
|
|
54
|
+
|
|
62
55
|
@staticmethod
|
|
63
56
|
def _file_blame(repo, filepath: str, ref: str) -> int:
|
|
64
57
|
"""Traditional file-based blame."""
|
|
@@ -66,45 +59,45 @@ class BlameCommand:
|
|
|
66
59
|
if not commit_hash:
|
|
67
60
|
print(f"Error: Unknown revision: {ref}")
|
|
68
61
|
return 1
|
|
69
|
-
|
|
62
|
+
|
|
70
63
|
# Get file content at commit
|
|
71
64
|
tree = repo.get_commit_tree(commit_hash)
|
|
72
65
|
if not tree:
|
|
73
66
|
print("Error: Could not load tree.")
|
|
74
67
|
return 1
|
|
75
|
-
|
|
68
|
+
|
|
76
69
|
# Find file in tree (support path like semantic/user-prefs.md)
|
|
77
70
|
blob_hash = None
|
|
78
71
|
for entry in tree.entries:
|
|
79
|
-
path = entry.path +
|
|
72
|
+
path = entry.path + "/" + entry.name if entry.path else entry.name
|
|
80
73
|
if path == filepath:
|
|
81
74
|
blob_hash = entry.hash
|
|
82
75
|
break
|
|
83
|
-
|
|
76
|
+
|
|
84
77
|
if not blob_hash:
|
|
85
78
|
print(f"Error: File not found in {ref}: {filepath}")
|
|
86
79
|
return 1
|
|
87
|
-
|
|
80
|
+
|
|
88
81
|
blob = Blob.load(repo.object_store, blob_hash)
|
|
89
82
|
if not blob:
|
|
90
83
|
print("Error: Could not load file content.")
|
|
91
84
|
return 1
|
|
92
|
-
|
|
93
|
-
lines = blob.content.decode(
|
|
85
|
+
|
|
86
|
+
lines = blob.content.decode("utf-8", errors="replace").splitlines()
|
|
94
87
|
commit = Commit.load(repo.object_store, commit_hash)
|
|
95
|
-
author_short = commit.author.split(
|
|
88
|
+
author_short = commit.author.split("<")[0].strip()[:20] if commit else "unknown"
|
|
96
89
|
hash_short = commit_hash[:8]
|
|
97
|
-
|
|
90
|
+
|
|
98
91
|
for i, line in enumerate(lines, 1):
|
|
99
92
|
print(f"{hash_short} ({author_short:20} {i:4}) {line}")
|
|
100
|
-
|
|
93
|
+
|
|
101
94
|
return 0
|
|
102
|
-
|
|
95
|
+
|
|
103
96
|
@staticmethod
|
|
104
97
|
def _semantic_blame(repo, query: str, limit: int) -> int:
|
|
105
98
|
"""
|
|
106
99
|
Semantic blame - trace which commit introduced a fact.
|
|
107
|
-
|
|
100
|
+
|
|
108
101
|
Searches the vector store and shows provenance for matching chunks.
|
|
109
102
|
"""
|
|
110
103
|
try:
|
|
@@ -113,34 +106,34 @@ class BlameCommand:
|
|
|
113
106
|
print("Error: Vector search requires sqlite-vec.")
|
|
114
107
|
print("Install with: pip install agmem[vector]")
|
|
115
108
|
return 1
|
|
116
|
-
|
|
109
|
+
|
|
117
110
|
try:
|
|
118
|
-
vs = VectorStore(repo.root /
|
|
111
|
+
vs = VectorStore(repo.root / ".mem")
|
|
119
112
|
results = vs.search_with_provenance(query, limit=limit)
|
|
120
113
|
except Exception as e:
|
|
121
114
|
print(f"Error: Vector search failed: {e}")
|
|
122
115
|
print("Try running 'agmem search --rebuild' to rebuild the index.")
|
|
123
116
|
return 1
|
|
124
|
-
|
|
117
|
+
|
|
125
118
|
if not results:
|
|
126
119
|
print("No matching facts found in memory.")
|
|
127
120
|
print("Try rebuilding the index with 'agmem search --rebuild'")
|
|
128
121
|
return 0
|
|
129
|
-
|
|
130
|
-
print(f
|
|
122
|
+
|
|
123
|
+
print(f'Semantic blame for: "{query}"')
|
|
131
124
|
print("=" * 60)
|
|
132
|
-
|
|
125
|
+
|
|
133
126
|
for i, result in enumerate(results, 1):
|
|
134
|
-
path = result[
|
|
135
|
-
content = result[
|
|
136
|
-
similarity = result[
|
|
137
|
-
commit_hash = result[
|
|
138
|
-
author = result[
|
|
139
|
-
indexed_at = result[
|
|
140
|
-
|
|
127
|
+
path = result["path"]
|
|
128
|
+
content = result["content"]
|
|
129
|
+
similarity = result["similarity"]
|
|
130
|
+
commit_hash = result["commit_hash"]
|
|
131
|
+
author = result["author"]
|
|
132
|
+
indexed_at = result["indexed_at"]
|
|
133
|
+
|
|
141
134
|
print(f"\n[{i}] {path}")
|
|
142
135
|
print(f" Similarity: {similarity:.2%}")
|
|
143
|
-
|
|
136
|
+
|
|
144
137
|
if commit_hash:
|
|
145
138
|
# Try to get commit details
|
|
146
139
|
commit = Commit.load(repo.object_store, commit_hash)
|
|
@@ -157,13 +150,13 @@ class BlameCommand:
|
|
|
157
150
|
print(" Commit: (not tracked)")
|
|
158
151
|
if indexed_at:
|
|
159
152
|
print(f" Indexed: {indexed_at}")
|
|
160
|
-
|
|
153
|
+
|
|
161
154
|
# Show content preview
|
|
162
155
|
print(f"\n Content preview:")
|
|
163
|
-
for line in content.split(
|
|
156
|
+
for line in content.split("\n")[:5]:
|
|
164
157
|
print(f" {line[:70]}")
|
|
165
|
-
if len(content.split(
|
|
158
|
+
if len(content.split("\n")) > 5:
|
|
166
159
|
print(" ...")
|
|
167
|
-
|
|
160
|
+
|
|
168
161
|
print()
|
|
169
162
|
return 0
|
memvcs/commands/branch.py
CHANGED
|
@@ -10,43 +10,23 @@ from ..core.repository import Repository
|
|
|
10
10
|
|
|
11
11
|
class BranchCommand:
|
|
12
12
|
"""Manage branches."""
|
|
13
|
-
|
|
14
|
-
name =
|
|
15
|
-
help =
|
|
16
|
-
|
|
13
|
+
|
|
14
|
+
name = "branch"
|
|
15
|
+
help = "List, create, or delete branches"
|
|
16
|
+
|
|
17
17
|
@staticmethod
|
|
18
18
|
def add_arguments(parser: argparse.ArgumentParser):
|
|
19
|
+
parser.add_argument("name", nargs="?", help="Branch name to create or delete")
|
|
20
|
+
parser.add_argument("--delete", "-d", action="store_true", help="Delete a branch")
|
|
19
21
|
parser.add_argument(
|
|
20
|
-
|
|
21
|
-
nargs='?',
|
|
22
|
-
help='Branch name to create or delete'
|
|
23
|
-
)
|
|
24
|
-
parser.add_argument(
|
|
25
|
-
'--delete', '-d',
|
|
26
|
-
action='store_true',
|
|
27
|
-
help='Delete a branch'
|
|
22
|
+
"--force", "-D", action="store_true", help="Force delete a branch (even if not merged)"
|
|
28
23
|
)
|
|
24
|
+
parser.add_argument("--list", "-l", action="store_true", help="List all branches")
|
|
29
25
|
parser.add_argument(
|
|
30
|
-
|
|
31
|
-
action='store_true',
|
|
32
|
-
help='Force delete a branch (even if not merged)'
|
|
26
|
+
"--all", "-a", action="store_true", help="List all branches including remote"
|
|
33
27
|
)
|
|
34
|
-
parser.add_argument(
|
|
35
|
-
|
|
36
|
-
action='store_true',
|
|
37
|
-
help='List all branches'
|
|
38
|
-
)
|
|
39
|
-
parser.add_argument(
|
|
40
|
-
'--all', '-a',
|
|
41
|
-
action='store_true',
|
|
42
|
-
help='List all branches including remote'
|
|
43
|
-
)
|
|
44
|
-
parser.add_argument(
|
|
45
|
-
'start_point',
|
|
46
|
-
nargs='?',
|
|
47
|
-
help='Commit to start the new branch from'
|
|
48
|
-
)
|
|
49
|
-
|
|
28
|
+
parser.add_argument("start_point", nargs="?", help="Commit to start the new branch from")
|
|
29
|
+
|
|
50
30
|
@staticmethod
|
|
51
31
|
def execute(args) -> int:
|
|
52
32
|
repo, code = require_repo()
|
|
@@ -60,13 +40,13 @@ class BranchCommand:
|
|
|
60
40
|
if not args.name:
|
|
61
41
|
print("Error: Branch name required for deletion")
|
|
62
42
|
return 1
|
|
63
|
-
|
|
43
|
+
|
|
64
44
|
current = repo.refs.get_current_branch()
|
|
65
45
|
if args.name == current:
|
|
66
46
|
print(f"Error: Cannot delete current branch '{args.name}'")
|
|
67
47
|
print("Switch to another branch first.")
|
|
68
48
|
return 1
|
|
69
|
-
|
|
49
|
+
|
|
70
50
|
if repo.refs.delete_branch(args.name):
|
|
71
51
|
print(f"Deleted branch {args.name}")
|
|
72
52
|
return 0
|