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/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
|
memvcs/commands/checkout.py
CHANGED
|
@@ -10,27 +10,18 @@ from ..core.repository import Repository
|
|
|
10
10
|
|
|
11
11
|
class CheckoutCommand:
|
|
12
12
|
"""Switch branches or restore working tree files."""
|
|
13
|
-
|
|
14
|
-
name =
|
|
15
|
-
help =
|
|
16
|
-
|
|
13
|
+
|
|
14
|
+
name = "checkout"
|
|
15
|
+
help = "Switch branches or restore working tree files"
|
|
16
|
+
|
|
17
17
|
@staticmethod
|
|
18
18
|
def add_arguments(parser: argparse.ArgumentParser):
|
|
19
|
+
parser.add_argument("ref", help="Branch, tag, or commit to checkout")
|
|
20
|
+
parser.add_argument("-b", action="store_true", help="Create and checkout a new branch")
|
|
19
21
|
parser.add_argument(
|
|
20
|
-
|
|
21
|
-
help='Branch, tag, or commit to checkout'
|
|
22
|
+
"--force", "-f", action="store_true", help="Force checkout (discard local changes)"
|
|
22
23
|
)
|
|
23
|
-
|
|
24
|
-
'-b',
|
|
25
|
-
action='store_true',
|
|
26
|
-
help='Create and checkout a new branch'
|
|
27
|
-
)
|
|
28
|
-
parser.add_argument(
|
|
29
|
-
'--force', '-f',
|
|
30
|
-
action='store_true',
|
|
31
|
-
help='Force checkout (discard local changes)'
|
|
32
|
-
)
|
|
33
|
-
|
|
24
|
+
|
|
34
25
|
@staticmethod
|
|
35
26
|
def execute(args) -> int:
|
|
36
27
|
repo, code = require_repo()
|
|
@@ -40,24 +31,24 @@ class CheckoutCommand:
|
|
|
40
31
|
# Create and checkout new branch
|
|
41
32
|
if args.b:
|
|
42
33
|
branch_name = args.ref
|
|
43
|
-
|
|
34
|
+
|
|
44
35
|
# Check if branch already exists
|
|
45
36
|
if repo.refs.branch_exists(branch_name):
|
|
46
37
|
print(f"Error: A branch named '{branch_name}' already exists.")
|
|
47
38
|
return 1
|
|
48
|
-
|
|
39
|
+
|
|
49
40
|
# Get current HEAD commit
|
|
50
41
|
head = repo.refs.get_head()
|
|
51
|
-
if head[
|
|
52
|
-
current_commit = repo.refs.get_branch_commit(head[
|
|
42
|
+
if head["type"] == "branch":
|
|
43
|
+
current_commit = repo.refs.get_branch_commit(head["value"])
|
|
53
44
|
else:
|
|
54
|
-
current_commit = head[
|
|
55
|
-
|
|
45
|
+
current_commit = head["value"]
|
|
46
|
+
|
|
56
47
|
# Create branch
|
|
57
48
|
if not repo.refs.create_branch(branch_name, current_commit):
|
|
58
49
|
print(f"Error: Could not create branch '{branch_name}'")
|
|
59
50
|
return 1
|
|
60
|
-
|
|
51
|
+
|
|
61
52
|
# Switch to new branch
|
|
62
53
|
try:
|
|
63
54
|
repo.refs.set_head_branch(branch_name)
|
|
@@ -66,16 +57,16 @@ class CheckoutCommand:
|
|
|
66
57
|
except Exception as e:
|
|
67
58
|
print(f"Error switching to branch: {e}")
|
|
68
59
|
return 1
|
|
69
|
-
|
|
60
|
+
|
|
70
61
|
# Regular checkout
|
|
71
62
|
ref = args.ref
|
|
72
|
-
|
|
63
|
+
|
|
73
64
|
# Check if it's a branch
|
|
74
65
|
is_branch = repo.refs.branch_exists(ref)
|
|
75
|
-
|
|
66
|
+
|
|
76
67
|
try:
|
|
77
68
|
commit_hash = repo.checkout(ref, force=args.force)
|
|
78
|
-
|
|
69
|
+
|
|
79
70
|
if is_branch:
|
|
80
71
|
print(f"Switched to branch '{ref}'")
|
|
81
72
|
else:
|
|
@@ -83,16 +74,20 @@ class CheckoutCommand:
|
|
|
83
74
|
if repo.refs.tag_exists(ref):
|
|
84
75
|
print(f"Note: checking out '{ref}'.")
|
|
85
76
|
print()
|
|
86
|
-
print(
|
|
87
|
-
|
|
77
|
+
print(
|
|
78
|
+
"You are in 'detached HEAD' state. You can look around, make experimental"
|
|
79
|
+
)
|
|
80
|
+
print(
|
|
81
|
+
"changes and commit them, and you can discard any commits you make in this"
|
|
82
|
+
)
|
|
88
83
|
print("state without impacting any branches by switching back to a branch.")
|
|
89
84
|
else:
|
|
90
85
|
print(f"Note: checking out '{commit_hash[:8]}'.")
|
|
91
86
|
print()
|
|
92
87
|
print("You are in 'detached HEAD' state.")
|
|
93
|
-
|
|
88
|
+
|
|
94
89
|
return 0
|
|
95
|
-
|
|
90
|
+
|
|
96
91
|
except ValueError as e:
|
|
97
92
|
print(f"Error: {e}")
|
|
98
93
|
return 1
|
memvcs/commands/clean.py
CHANGED
|
@@ -12,52 +12,46 @@ from ..core.repository import Repository
|
|
|
12
12
|
|
|
13
13
|
class CleanCommand:
|
|
14
14
|
"""Remove untracked files from working directory."""
|
|
15
|
-
|
|
16
|
-
name =
|
|
17
|
-
help =
|
|
18
|
-
|
|
15
|
+
|
|
16
|
+
name = "clean"
|
|
17
|
+
help = "Remove untracked files from working directory"
|
|
18
|
+
|
|
19
19
|
@staticmethod
|
|
20
20
|
def add_arguments(parser: argparse.ArgumentParser):
|
|
21
21
|
parser.add_argument(
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
parser.add_argument(
|
|
27
|
-
'-f', '--force',
|
|
28
|
-
action='store_true',
|
|
29
|
-
help='Required to actually remove files'
|
|
22
|
+
"-n",
|
|
23
|
+
"--dry-run",
|
|
24
|
+
action="store_true",
|
|
25
|
+
help="Show what would be removed without removing",
|
|
30
26
|
)
|
|
31
27
|
parser.add_argument(
|
|
32
|
-
|
|
33
|
-
action='store_true',
|
|
34
|
-
help='Remove untracked directories too'
|
|
28
|
+
"-f", "--force", action="store_true", help="Required to actually remove files"
|
|
35
29
|
)
|
|
36
|
-
|
|
30
|
+
parser.add_argument("-d", action="store_true", help="Remove untracked directories too")
|
|
31
|
+
|
|
37
32
|
@staticmethod
|
|
38
33
|
def execute(args) -> int:
|
|
39
34
|
repo, code = require_repo()
|
|
40
35
|
if code != 0:
|
|
41
36
|
return code
|
|
42
37
|
|
|
43
|
-
|
|
44
38
|
status = repo.get_status()
|
|
45
|
-
untracked = status.get(
|
|
46
|
-
|
|
39
|
+
untracked = status.get("untracked", [])
|
|
40
|
+
|
|
47
41
|
if not untracked:
|
|
48
42
|
print("Nothing to clean.")
|
|
49
43
|
return 0
|
|
50
|
-
|
|
44
|
+
|
|
51
45
|
if args.dry_run:
|
|
52
46
|
print("Would remove:")
|
|
53
47
|
for p in untracked:
|
|
54
48
|
print(f" {p}")
|
|
55
49
|
return 0
|
|
56
|
-
|
|
50
|
+
|
|
57
51
|
if not args.force:
|
|
58
52
|
print("Use -f to force removal of untracked files.")
|
|
59
53
|
return 1
|
|
60
|
-
|
|
54
|
+
|
|
61
55
|
removed = 0
|
|
62
56
|
for rel_path in untracked:
|
|
63
57
|
full_path = repo.current_dir / rel_path
|
|
@@ -68,9 +62,10 @@ class CleanCommand:
|
|
|
68
62
|
print(f"Removed {rel_path}")
|
|
69
63
|
elif args.d and full_path.is_dir():
|
|
70
64
|
import shutil
|
|
65
|
+
|
|
71
66
|
shutil.rmtree(full_path)
|
|
72
67
|
removed += 1
|
|
73
68
|
print(f"Removed {rel_path}/")
|
|
74
|
-
|
|
69
|
+
|
|
75
70
|
print(f"Removed {removed} file(s)")
|
|
76
71
|
return 0
|
memvcs/commands/clone.py
CHANGED
|
@@ -80,11 +80,14 @@ class CloneCommand:
|
|
|
80
80
|
|
|
81
81
|
# Set remote origin to source
|
|
82
82
|
import json
|
|
83
|
+
|
|
83
84
|
config_file = target / ".mem" / "config.json"
|
|
84
85
|
config = json.loads(config_file.read_text()) if config_file.exists() else {}
|
|
85
86
|
if "remotes" not in config:
|
|
86
87
|
config["remotes"] = {}
|
|
87
|
-
config["remotes"]["origin"] = {
|
|
88
|
+
config["remotes"]["origin"] = {
|
|
89
|
+
"url": url if url.startswith("file://") else f"file://{remote_path}"
|
|
90
|
+
}
|
|
88
91
|
config_file.write_text(json.dumps(config, indent=2))
|
|
89
92
|
|
|
90
93
|
print(f"Cloned into {target}")
|
memvcs/commands/commit.py
CHANGED
|
@@ -7,88 +7,82 @@ from datetime import datetime
|
|
|
7
7
|
|
|
8
8
|
from ..commands.base import require_repo
|
|
9
9
|
from ..core.schema import SchemaValidator
|
|
10
|
-
from ..core.hooks import run_pre_commit_hooks
|
|
10
|
+
from ..core.hooks import run_pre_commit_hooks, compute_suggested_importance
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
class CommitCommand:
|
|
14
14
|
"""Create a commit from staged changes."""
|
|
15
|
-
|
|
16
|
-
name =
|
|
17
|
-
help =
|
|
18
|
-
|
|
15
|
+
|
|
16
|
+
name = "commit"
|
|
17
|
+
help = "Save staged changes as a memory snapshot"
|
|
18
|
+
|
|
19
19
|
@staticmethod
|
|
20
20
|
def add_arguments(parser: argparse.ArgumentParser):
|
|
21
21
|
parser.add_argument(
|
|
22
|
-
|
|
23
|
-
required=True,
|
|
24
|
-
help='Commit message describing the changes'
|
|
25
|
-
)
|
|
26
|
-
parser.add_argument(
|
|
27
|
-
'--author',
|
|
28
|
-
help='Override default author'
|
|
22
|
+
"-m", "--message", required=True, help="Commit message describing the changes"
|
|
29
23
|
)
|
|
24
|
+
parser.add_argument("--author", help="Override default author")
|
|
30
25
|
parser.add_argument(
|
|
31
|
-
|
|
32
|
-
action='store_true',
|
|
33
|
-
help='Skip pre-commit hooks and schema validation'
|
|
26
|
+
"--no-verify", action="store_true", help="Skip pre-commit hooks and schema validation"
|
|
34
27
|
)
|
|
28
|
+
parser.add_argument("--strict", action="store_true", help="Treat schema warnings as errors")
|
|
35
29
|
parser.add_argument(
|
|
36
|
-
|
|
37
|
-
action='store_true',
|
|
38
|
-
help='Treat schema warnings as errors'
|
|
30
|
+
"--run-tests", action="store_true", help="Run memory tests before committing"
|
|
39
31
|
)
|
|
40
32
|
parser.add_argument(
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
33
|
+
"--importance",
|
|
34
|
+
type=float,
|
|
35
|
+
metavar="SCORE",
|
|
36
|
+
help="Importance score 0.0-1.0 for recall/decay weighting",
|
|
44
37
|
)
|
|
45
|
-
|
|
38
|
+
|
|
46
39
|
@staticmethod
|
|
47
40
|
def _get_blob_hash(file_info) -> str:
|
|
48
41
|
"""Get blob hash from StagedFile or dict (for hooks that pass either)."""
|
|
49
|
-
if hasattr(file_info,
|
|
42
|
+
if hasattr(file_info, "blob_hash"):
|
|
50
43
|
return file_info.blob_hash
|
|
51
44
|
if isinstance(file_info, dict):
|
|
52
|
-
return file_info.get(
|
|
53
|
-
return
|
|
45
|
+
return file_info.get("blob_hash") or file_info.get("hash") or ""
|
|
46
|
+
return ""
|
|
54
47
|
|
|
55
48
|
@staticmethod
|
|
56
49
|
def _validate_staged_files(repo, staged: dict, strict: bool) -> tuple:
|
|
57
50
|
"""
|
|
58
51
|
Validate staged files for schema compliance.
|
|
59
|
-
|
|
52
|
+
|
|
60
53
|
Returns:
|
|
61
54
|
Tuple of (success, validation_results)
|
|
62
55
|
"""
|
|
63
56
|
validation_results = {}
|
|
64
57
|
has_errors = False
|
|
65
|
-
|
|
58
|
+
|
|
66
59
|
for filepath, file_info in staged.items():
|
|
67
60
|
blob_hash = CommitCommand._get_blob_hash(file_info)
|
|
68
61
|
if not blob_hash:
|
|
69
62
|
continue
|
|
70
|
-
|
|
63
|
+
|
|
71
64
|
# Read content from object store
|
|
72
65
|
from ..core.objects import Blob
|
|
66
|
+
|
|
73
67
|
blob = Blob.load(repo.object_store, blob_hash)
|
|
74
68
|
if not blob:
|
|
75
69
|
continue
|
|
76
|
-
|
|
70
|
+
|
|
77
71
|
try:
|
|
78
|
-
content = blob.content.decode(
|
|
72
|
+
content = blob.content.decode("utf-8")
|
|
79
73
|
except UnicodeDecodeError:
|
|
80
74
|
# Skip binary files
|
|
81
75
|
continue
|
|
82
|
-
|
|
76
|
+
|
|
83
77
|
# Validate the file
|
|
84
78
|
result = SchemaValidator.validate(content, filepath, strict=strict)
|
|
85
79
|
validation_results[filepath] = result
|
|
86
|
-
|
|
80
|
+
|
|
87
81
|
if not result.valid:
|
|
88
82
|
has_errors = True
|
|
89
|
-
|
|
83
|
+
|
|
90
84
|
return not has_errors, validation_results
|
|
91
|
-
|
|
85
|
+
|
|
92
86
|
@staticmethod
|
|
93
87
|
def _print_validation_results(results: dict) -> None:
|
|
94
88
|
"""Print validation results in a readable format."""
|
|
@@ -126,6 +120,7 @@ class CommitCommand:
|
|
|
126
120
|
"""Run memory tests if available. Returns 1 on failure, 0 on success or skip."""
|
|
127
121
|
try:
|
|
128
122
|
from ..core.test_runner import TestRunner
|
|
123
|
+
|
|
129
124
|
test_runner = TestRunner(repo)
|
|
130
125
|
test_result = test_runner.run_all()
|
|
131
126
|
if not test_result.passed:
|
|
@@ -156,13 +151,19 @@ class CommitCommand:
|
|
|
156
151
|
if args.run_tests:
|
|
157
152
|
if CommitCommand._run_memory_tests(repo) != 0:
|
|
158
153
|
return 1
|
|
159
|
-
metadata = {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
154
|
+
metadata = {"files_changed": len(staged), "timestamp": datetime.utcnow().isoformat() + "Z"}
|
|
155
|
+
# Importance scoring: explicit --importance or auto from heuristics
|
|
156
|
+
if args.importance is not None:
|
|
157
|
+
if not (0.0 <= args.importance <= 1.0):
|
|
158
|
+
print("Error: --importance must be between 0.0 and 1.0")
|
|
159
|
+
return 1
|
|
160
|
+
metadata["importance"] = args.importance
|
|
161
|
+
else:
|
|
162
|
+
suggested = compute_suggested_importance(repo, staged, args.message, metadata)
|
|
163
|
+
metadata["importance"] = suggested
|
|
163
164
|
if args.author:
|
|
164
165
|
config = repo.get_config()
|
|
165
|
-
config[
|
|
166
|
+
config["author"]["name"] = args.author
|
|
166
167
|
repo.set_config(config)
|
|
167
168
|
try:
|
|
168
169
|
commit_hash = repo.commit(args.message, metadata)
|