agmem 0.1.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. agmem-0.1.1.dist-info/METADATA +656 -0
  2. agmem-0.1.1.dist-info/RECORD +67 -0
  3. agmem-0.1.1.dist-info/WHEEL +5 -0
  4. agmem-0.1.1.dist-info/entry_points.txt +2 -0
  5. agmem-0.1.1.dist-info/licenses/LICENSE +21 -0
  6. agmem-0.1.1.dist-info/top_level.txt +1 -0
  7. memvcs/__init__.py +9 -0
  8. memvcs/cli.py +178 -0
  9. memvcs/commands/__init__.py +23 -0
  10. memvcs/commands/add.py +258 -0
  11. memvcs/commands/base.py +23 -0
  12. memvcs/commands/blame.py +169 -0
  13. memvcs/commands/branch.py +110 -0
  14. memvcs/commands/checkout.py +101 -0
  15. memvcs/commands/clean.py +76 -0
  16. memvcs/commands/clone.py +91 -0
  17. memvcs/commands/commit.py +174 -0
  18. memvcs/commands/daemon.py +267 -0
  19. memvcs/commands/diff.py +157 -0
  20. memvcs/commands/fsck.py +203 -0
  21. memvcs/commands/garden.py +107 -0
  22. memvcs/commands/graph.py +151 -0
  23. memvcs/commands/init.py +61 -0
  24. memvcs/commands/log.py +103 -0
  25. memvcs/commands/mcp.py +59 -0
  26. memvcs/commands/merge.py +88 -0
  27. memvcs/commands/pull.py +65 -0
  28. memvcs/commands/push.py +143 -0
  29. memvcs/commands/reflog.py +52 -0
  30. memvcs/commands/remote.py +51 -0
  31. memvcs/commands/reset.py +98 -0
  32. memvcs/commands/search.py +163 -0
  33. memvcs/commands/serve.py +54 -0
  34. memvcs/commands/show.py +125 -0
  35. memvcs/commands/stash.py +97 -0
  36. memvcs/commands/status.py +112 -0
  37. memvcs/commands/tag.py +117 -0
  38. memvcs/commands/test.py +132 -0
  39. memvcs/commands/tree.py +156 -0
  40. memvcs/core/__init__.py +21 -0
  41. memvcs/core/config_loader.py +245 -0
  42. memvcs/core/constants.py +12 -0
  43. memvcs/core/diff.py +380 -0
  44. memvcs/core/gardener.py +466 -0
  45. memvcs/core/hooks.py +151 -0
  46. memvcs/core/knowledge_graph.py +381 -0
  47. memvcs/core/merge.py +474 -0
  48. memvcs/core/objects.py +323 -0
  49. memvcs/core/pii_scanner.py +343 -0
  50. memvcs/core/refs.py +447 -0
  51. memvcs/core/remote.py +278 -0
  52. memvcs/core/repository.py +522 -0
  53. memvcs/core/schema.py +414 -0
  54. memvcs/core/staging.py +227 -0
  55. memvcs/core/storage/__init__.py +72 -0
  56. memvcs/core/storage/base.py +359 -0
  57. memvcs/core/storage/gcs.py +308 -0
  58. memvcs/core/storage/local.py +182 -0
  59. memvcs/core/storage/s3.py +369 -0
  60. memvcs/core/test_runner.py +371 -0
  61. memvcs/core/vector_store.py +313 -0
  62. memvcs/integrations/__init__.py +5 -0
  63. memvcs/integrations/mcp_server.py +267 -0
  64. memvcs/integrations/web_ui/__init__.py +1 -0
  65. memvcs/integrations/web_ui/server.py +352 -0
  66. memvcs/utils/__init__.py +9 -0
  67. memvcs/utils/helpers.py +178 -0
@@ -0,0 +1,169 @@
1
+ """
2
+ agmem blame - Show who changed each line (Git-like) or trace semantic facts.
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
+ from ..core.objects import Commit, Tree, Blob
11
+
12
+
13
+ class BlameCommand:
14
+ """Show author and commit for each line of a file, or trace semantic facts."""
15
+
16
+ name = 'blame'
17
+ help = 'Show who changed each line of a memory file, or trace semantic facts'
18
+
19
+ @staticmethod
20
+ def add_arguments(parser: argparse.ArgumentParser):
21
+ parser.add_argument(
22
+ 'file',
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)'
31
+ )
32
+ parser.add_argument(
33
+ '--query', '-q',
34
+ help='Semantic query to trace (e.g., "Why does agent think X?")'
35
+ )
36
+ parser.add_argument(
37
+ '--limit', '-n',
38
+ type=int,
39
+ default=5,
40
+ help='Number of results to show for semantic blame (default: 5)'
41
+ )
42
+
43
+ @staticmethod
44
+ def execute(args) -> int:
45
+ repo, code = require_repo()
46
+ if code != 0:
47
+ return code
48
+
49
+ # Semantic blame mode
50
+ if args.query:
51
+ return BlameCommand._semantic_blame(repo, args.query, args.limit)
52
+
53
+ # File blame mode
54
+ if not args.file:
55
+ print("Error: Either --query or a file path is required.")
56
+ print("Usage: agmem blame <file> [ref]")
57
+ print(" agmem blame --query \"Why does agent think X?\"")
58
+ return 1
59
+
60
+ return BlameCommand._file_blame(repo, args.file, args.ref)
61
+
62
+ @staticmethod
63
+ def _file_blame(repo, filepath: str, ref: str) -> int:
64
+ """Traditional file-based blame."""
65
+ commit_hash = repo.resolve_ref(ref)
66
+ if not commit_hash:
67
+ print(f"Error: Unknown revision: {ref}")
68
+ return 1
69
+
70
+ # Get file content at commit
71
+ tree = repo.get_commit_tree(commit_hash)
72
+ if not tree:
73
+ print("Error: Could not load tree.")
74
+ return 1
75
+
76
+ # Find file in tree (support path like semantic/user-prefs.md)
77
+ blob_hash = None
78
+ for entry in tree.entries:
79
+ path = entry.path + '/' + entry.name if entry.path else entry.name
80
+ if path == filepath:
81
+ blob_hash = entry.hash
82
+ break
83
+
84
+ if not blob_hash:
85
+ print(f"Error: File not found in {ref}: {filepath}")
86
+ return 1
87
+
88
+ blob = Blob.load(repo.object_store, blob_hash)
89
+ if not blob:
90
+ print("Error: Could not load file content.")
91
+ return 1
92
+
93
+ lines = blob.content.decode('utf-8', errors='replace').splitlines()
94
+ commit = Commit.load(repo.object_store, commit_hash)
95
+ author_short = commit.author.split('<')[0].strip()[:20] if commit else 'unknown'
96
+ hash_short = commit_hash[:8]
97
+
98
+ for i, line in enumerate(lines, 1):
99
+ print(f"{hash_short} ({author_short:20} {i:4}) {line}")
100
+
101
+ return 0
102
+
103
+ @staticmethod
104
+ def _semantic_blame(repo, query: str, limit: int) -> int:
105
+ """
106
+ Semantic blame - trace which commit introduced a fact.
107
+
108
+ Searches the vector store and shows provenance for matching chunks.
109
+ """
110
+ try:
111
+ from ..core.vector_store import VectorStore
112
+ except ImportError:
113
+ print("Error: Vector search requires sqlite-vec.")
114
+ print("Install with: pip install agmem[vector]")
115
+ return 1
116
+
117
+ try:
118
+ vs = VectorStore(repo.root / '.mem')
119
+ results = vs.search_with_provenance(query, limit=limit)
120
+ except Exception as e:
121
+ print(f"Error: Vector search failed: {e}")
122
+ print("Try running 'agmem search --rebuild' to rebuild the index.")
123
+ return 1
124
+
125
+ if not results:
126
+ print("No matching facts found in memory.")
127
+ print("Try rebuilding the index with 'agmem search --rebuild'")
128
+ return 0
129
+
130
+ print(f"Semantic blame for: \"{query}\"")
131
+ print("=" * 60)
132
+
133
+ for i, result in enumerate(results, 1):
134
+ path = result['path']
135
+ content = result['content']
136
+ similarity = result['similarity']
137
+ commit_hash = result['commit_hash']
138
+ author = result['author']
139
+ indexed_at = result['indexed_at']
140
+
141
+ print(f"\n[{i}] {path}")
142
+ print(f" Similarity: {similarity:.2%}")
143
+
144
+ if commit_hash:
145
+ # Try to get commit details
146
+ commit = Commit.load(repo.object_store, commit_hash)
147
+ if commit:
148
+ print(f" Commit: {commit_hash[:8]}")
149
+ print(f" Author: {commit.author}")
150
+ print(f" Date: {commit.timestamp}")
151
+ print(f" Message: {commit.message}")
152
+ else:
153
+ print(f" Commit: {commit_hash[:8]} (details unavailable)")
154
+ if author:
155
+ print(f" Author: {author}")
156
+ else:
157
+ print(" Commit: (not tracked)")
158
+ if indexed_at:
159
+ print(f" Indexed: {indexed_at}")
160
+
161
+ # Show content preview
162
+ print(f"\n Content preview:")
163
+ for line in content.split('\n')[:5]:
164
+ print(f" {line[:70]}")
165
+ if len(content.split('\n')) > 5:
166
+ print(" ...")
167
+
168
+ print()
169
+ return 0
@@ -0,0 +1,110 @@
1
+ """
2
+ agmem branch - List, create, or delete branches.
3
+ """
4
+
5
+ import argparse
6
+
7
+ from ..commands.base import require_repo
8
+ from ..core.repository import Repository
9
+
10
+
11
+ class BranchCommand:
12
+ """Manage branches."""
13
+
14
+ name = 'branch'
15
+ help = 'List, create, or delete branches'
16
+
17
+ @staticmethod
18
+ def add_arguments(parser: argparse.ArgumentParser):
19
+ parser.add_argument(
20
+ 'name',
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'
28
+ )
29
+ parser.add_argument(
30
+ '--force', '-D',
31
+ action='store_true',
32
+ help='Force delete a branch (even if not merged)'
33
+ )
34
+ parser.add_argument(
35
+ '--list', '-l',
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
+
50
+ @staticmethod
51
+ def execute(args) -> int:
52
+ repo, code = require_repo()
53
+ if code != 0:
54
+ return code
55
+
56
+ if args.list or (not args.name and not args.delete):
57
+ return BranchCommand._list_branches(repo)
58
+
59
+ if args.delete or args.force:
60
+ if not args.name:
61
+ print("Error: Branch name required for deletion")
62
+ return 1
63
+
64
+ current = repo.refs.get_current_branch()
65
+ if args.name == current:
66
+ print(f"Error: Cannot delete current branch '{args.name}'")
67
+ print("Switch to another branch first.")
68
+ return 1
69
+
70
+ if repo.refs.delete_branch(args.name):
71
+ print(f"Deleted branch {args.name}")
72
+ return 0
73
+ print(f"Error: Branch '{args.name}' not found")
74
+ return 1
75
+
76
+ if args.name:
77
+ if repo.refs.branch_exists(args.name):
78
+ print(f"Error: A branch named '{args.name}' already exists.")
79
+ return 1
80
+
81
+ start_commit = None
82
+ if args.start_point:
83
+ start_commit = repo.resolve_ref(args.start_point)
84
+ if not start_commit:
85
+ print(f"Error: Not a valid object name: '{args.start_point}'")
86
+ return 1
87
+
88
+ if repo.refs.create_branch(args.name, start_commit):
89
+ print(f"Created branch {args.name}")
90
+ head = repo.refs.get_head()
91
+ if head.get("type") == "branch":
92
+ print(f" (based on {head['value']})")
93
+ return 0
94
+ print(f"Error: Could not create branch '{args.name}'")
95
+ return 1
96
+
97
+ return 0
98
+
99
+ @staticmethod
100
+ def _list_branches(repo) -> int:
101
+ """List all branches with current branch marked."""
102
+ branches = repo.refs.list_branches()
103
+ current = repo.refs.get_current_branch()
104
+ if not branches:
105
+ print("No branches yet.")
106
+ return 0
107
+ for branch in branches:
108
+ prefix = "* " if branch == current else " "
109
+ print(f"{prefix}{branch}")
110
+ return 0
@@ -0,0 +1,101 @@
1
+ """
2
+ agmem checkout - Switch branches or restore files.
3
+ """
4
+
5
+ import argparse
6
+
7
+ from ..commands.base import require_repo
8
+ from ..core.repository import Repository
9
+
10
+
11
+ class CheckoutCommand:
12
+ """Switch branches or restore working tree files."""
13
+
14
+ name = 'checkout'
15
+ help = 'Switch branches or restore working tree files'
16
+
17
+ @staticmethod
18
+ def add_arguments(parser: argparse.ArgumentParser):
19
+ parser.add_argument(
20
+ 'ref',
21
+ help='Branch, tag, or commit to checkout'
22
+ )
23
+ parser.add_argument(
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
+
34
+ @staticmethod
35
+ def execute(args) -> int:
36
+ repo, code = require_repo()
37
+ if code != 0:
38
+ return code
39
+
40
+ # Create and checkout new branch
41
+ if args.b:
42
+ branch_name = args.ref
43
+
44
+ # Check if branch already exists
45
+ if repo.refs.branch_exists(branch_name):
46
+ print(f"Error: A branch named '{branch_name}' already exists.")
47
+ return 1
48
+
49
+ # Get current HEAD commit
50
+ head = repo.refs.get_head()
51
+ if head['type'] == 'branch':
52
+ current_commit = repo.refs.get_branch_commit(head['value'])
53
+ else:
54
+ current_commit = head['value']
55
+
56
+ # Create branch
57
+ if not repo.refs.create_branch(branch_name, current_commit):
58
+ print(f"Error: Could not create branch '{branch_name}'")
59
+ return 1
60
+
61
+ # Switch to new branch
62
+ try:
63
+ repo.refs.set_head_branch(branch_name)
64
+ print(f"Switched to a new branch '{branch_name}'")
65
+ return 0
66
+ except Exception as e:
67
+ print(f"Error switching to branch: {e}")
68
+ return 1
69
+
70
+ # Regular checkout
71
+ ref = args.ref
72
+
73
+ # Check if it's a branch
74
+ is_branch = repo.refs.branch_exists(ref)
75
+
76
+ try:
77
+ commit_hash = repo.checkout(ref, force=args.force)
78
+
79
+ if is_branch:
80
+ print(f"Switched to branch '{ref}'")
81
+ else:
82
+ # Check if it's a tag
83
+ if repo.refs.tag_exists(ref):
84
+ print(f"Note: checking out '{ref}'.")
85
+ print()
86
+ print("You are in 'detached HEAD' state. You can look around, make experimental")
87
+ print("changes and commit them, and you can discard any commits you make in this")
88
+ print("state without impacting any branches by switching back to a branch.")
89
+ else:
90
+ print(f"Note: checking out '{commit_hash[:8]}'.")
91
+ print()
92
+ print("You are in 'detached HEAD' state.")
93
+
94
+ return 0
95
+
96
+ except ValueError as e:
97
+ print(f"Error: {e}")
98
+ return 1
99
+ except Exception as e:
100
+ print(f"Error during checkout: {e}")
101
+ return 1
@@ -0,0 +1,76 @@
1
+ """
2
+ agmem clean - Remove untracked files (Git-like).
3
+ """
4
+
5
+ import argparse
6
+ import os
7
+ from pathlib import Path
8
+
9
+ from ..commands.base import require_repo
10
+ from ..core.repository import Repository
11
+
12
+
13
+ class CleanCommand:
14
+ """Remove untracked files from working directory."""
15
+
16
+ name = 'clean'
17
+ help = 'Remove untracked files from working directory'
18
+
19
+ @staticmethod
20
+ def add_arguments(parser: argparse.ArgumentParser):
21
+ parser.add_argument(
22
+ '-n', '--dry-run',
23
+ action='store_true',
24
+ help='Show what would be removed without removing'
25
+ )
26
+ parser.add_argument(
27
+ '-f', '--force',
28
+ action='store_true',
29
+ help='Required to actually remove files'
30
+ )
31
+ parser.add_argument(
32
+ '-d',
33
+ action='store_true',
34
+ help='Remove untracked directories too'
35
+ )
36
+
37
+ @staticmethod
38
+ def execute(args) -> int:
39
+ repo, code = require_repo()
40
+ if code != 0:
41
+ return code
42
+
43
+
44
+ status = repo.get_status()
45
+ untracked = status.get('untracked', [])
46
+
47
+ if not untracked:
48
+ print("Nothing to clean.")
49
+ return 0
50
+
51
+ if args.dry_run:
52
+ print("Would remove:")
53
+ for p in untracked:
54
+ print(f" {p}")
55
+ return 0
56
+
57
+ if not args.force:
58
+ print("Use -f to force removal of untracked files.")
59
+ return 1
60
+
61
+ removed = 0
62
+ for rel_path in untracked:
63
+ full_path = repo.current_dir / rel_path
64
+ if full_path.exists():
65
+ if full_path.is_file():
66
+ full_path.unlink()
67
+ removed += 1
68
+ print(f"Removed {rel_path}")
69
+ elif args.d and full_path.is_dir():
70
+ import shutil
71
+ shutil.rmtree(full_path)
72
+ removed += 1
73
+ print(f"Removed {rel_path}/")
74
+
75
+ print(f"Removed {removed} file(s)")
76
+ return 0
@@ -0,0 +1,91 @@
1
+ """
2
+ agmem clone - Clone a remote memory repository.
3
+ """
4
+
5
+ import argparse
6
+ import shutil
7
+ from pathlib import Path
8
+
9
+ from memvcs.core.constants import MEMORY_TYPES
10
+
11
+
12
+ class CloneCommand:
13
+ """Clone a remote agmem repository."""
14
+
15
+ name = "clone"
16
+ help = "Clone a memory repository from a remote (file:// URL)"
17
+
18
+ @staticmethod
19
+ def add_arguments(parser: argparse.ArgumentParser):
20
+ parser.add_argument(
21
+ "url",
22
+ help="Remote URL (e.g. file:///path/to/remote-repo)",
23
+ )
24
+ parser.add_argument(
25
+ "directory",
26
+ nargs="?",
27
+ help="Local directory to clone into (default: infer from remote)",
28
+ )
29
+
30
+ @staticmethod
31
+ def execute(args) -> int:
32
+ from memvcs.core.remote import parse_remote_url
33
+ from memvcs.core.repository import Repository
34
+
35
+ url = args.url
36
+ if not url:
37
+ print("Error: URL required")
38
+ return 1
39
+
40
+ try:
41
+ remote_path = parse_remote_url(url)
42
+ except ValueError as e:
43
+ print(f"Error: {e}")
44
+ return 1
45
+
46
+ remote_mem = remote_path / ".mem"
47
+ if not remote_mem.exists():
48
+ print(f"Error: Not an agmem repository: {remote_path}")
49
+ return 1
50
+
51
+ # Target directory (validate relative paths to avoid path traversal)
52
+ cwd = Path.cwd().resolve()
53
+ if args.directory:
54
+ p = Path(args.directory)
55
+ target = p.resolve()
56
+ if not p.is_absolute():
57
+ try:
58
+ target.relative_to(cwd)
59
+ except ValueError:
60
+ print("Error: Target path escapes current directory")
61
+ return 1
62
+ else:
63
+ target = cwd / remote_path.name
64
+
65
+ if target.exists() and any(target.iterdir()):
66
+ print(f"Error: Directory not empty: {target}")
67
+ return 1
68
+
69
+ target.mkdir(parents=True, exist_ok=True)
70
+
71
+ # Copy .mem and current from remote
72
+ shutil.copytree(remote_mem, target / ".mem")
73
+ remote_current = remote_path / "current"
74
+ if remote_current.exists():
75
+ shutil.copytree(remote_current, target / "current")
76
+ else:
77
+ (target / "current").mkdir(parents=True)
78
+ for mem_type in MEMORY_TYPES:
79
+ (target / "current" / mem_type).mkdir(exist_ok=True)
80
+
81
+ # Set remote origin to source
82
+ import json
83
+ config_file = target / ".mem" / "config.json"
84
+ config = json.loads(config_file.read_text()) if config_file.exists() else {}
85
+ if "remotes" not in config:
86
+ config["remotes"] = {}
87
+ config["remotes"]["origin"] = {"url": url if url.startswith("file://") else f"file://{remote_path}"}
88
+ config_file.write_text(json.dumps(config, indent=2))
89
+
90
+ print(f"Cloned into {target}")
91
+ return 0