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,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
@@ -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
@@ -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
@@ -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