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.
- agmem-0.1.1.dist-info/METADATA +656 -0
- agmem-0.1.1.dist-info/RECORD +67 -0
- agmem-0.1.1.dist-info/WHEEL +5 -0
- agmem-0.1.1.dist-info/entry_points.txt +2 -0
- agmem-0.1.1.dist-info/licenses/LICENSE +21 -0
- agmem-0.1.1.dist-info/top_level.txt +1 -0
- memvcs/__init__.py +9 -0
- memvcs/cli.py +178 -0
- memvcs/commands/__init__.py +23 -0
- memvcs/commands/add.py +258 -0
- memvcs/commands/base.py +23 -0
- memvcs/commands/blame.py +169 -0
- memvcs/commands/branch.py +110 -0
- memvcs/commands/checkout.py +101 -0
- memvcs/commands/clean.py +76 -0
- memvcs/commands/clone.py +91 -0
- memvcs/commands/commit.py +174 -0
- memvcs/commands/daemon.py +267 -0
- memvcs/commands/diff.py +157 -0
- memvcs/commands/fsck.py +203 -0
- memvcs/commands/garden.py +107 -0
- memvcs/commands/graph.py +151 -0
- memvcs/commands/init.py +61 -0
- memvcs/commands/log.py +103 -0
- memvcs/commands/mcp.py +59 -0
- memvcs/commands/merge.py +88 -0
- memvcs/commands/pull.py +65 -0
- memvcs/commands/push.py +143 -0
- memvcs/commands/reflog.py +52 -0
- memvcs/commands/remote.py +51 -0
- memvcs/commands/reset.py +98 -0
- memvcs/commands/search.py +163 -0
- memvcs/commands/serve.py +54 -0
- memvcs/commands/show.py +125 -0
- memvcs/commands/stash.py +97 -0
- memvcs/commands/status.py +112 -0
- memvcs/commands/tag.py +117 -0
- memvcs/commands/test.py +132 -0
- memvcs/commands/tree.py +156 -0
- memvcs/core/__init__.py +21 -0
- memvcs/core/config_loader.py +245 -0
- memvcs/core/constants.py +12 -0
- memvcs/core/diff.py +380 -0
- memvcs/core/gardener.py +466 -0
- memvcs/core/hooks.py +151 -0
- memvcs/core/knowledge_graph.py +381 -0
- memvcs/core/merge.py +474 -0
- memvcs/core/objects.py +323 -0
- memvcs/core/pii_scanner.py +343 -0
- memvcs/core/refs.py +447 -0
- memvcs/core/remote.py +278 -0
- memvcs/core/repository.py +522 -0
- memvcs/core/schema.py +414 -0
- memvcs/core/staging.py +227 -0
- memvcs/core/storage/__init__.py +72 -0
- memvcs/core/storage/base.py +359 -0
- memvcs/core/storage/gcs.py +308 -0
- memvcs/core/storage/local.py +182 -0
- memvcs/core/storage/s3.py +369 -0
- memvcs/core/test_runner.py +371 -0
- memvcs/core/vector_store.py +313 -0
- memvcs/integrations/__init__.py +5 -0
- memvcs/integrations/mcp_server.py +267 -0
- memvcs/integrations/web_ui/__init__.py +1 -0
- memvcs/integrations/web_ui/server.py +352 -0
- memvcs/utils/__init__.py +9 -0
- memvcs/utils/helpers.py +178 -0
memvcs/commands/serve.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""
|
|
2
|
+
agmem serve - Start web UI server for browsing history.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ServeCommand:
|
|
11
|
+
"""Start the agmem web UI server."""
|
|
12
|
+
|
|
13
|
+
name = "serve"
|
|
14
|
+
help = "Start web UI for browsing memory history"
|
|
15
|
+
|
|
16
|
+
@staticmethod
|
|
17
|
+
def add_arguments(parser: argparse.ArgumentParser):
|
|
18
|
+
parser.add_argument(
|
|
19
|
+
"--port", "-p",
|
|
20
|
+
type=int,
|
|
21
|
+
default=8765,
|
|
22
|
+
help="Port to bind (default: 8765)",
|
|
23
|
+
)
|
|
24
|
+
parser.add_argument(
|
|
25
|
+
"--host",
|
|
26
|
+
default="127.0.0.1",
|
|
27
|
+
help="Host to bind (default: 127.0.0.1)",
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
@staticmethod
|
|
31
|
+
def execute(args) -> int:
|
|
32
|
+
try:
|
|
33
|
+
import uvicorn
|
|
34
|
+
except ImportError:
|
|
35
|
+
print(
|
|
36
|
+
"Error: Web UI requires fastapi and uvicorn. "
|
|
37
|
+
"Install with: pip install agmem[web]",
|
|
38
|
+
file=sys.stderr,
|
|
39
|
+
)
|
|
40
|
+
return 1
|
|
41
|
+
|
|
42
|
+
from memvcs.commands.base import require_repo
|
|
43
|
+
from memvcs.core.repository import Repository
|
|
44
|
+
|
|
45
|
+
repo, code = require_repo()
|
|
46
|
+
if code != 0:
|
|
47
|
+
return code
|
|
48
|
+
|
|
49
|
+
from memvcs.integrations.web_ui.server import create_app
|
|
50
|
+
|
|
51
|
+
app = create_app(repo.root)
|
|
52
|
+
print(f"agmem Web UI: http://{args.host}:{args.port}")
|
|
53
|
+
uvicorn.run(app, host=args.host, port=args.port)
|
|
54
|
+
return 0
|
memvcs/commands/show.py
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""
|
|
2
|
+
agmem show - Show various types of objects.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from ..commands.base import require_repo
|
|
10
|
+
from ..core.objects import Blob, Commit, Tree
|
|
11
|
+
from ..core.repository import Repository
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ShowCommand:
|
|
15
|
+
"""Show various types of objects."""
|
|
16
|
+
|
|
17
|
+
name = 'show'
|
|
18
|
+
help = 'Show various types of objects'
|
|
19
|
+
|
|
20
|
+
@staticmethod
|
|
21
|
+
def add_arguments(parser: argparse.ArgumentParser):
|
|
22
|
+
parser.add_argument(
|
|
23
|
+
'object',
|
|
24
|
+
help='Object to show (commit, tree, blob, branch)'
|
|
25
|
+
)
|
|
26
|
+
parser.add_argument(
|
|
27
|
+
'--type', '-t',
|
|
28
|
+
choices=['commit', 'tree', 'blob', 'auto'],
|
|
29
|
+
default='auto',
|
|
30
|
+
help='Type of object to show'
|
|
31
|
+
)
|
|
32
|
+
parser.add_argument(
|
|
33
|
+
'--raw',
|
|
34
|
+
action='store_true',
|
|
35
|
+
help='Show raw object content'
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
@staticmethod
|
|
39
|
+
def execute(args) -> int:
|
|
40
|
+
# Find repository
|
|
41
|
+
repo, code = require_repo()
|
|
42
|
+
if code != 0:
|
|
43
|
+
return code
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
obj_ref = args.object
|
|
47
|
+
obj_hash = None
|
|
48
|
+
obj_type = args.type
|
|
49
|
+
|
|
50
|
+
# Try to resolve as reference
|
|
51
|
+
resolved = repo.resolve_ref(obj_ref)
|
|
52
|
+
if resolved:
|
|
53
|
+
obj_hash = resolved
|
|
54
|
+
else:
|
|
55
|
+
# Assume it's a hash
|
|
56
|
+
obj_hash = obj_ref
|
|
57
|
+
|
|
58
|
+
# Try to determine type if auto
|
|
59
|
+
if obj_type == 'auto':
|
|
60
|
+
# Try commit first
|
|
61
|
+
commit = Commit.load(repo.object_store, obj_hash)
|
|
62
|
+
if commit:
|
|
63
|
+
obj_type = 'commit'
|
|
64
|
+
else:
|
|
65
|
+
# Try tree
|
|
66
|
+
tree = Tree.load(repo.object_store, obj_hash)
|
|
67
|
+
if tree:
|
|
68
|
+
obj_type = 'tree'
|
|
69
|
+
else:
|
|
70
|
+
# Try blob
|
|
71
|
+
blob = Blob.load(repo.object_store, obj_hash)
|
|
72
|
+
if blob:
|
|
73
|
+
obj_type = 'blob'
|
|
74
|
+
else:
|
|
75
|
+
print(f"Error: Object not found: {obj_ref}")
|
|
76
|
+
return 1
|
|
77
|
+
|
|
78
|
+
# Display based on type
|
|
79
|
+
if obj_type == 'commit':
|
|
80
|
+
commit = Commit.load(repo.object_store, obj_hash)
|
|
81
|
+
if not commit:
|
|
82
|
+
print(f"Error: Commit not found: {obj_ref}")
|
|
83
|
+
return 1
|
|
84
|
+
|
|
85
|
+
print(f"commit {obj_hash}")
|
|
86
|
+
print(f"Author: {commit.author}")
|
|
87
|
+
print(f"Date: {commit.timestamp}")
|
|
88
|
+
print()
|
|
89
|
+
print(f" {commit.message}")
|
|
90
|
+
print()
|
|
91
|
+
print(f"tree {commit.tree}")
|
|
92
|
+
if commit.parents:
|
|
93
|
+
print(f"parent {' '.join(commit.parents)}")
|
|
94
|
+
|
|
95
|
+
elif obj_type == 'tree':
|
|
96
|
+
tree = Tree.load(repo.object_store, obj_hash)
|
|
97
|
+
if not tree:
|
|
98
|
+
print(f"Error: Tree not found: {obj_ref}")
|
|
99
|
+
return 1
|
|
100
|
+
|
|
101
|
+
print(f"tree {obj_hash}")
|
|
102
|
+
print()
|
|
103
|
+
for entry in sorted(tree.entries, key=lambda e: e.name):
|
|
104
|
+
path = entry.path + '/' + entry.name if entry.path else entry.name
|
|
105
|
+
print(f"{entry.mode} {entry.type} {entry.hash[:8]}\t{path}")
|
|
106
|
+
|
|
107
|
+
elif obj_type == 'blob':
|
|
108
|
+
blob = Blob.load(repo.object_store, obj_hash)
|
|
109
|
+
if not blob:
|
|
110
|
+
print(f"Error: Blob not found: {obj_ref}")
|
|
111
|
+
return 1
|
|
112
|
+
|
|
113
|
+
if args.raw:
|
|
114
|
+
print(blob.content.decode('utf-8', errors='replace'))
|
|
115
|
+
else:
|
|
116
|
+
print(f"blob {obj_hash}")
|
|
117
|
+
print(f"Size: {len(blob.content)} bytes")
|
|
118
|
+
print()
|
|
119
|
+
content = blob.content.decode('utf-8', errors='replace')
|
|
120
|
+
if len(content) > 1000:
|
|
121
|
+
print(content[:1000] + "\n... (truncated)")
|
|
122
|
+
else:
|
|
123
|
+
print(content)
|
|
124
|
+
|
|
125
|
+
return 0
|
memvcs/commands/stash.py
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""
|
|
2
|
+
agmem stash - Stash changes for later (Git-like).
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from ..commands.base import require_repo
|
|
9
|
+
from ..core.repository import Repository
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class StashCommand:
|
|
13
|
+
"""Stash working directory changes."""
|
|
14
|
+
|
|
15
|
+
name = 'stash'
|
|
16
|
+
help = 'Stash changes for later (save work in progress)'
|
|
17
|
+
|
|
18
|
+
@staticmethod
|
|
19
|
+
def add_arguments(parser: argparse.ArgumentParser):
|
|
20
|
+
subparsers = parser.add_subparsers(dest='stash_action', help='Stash action')
|
|
21
|
+
|
|
22
|
+
# Default: list (when no subcommand given)
|
|
23
|
+
parser.set_defaults(stash_action='list')
|
|
24
|
+
|
|
25
|
+
# stash (push)
|
|
26
|
+
push_p = subparsers.add_parser('push', help='Stash current changes')
|
|
27
|
+
push_p.add_argument('-m', '--message', default='', help='Stash message')
|
|
28
|
+
|
|
29
|
+
# stash pop
|
|
30
|
+
subparsers.add_parser('pop', help='Apply and remove most recent stash')
|
|
31
|
+
|
|
32
|
+
# stash list (default)
|
|
33
|
+
subparsers.add_parser('list', help='List stashes')
|
|
34
|
+
|
|
35
|
+
# stash apply
|
|
36
|
+
apply_p = subparsers.add_parser('apply', help='Apply stash without removing')
|
|
37
|
+
apply_p.add_argument('stash_ref', nargs='?', default='stash@{0}', help='Stash reference')
|
|
38
|
+
|
|
39
|
+
@staticmethod
|
|
40
|
+
def execute(args) -> int:
|
|
41
|
+
repo, code = require_repo()
|
|
42
|
+
if code != 0:
|
|
43
|
+
return code
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
action = getattr(args, 'stash_action', None)
|
|
47
|
+
if action is None:
|
|
48
|
+
action = 'list'
|
|
49
|
+
|
|
50
|
+
if action == 'push':
|
|
51
|
+
stash_hash = repo.stash_create(getattr(args, 'message', '') or '')
|
|
52
|
+
if stash_hash:
|
|
53
|
+
print(f"Stashed changes (stash@{0})")
|
|
54
|
+
return 0
|
|
55
|
+
else:
|
|
56
|
+
print("No local changes to stash.")
|
|
57
|
+
return 0
|
|
58
|
+
|
|
59
|
+
elif action == 'pop':
|
|
60
|
+
stash_hash = repo.stash_pop(0)
|
|
61
|
+
if stash_hash:
|
|
62
|
+
print(f"Restored stashed changes")
|
|
63
|
+
return 0
|
|
64
|
+
else:
|
|
65
|
+
print("No stash entries found.")
|
|
66
|
+
return 1
|
|
67
|
+
|
|
68
|
+
elif action == 'list':
|
|
69
|
+
stashes = repo.refs.stash_list()
|
|
70
|
+
if not stashes:
|
|
71
|
+
print("No stash entries found.")
|
|
72
|
+
return 0
|
|
73
|
+
for i, s in enumerate(stashes):
|
|
74
|
+
msg = s.get('message', 'WIP')
|
|
75
|
+
h = s.get('hash', '')[:8]
|
|
76
|
+
print(f"stash@{{{i}}}: {h} {msg}")
|
|
77
|
+
return 0
|
|
78
|
+
|
|
79
|
+
elif action == 'apply':
|
|
80
|
+
ref = getattr(args, 'stash_ref', 'stash@{0}')
|
|
81
|
+
commit_hash = repo.resolve_ref(ref)
|
|
82
|
+
if not commit_hash:
|
|
83
|
+
print(f"Error: Stash not found: {ref}")
|
|
84
|
+
return 1
|
|
85
|
+
from ..core.objects import Tree, Blob
|
|
86
|
+
tree = repo.get_commit_tree(commit_hash)
|
|
87
|
+
if tree:
|
|
88
|
+
for entry in tree.entries:
|
|
89
|
+
blob = Blob.load(repo.object_store, entry.hash)
|
|
90
|
+
if blob:
|
|
91
|
+
fp = repo.current_dir / entry.path / entry.name
|
|
92
|
+
fp.parent.mkdir(parents=True, exist_ok=True)
|
|
93
|
+
fp.write_bytes(blob.content)
|
|
94
|
+
print("Applied stash (changes in working directory)")
|
|
95
|
+
return 0
|
|
96
|
+
|
|
97
|
+
return 1
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""
|
|
2
|
+
agmem status - Show working tree status.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
|
|
7
|
+
from ..commands.base import require_repo
|
|
8
|
+
from ..core.repository import Repository
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class StatusCommand:
|
|
12
|
+
"""Show the working tree status."""
|
|
13
|
+
|
|
14
|
+
name = 'status'
|
|
15
|
+
help = 'Show the working tree status'
|
|
16
|
+
|
|
17
|
+
@staticmethod
|
|
18
|
+
def add_arguments(parser: argparse.ArgumentParser):
|
|
19
|
+
parser.add_argument(
|
|
20
|
+
'--short', '-s',
|
|
21
|
+
action='store_true',
|
|
22
|
+
help='Show short format'
|
|
23
|
+
)
|
|
24
|
+
parser.add_argument(
|
|
25
|
+
'--branch', '-b',
|
|
26
|
+
action='store_true',
|
|
27
|
+
help='Show branch information'
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
@staticmethod
|
|
31
|
+
def execute(args) -> int:
|
|
32
|
+
repo, code = require_repo()
|
|
33
|
+
if code != 0:
|
|
34
|
+
return code
|
|
35
|
+
|
|
36
|
+
status = repo.get_status()
|
|
37
|
+
|
|
38
|
+
# Show branch info
|
|
39
|
+
branch = status.get('branch')
|
|
40
|
+
head = status.get('head', {})
|
|
41
|
+
|
|
42
|
+
if args.short:
|
|
43
|
+
# Short format: XY filename
|
|
44
|
+
for f in status.get('staged', []):
|
|
45
|
+
print(f"A {f}")
|
|
46
|
+
for f in status.get('modified', []):
|
|
47
|
+
print(f" M {f}")
|
|
48
|
+
for f in status.get('deleted', []):
|
|
49
|
+
print(f" D {f}")
|
|
50
|
+
for f in status.get('untracked', []):
|
|
51
|
+
print(f"?? {f}")
|
|
52
|
+
else:
|
|
53
|
+
# Long format
|
|
54
|
+
if branch:
|
|
55
|
+
print(f"On branch {branch}")
|
|
56
|
+
elif head.get('type') == 'commit':
|
|
57
|
+
print(f"HEAD detached at {head['value'][:8]}")
|
|
58
|
+
|
|
59
|
+
# Check for commits
|
|
60
|
+
head_commit = repo.get_head_commit()
|
|
61
|
+
if not head_commit:
|
|
62
|
+
print("\nNo commits yet")
|
|
63
|
+
|
|
64
|
+
# Staged changes
|
|
65
|
+
staged = status.get('staged', [])
|
|
66
|
+
if staged:
|
|
67
|
+
print(f"\nChanges to be committed:")
|
|
68
|
+
print(f' (use "agmem reset HEAD <file>..." to unstage)')
|
|
69
|
+
print()
|
|
70
|
+
for f in staged:
|
|
71
|
+
print(f" new file: {f}")
|
|
72
|
+
print()
|
|
73
|
+
|
|
74
|
+
# Modified but not staged
|
|
75
|
+
modified = status.get('modified', [])
|
|
76
|
+
if modified:
|
|
77
|
+
print(f"Changes not staged for commit:")
|
|
78
|
+
print(f' (use "agmem add <file>..." to update what will be committed)')
|
|
79
|
+
print()
|
|
80
|
+
for f in modified:
|
|
81
|
+
print(f" modified: {f}")
|
|
82
|
+
print()
|
|
83
|
+
|
|
84
|
+
# Deleted but not staged
|
|
85
|
+
deleted = status.get('deleted', [])
|
|
86
|
+
if deleted:
|
|
87
|
+
print(f"Deleted files:")
|
|
88
|
+
print(f' (use "agmem add <file>..." to stage deletion)')
|
|
89
|
+
print()
|
|
90
|
+
for f in deleted:
|
|
91
|
+
print(f" deleted: {f}")
|
|
92
|
+
print()
|
|
93
|
+
|
|
94
|
+
# Untracked files
|
|
95
|
+
untracked = status.get('untracked', [])
|
|
96
|
+
if untracked:
|
|
97
|
+
print(f"Untracked files:")
|
|
98
|
+
print(f' (use "agmem add <file>..." to include in what will be committed)')
|
|
99
|
+
print()
|
|
100
|
+
for f in untracked:
|
|
101
|
+
print(f" {f}")
|
|
102
|
+
print()
|
|
103
|
+
|
|
104
|
+
# Summary
|
|
105
|
+
total_changes = len(staged) + len(modified) + len(deleted) + len(untracked)
|
|
106
|
+
if total_changes == 0:
|
|
107
|
+
if head_commit:
|
|
108
|
+
print("nothing to commit, working tree clean")
|
|
109
|
+
else:
|
|
110
|
+
print('nothing to commit (create/copy files and use "agmem add" to track)')
|
|
111
|
+
|
|
112
|
+
return 0
|
memvcs/commands/tag.py
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""
|
|
2
|
+
agmem tag - Create, list, delete or verify a tag.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from ..commands.base import require_repo
|
|
10
|
+
from ..core.objects import Tag
|
|
11
|
+
from ..core.repository import Repository
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class TagCommand:
|
|
15
|
+
"""Manage tags."""
|
|
16
|
+
|
|
17
|
+
name = 'tag'
|
|
18
|
+
help = 'Create, list, delete or verify a tag'
|
|
19
|
+
|
|
20
|
+
@staticmethod
|
|
21
|
+
def add_arguments(parser: argparse.ArgumentParser):
|
|
22
|
+
parser.add_argument(
|
|
23
|
+
'name',
|
|
24
|
+
nargs='?',
|
|
25
|
+
help='Tag name'
|
|
26
|
+
)
|
|
27
|
+
parser.add_argument(
|
|
28
|
+
'commit',
|
|
29
|
+
nargs='?',
|
|
30
|
+
help='Commit to tag (default: HEAD)'
|
|
31
|
+
)
|
|
32
|
+
parser.add_argument(
|
|
33
|
+
'--list', '-l',
|
|
34
|
+
action='store_true',
|
|
35
|
+
help='List tags'
|
|
36
|
+
)
|
|
37
|
+
parser.add_argument(
|
|
38
|
+
'--delete', '-d',
|
|
39
|
+
action='store_true',
|
|
40
|
+
help='Delete a tag'
|
|
41
|
+
)
|
|
42
|
+
parser.add_argument(
|
|
43
|
+
'-m', '--message',
|
|
44
|
+
help='Tag message'
|
|
45
|
+
)
|
|
46
|
+
parser.add_argument(
|
|
47
|
+
'--force', '-f',
|
|
48
|
+
action='store_true',
|
|
49
|
+
help='Force replace existing tag'
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
@staticmethod
|
|
53
|
+
def execute(args) -> int:
|
|
54
|
+
# Find repository
|
|
55
|
+
repo, code = require_repo()
|
|
56
|
+
if code != 0:
|
|
57
|
+
return code
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
# List tags
|
|
61
|
+
if args.list or (not args.name and not args.delete):
|
|
62
|
+
tags = repo.refs.list_tags()
|
|
63
|
+
|
|
64
|
+
if not tags:
|
|
65
|
+
print("No tags yet.")
|
|
66
|
+
return 0
|
|
67
|
+
|
|
68
|
+
for tag in sorted(tags):
|
|
69
|
+
commit_hash = repo.refs.get_tag_commit(tag)
|
|
70
|
+
short_hash = commit_hash[:8] if commit_hash else "????????"
|
|
71
|
+
print(f"{tag}\t{short_hash}")
|
|
72
|
+
|
|
73
|
+
return 0
|
|
74
|
+
|
|
75
|
+
# Delete tag
|
|
76
|
+
if args.delete:
|
|
77
|
+
if not args.name:
|
|
78
|
+
print("Error: Tag name required for deletion")
|
|
79
|
+
return 1
|
|
80
|
+
|
|
81
|
+
if repo.refs.delete_tag(args.name):
|
|
82
|
+
print(f"Deleted tag '{args.name}'")
|
|
83
|
+
return 0
|
|
84
|
+
else:
|
|
85
|
+
print(f"Error: Tag '{args.name}' not found")
|
|
86
|
+
return 1
|
|
87
|
+
|
|
88
|
+
# Create tag
|
|
89
|
+
if args.name:
|
|
90
|
+
# Check if tag exists
|
|
91
|
+
if repo.refs.tag_exists(args.name) and not args.force:
|
|
92
|
+
print(f"Error: Tag '{args.name}' already exists")
|
|
93
|
+
print("Use -f to force replace")
|
|
94
|
+
return 1
|
|
95
|
+
|
|
96
|
+
# Get commit to tag
|
|
97
|
+
commit_ref = args.commit or 'HEAD'
|
|
98
|
+
commit_hash = repo.resolve_ref(commit_ref)
|
|
99
|
+
|
|
100
|
+
if not commit_hash:
|
|
101
|
+
print(f"Error: Unknown revision: {commit_ref}")
|
|
102
|
+
return 1
|
|
103
|
+
|
|
104
|
+
# Delete existing tag if forcing
|
|
105
|
+
if args.force and repo.refs.tag_exists(args.name):
|
|
106
|
+
repo.refs.delete_tag(args.name)
|
|
107
|
+
|
|
108
|
+
# Create tag
|
|
109
|
+
message = args.message or f"Tag {args.name}"
|
|
110
|
+
if repo.refs.create_tag(args.name, commit_hash, message):
|
|
111
|
+
print(f"Created tag '{args.name}' at {commit_hash[:8]}")
|
|
112
|
+
return 0
|
|
113
|
+
else:
|
|
114
|
+
print(f"Error: Could not create tag '{args.name}'")
|
|
115
|
+
return 1
|
|
116
|
+
|
|
117
|
+
return 0
|
memvcs/commands/test.py
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""
|
|
2
|
+
agmem test - Run memory regression tests.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from ..commands.base import require_repo
|
|
9
|
+
from ..core.test_runner import TestRunner, create_test_template
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class TestCommand:
|
|
13
|
+
"""Run memory regression tests."""
|
|
14
|
+
|
|
15
|
+
name = 'test'
|
|
16
|
+
help = 'Run memory regression tests to validate knowledge consistency'
|
|
17
|
+
|
|
18
|
+
@staticmethod
|
|
19
|
+
def add_arguments(parser: argparse.ArgumentParser):
|
|
20
|
+
parser.add_argument(
|
|
21
|
+
'--branch',
|
|
22
|
+
help='Run tests against a specific branch'
|
|
23
|
+
)
|
|
24
|
+
parser.add_argument(
|
|
25
|
+
'--tags',
|
|
26
|
+
nargs='+',
|
|
27
|
+
help='Filter tests by tags'
|
|
28
|
+
)
|
|
29
|
+
parser.add_argument(
|
|
30
|
+
'--init',
|
|
31
|
+
action='store_true',
|
|
32
|
+
help='Initialize tests directory with template'
|
|
33
|
+
)
|
|
34
|
+
parser.add_argument(
|
|
35
|
+
'-v', '--verbose',
|
|
36
|
+
action='store_true',
|
|
37
|
+
help='Show detailed test output'
|
|
38
|
+
)
|
|
39
|
+
parser.add_argument(
|
|
40
|
+
'--fail-fast',
|
|
41
|
+
action='store_true',
|
|
42
|
+
help='Stop on first failure'
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
@staticmethod
|
|
46
|
+
def execute(args) -> int:
|
|
47
|
+
repo, code = require_repo()
|
|
48
|
+
if code != 0:
|
|
49
|
+
return code
|
|
50
|
+
|
|
51
|
+
# Handle --init
|
|
52
|
+
if args.init:
|
|
53
|
+
return TestCommand._init_tests(repo)
|
|
54
|
+
|
|
55
|
+
# Try to get vector store
|
|
56
|
+
vector_store = None
|
|
57
|
+
try:
|
|
58
|
+
from ..core.vector_store import VectorStore
|
|
59
|
+
vector_store = VectorStore(repo.root / '.mem')
|
|
60
|
+
except ImportError:
|
|
61
|
+
if args.verbose:
|
|
62
|
+
print("Note: Vector store not available, using text-based tests")
|
|
63
|
+
except Exception as e:
|
|
64
|
+
if args.verbose:
|
|
65
|
+
print(f"Note: Could not initialize vector store: {e}")
|
|
66
|
+
|
|
67
|
+
# Create test runner
|
|
68
|
+
runner = TestRunner(repo, vector_store)
|
|
69
|
+
|
|
70
|
+
# Load and check for tests
|
|
71
|
+
tests = runner.load_tests()
|
|
72
|
+
if not tests:
|
|
73
|
+
print("No tests found.")
|
|
74
|
+
print("Create test files in tests/ directory or run 'agmem test --init'")
|
|
75
|
+
return 0
|
|
76
|
+
|
|
77
|
+
print(f"Running {len(tests)} tests...")
|
|
78
|
+
|
|
79
|
+
# Run tests
|
|
80
|
+
if args.branch:
|
|
81
|
+
result = runner.run_for_branch(args.branch)
|
|
82
|
+
else:
|
|
83
|
+
result = runner.run_all(tags=args.tags)
|
|
84
|
+
|
|
85
|
+
# Print results
|
|
86
|
+
print()
|
|
87
|
+
|
|
88
|
+
if result.failures:
|
|
89
|
+
print("Failed tests:")
|
|
90
|
+
for failure in result.failures:
|
|
91
|
+
critical_marker = " [CRITICAL]" if failure.is_critical else ""
|
|
92
|
+
print(f" ✗ {failure.test_name}{critical_marker}")
|
|
93
|
+
if args.verbose:
|
|
94
|
+
print(f" Query: {failure.query}")
|
|
95
|
+
print(f" Expected: {failure.expected}")
|
|
96
|
+
if failure.actual:
|
|
97
|
+
print(f" Got: {failure.actual[:100]}...")
|
|
98
|
+
print(f" Error: {failure.message}")
|
|
99
|
+
print()
|
|
100
|
+
|
|
101
|
+
# Summary
|
|
102
|
+
status = "PASSED" if result.passed else "FAILED"
|
|
103
|
+
critical_failures = [f for f in result.failures if f.is_critical]
|
|
104
|
+
|
|
105
|
+
print(f"{'='*50}")
|
|
106
|
+
print(f"Results: {result.passed_count}/{result.total_count} tests passed")
|
|
107
|
+
if critical_failures:
|
|
108
|
+
print(f"Critical failures: {len(critical_failures)}")
|
|
109
|
+
print(f"Duration: {result.duration_ms}ms")
|
|
110
|
+
print(f"Status: {status}")
|
|
111
|
+
print(f"{'='*50}")
|
|
112
|
+
|
|
113
|
+
return 0 if result.passed else 1
|
|
114
|
+
|
|
115
|
+
@staticmethod
|
|
116
|
+
def _init_tests(repo) -> int:
|
|
117
|
+
"""Initialize tests directory with template."""
|
|
118
|
+
tests_dir = repo.root / 'tests'
|
|
119
|
+
tests_dir.mkdir(exist_ok=True)
|
|
120
|
+
|
|
121
|
+
template_file = tests_dir / 'example_tests.yaml'
|
|
122
|
+
|
|
123
|
+
if template_file.exists():
|
|
124
|
+
print(f"Test template already exists: {template_file}")
|
|
125
|
+
return 0
|
|
126
|
+
|
|
127
|
+
template_file.write_text(create_test_template())
|
|
128
|
+
print(f"Created test template: {template_file}")
|
|
129
|
+
print("\nEdit this file to add your memory tests.")
|
|
130
|
+
print("Run 'agmem test' to execute them.")
|
|
131
|
+
|
|
132
|
+
return 0
|