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,203 @@
1
+ """
2
+ agmem fsck - File system consistency check.
3
+ """
4
+
5
+ import argparse
6
+ from pathlib import Path
7
+
8
+ from ..commands.base import require_repo
9
+
10
+
11
+ class FsckCommand:
12
+ """Check and repair repository consistency."""
13
+
14
+ name = 'fsck'
15
+ help = 'Check and repair repository consistency (remove dangling vectors)'
16
+
17
+ @staticmethod
18
+ def add_arguments(parser: argparse.ArgumentParser):
19
+ parser.add_argument(
20
+ '--dry-run',
21
+ action='store_true',
22
+ help='Show what would be done without making changes'
23
+ )
24
+ parser.add_argument(
25
+ '--verbose', '-v',
26
+ action='store_true',
27
+ help='Show detailed output'
28
+ )
29
+ parser.add_argument(
30
+ '--fix',
31
+ action='store_true',
32
+ help='Actually remove dangling entries (required to make changes)'
33
+ )
34
+
35
+ @staticmethod
36
+ def execute(args) -> int:
37
+ repo, code = require_repo()
38
+ if code != 0:
39
+ return code
40
+
41
+ print("Running file system consistency check...")
42
+
43
+ issues_found = 0
44
+ issues_fixed = 0
45
+
46
+ # Check vector store for dangling entries
47
+ try:
48
+ from ..core.vector_store import VectorStore
49
+ vs = VectorStore(repo.root / '.mem')
50
+
51
+ vector_issues, vector_fixed = FsckCommand._check_vectors(
52
+ repo, vs, args.dry_run, args.verbose, args.fix
53
+ )
54
+ issues_found += vector_issues
55
+ issues_fixed += vector_fixed
56
+ except ImportError:
57
+ if args.verbose:
58
+ print("Vector store not available, skipping vector check")
59
+ except Exception as e:
60
+ print(f"Warning: Vector store check failed: {e}")
61
+
62
+ # Check object store integrity
63
+ obj_issues, obj_fixed = FsckCommand._check_objects(
64
+ repo, args.dry_run, args.verbose, args.fix
65
+ )
66
+ issues_found += obj_issues
67
+ issues_fixed += obj_fixed
68
+
69
+ # Check refs integrity
70
+ ref_issues, ref_fixed = FsckCommand._check_refs(
71
+ repo, args.dry_run, args.verbose, args.fix
72
+ )
73
+ issues_found += ref_issues
74
+ issues_fixed += ref_fixed
75
+
76
+ # Print summary
77
+ print()
78
+ print("=" * 40)
79
+ print("FSCK Summary")
80
+ print("=" * 40)
81
+ print(f"Issues found: {issues_found}")
82
+
83
+ if args.fix:
84
+ print(f"Issues fixed: {issues_fixed}")
85
+ elif issues_found > 0:
86
+ print("\nRun with --fix to repair issues")
87
+
88
+ if issues_found == 0:
89
+ print("Repository is healthy!")
90
+
91
+ return 0 if issues_found == 0 else 1
92
+
93
+ @staticmethod
94
+ def _check_vectors(repo, vs, dry_run: bool, verbose: bool, fix: bool) -> tuple:
95
+ """Check for dangling vector entries."""
96
+ print("\nChecking vector store...")
97
+
98
+ current_dir = repo.root / 'current'
99
+ entries = vs.get_all_entries()
100
+
101
+ dangling = []
102
+
103
+ for entry in entries:
104
+ path = entry['path']
105
+ full_path = current_dir / path
106
+
107
+ if not full_path.exists():
108
+ dangling.append(entry)
109
+ if verbose:
110
+ print(f" Dangling: {path} (rowid: {entry['rowid']})")
111
+
112
+ if dangling:
113
+ print(f" Found {len(dangling)} dangling vector entries")
114
+
115
+ if fix and not dry_run:
116
+ fixed = 0
117
+ for entry in dangling:
118
+ if vs.delete_entry(entry['rowid']):
119
+ fixed += 1
120
+ print(f" Removed {fixed} dangling entries")
121
+ return len(dangling), fixed
122
+ elif dry_run:
123
+ print(" (dry-run: no changes made)")
124
+ else:
125
+ print(" Vector store is consistent")
126
+
127
+ return len(dangling), 0
128
+
129
+ @staticmethod
130
+ def _check_objects(repo, dry_run: bool, verbose: bool, fix: bool) -> tuple:
131
+ """Check object store integrity."""
132
+ print("\nChecking object store...")
133
+
134
+ issues = 0
135
+
136
+ # Check if all referenced blobs exist
137
+ for obj_type in ['blob', 'tree', 'commit', 'tag']:
138
+ obj_dir = repo.root / '.mem' / 'objects' / obj_type
139
+ if not obj_dir.exists():
140
+ continue
141
+
142
+ for prefix_dir in obj_dir.iterdir():
143
+ if not prefix_dir.is_dir():
144
+ continue
145
+ for obj_file in prefix_dir.iterdir():
146
+ try:
147
+ # Try to read and decompress
148
+ import zlib
149
+ compressed = obj_file.read_bytes()
150
+ zlib.decompress(compressed)
151
+ except Exception as e:
152
+ issues += 1
153
+ if verbose:
154
+ hash_id = prefix_dir.name + obj_file.name
155
+ print(f" Corrupted {obj_type}: {hash_id[:8]}...")
156
+
157
+ if issues == 0:
158
+ print(" Object store is consistent")
159
+ else:
160
+ print(f" Found {issues} corrupted objects")
161
+
162
+ return issues, 0 # Object repair not implemented
163
+
164
+ @staticmethod
165
+ def _check_refs(repo, dry_run: bool, verbose: bool, fix: bool) -> tuple:
166
+ """Check refs integrity."""
167
+ print("\nChecking refs...")
168
+
169
+ issues = 0
170
+
171
+ # Check if HEAD points to valid commit
172
+ head = repo.refs.get_head()
173
+ if head['type'] == 'branch':
174
+ branch_commit = repo.refs.get_branch_commit(head['value'])
175
+ if not branch_commit:
176
+ issues += 1
177
+ if verbose:
178
+ print(f" HEAD branch '{head['value']}' has no commit")
179
+ elif not repo.object_store.exists(branch_commit, 'commit'):
180
+ issues += 1
181
+ if verbose:
182
+ print(f" HEAD points to missing commit: {branch_commit[:8]}")
183
+ elif head['type'] == 'detached':
184
+ if not repo.object_store.exists(head['value'], 'commit'):
185
+ issues += 1
186
+ if verbose:
187
+ print(f" Detached HEAD points to missing commit")
188
+
189
+ # Check all branches
190
+ branches = repo.refs.list_branches()
191
+ for branch in branches:
192
+ commit_hash = repo.refs.get_branch_commit(branch)
193
+ if commit_hash and not repo.object_store.exists(commit_hash, 'commit'):
194
+ issues += 1
195
+ if verbose:
196
+ print(f" Branch '{branch}' points to missing commit")
197
+
198
+ if issues == 0:
199
+ print(" Refs are consistent")
200
+ else:
201
+ print(f" Found {issues} ref issues")
202
+
203
+ return issues, 0
@@ -0,0 +1,107 @@
1
+ """
2
+ agmem garden - Run the Gardener to synthesize episodic memories into insights.
3
+ """
4
+
5
+ import argparse
6
+
7
+ from ..commands.base import require_repo
8
+ from ..core.gardener import Gardener, GardenerConfig
9
+
10
+
11
+ class GardenCommand:
12
+ """Run the Gardener reflection loop."""
13
+
14
+ name = 'garden'
15
+ help = 'Synthesize episodic memories into semantic insights'
16
+
17
+ @staticmethod
18
+ def add_arguments(parser: argparse.ArgumentParser):
19
+ parser.add_argument(
20
+ '--force',
21
+ action='store_true',
22
+ help='Run even if episode threshold not met'
23
+ )
24
+ parser.add_argument(
25
+ '--threshold',
26
+ type=int,
27
+ default=50,
28
+ help='Number of episodes before auto-triggering (default: 50)'
29
+ )
30
+ parser.add_argument(
31
+ '--dry-run',
32
+ action='store_true',
33
+ help='Show what would be done without making changes'
34
+ )
35
+ parser.add_argument(
36
+ '--no-commit',
37
+ action='store_true',
38
+ help='Do not auto-commit generated insights'
39
+ )
40
+ parser.add_argument(
41
+ '--llm',
42
+ choices=['openai', 'none'],
43
+ default='none',
44
+ help='LLM provider for summarization (default: none)'
45
+ )
46
+ parser.add_argument(
47
+ '--model',
48
+ help='LLM model to use (e.g., gpt-3.5-turbo)'
49
+ )
50
+
51
+ @staticmethod
52
+ def execute(args) -> int:
53
+ repo, code = require_repo()
54
+ if code != 0:
55
+ return code
56
+
57
+ # Build config
58
+ config = GardenerConfig(
59
+ threshold=args.threshold,
60
+ auto_commit=not args.no_commit,
61
+ llm_provider=args.llm if args.llm != 'none' else None,
62
+ llm_model=args.model
63
+ )
64
+
65
+ # Create gardener
66
+ gardener = Gardener(repo, config)
67
+
68
+ # Show status
69
+ episode_count = gardener.get_episode_count()
70
+ print(f"Episodic files: {episode_count}/{config.threshold}")
71
+
72
+ if args.dry_run:
73
+ if gardener.should_run() or args.force:
74
+ episodes = gardener.load_episodes()
75
+ clusters = gardener.cluster_episodes(episodes)
76
+
77
+ print(f"\nWould process {len(episodes)} episodes into {len(clusters)} clusters:")
78
+ for cluster in clusters:
79
+ print(f" - {cluster.topic}: {len(cluster.episodes)} episodes")
80
+
81
+ print("\nRun without --dry-run to execute.")
82
+ else:
83
+ print("\nThreshold not met. Use --force to run anyway.")
84
+ return 0
85
+
86
+ # Run gardener
87
+ if not gardener.should_run() and not args.force:
88
+ print("\nThreshold not met. Use --force to run anyway.")
89
+ return 0
90
+
91
+ print("\nRunning Gardener...")
92
+ result = gardener.run(force=args.force)
93
+
94
+ if result.success:
95
+ print(f"\nGardener completed:")
96
+ print(f" Clusters found: {result.clusters_found}")
97
+ print(f" Insights generated: {result.insights_generated}")
98
+ print(f" Episodes archived: {result.episodes_archived}")
99
+
100
+ if result.commit_hash:
101
+ print(f" Commit: {result.commit_hash[:8]}")
102
+
103
+ print(f"\n{result.message}")
104
+ return 0
105
+ else:
106
+ print(f"Gardener failed: {result.message}")
107
+ return 1
@@ -0,0 +1,151 @@
1
+ """
2
+ agmem graph - Visualize the knowledge graph.
3
+ """
4
+
5
+ import argparse
6
+ import json
7
+ from pathlib import Path
8
+
9
+ from ..commands.base import require_repo
10
+
11
+
12
+ class GraphCommand:
13
+ """Visualize connections between memory files."""
14
+
15
+ name = 'graph'
16
+ help = 'Visualize the knowledge graph of memory files'
17
+
18
+ @staticmethod
19
+ def add_arguments(parser: argparse.ArgumentParser):
20
+ parser.add_argument(
21
+ '--output', '-o',
22
+ help='Output file for graph data (JSON)'
23
+ )
24
+ parser.add_argument(
25
+ '--format',
26
+ choices=['json', 'd3', 'summary'],
27
+ default='summary',
28
+ help='Output format (default: summary)'
29
+ )
30
+ parser.add_argument(
31
+ '--no-similarity',
32
+ action='store_true',
33
+ help='Skip similarity-based edges (faster)'
34
+ )
35
+ parser.add_argument(
36
+ '--threshold',
37
+ type=float,
38
+ default=0.7,
39
+ help='Similarity threshold for edges (default: 0.7)'
40
+ )
41
+ parser.add_argument(
42
+ '--serve',
43
+ action='store_true',
44
+ help='Start web server to view interactive graph'
45
+ )
46
+
47
+ @staticmethod
48
+ def execute(args) -> int:
49
+ repo, code = require_repo()
50
+ if code != 0:
51
+ return code
52
+
53
+ # Try to get vector store for similarity
54
+ vector_store = None
55
+ if not args.no_similarity:
56
+ try:
57
+ from ..core.vector_store import VectorStore
58
+ vector_store = VectorStore(repo.root / '.mem')
59
+ except ImportError:
60
+ print("Note: Vector store not available, skipping similarity edges")
61
+
62
+ # Build graph
63
+ from ..core.knowledge_graph import KnowledgeGraphBuilder
64
+
65
+ builder = KnowledgeGraphBuilder(repo, vector_store)
66
+
67
+ print("Building knowledge graph...")
68
+ graph_data = builder.build_graph(
69
+ include_similarity=not args.no_similarity,
70
+ similarity_threshold=args.threshold
71
+ )
72
+
73
+ if args.serve:
74
+ return GraphCommand._serve_graph(repo, graph_data)
75
+
76
+ if args.format == 'summary':
77
+ GraphCommand._print_summary(graph_data, builder)
78
+
79
+ elif args.format == 'json':
80
+ output = graph_data.to_json()
81
+ if args.output:
82
+ Path(args.output).write_text(output)
83
+ print(f"Graph data written to: {args.output}")
84
+ else:
85
+ print(output)
86
+
87
+ elif args.format == 'd3':
88
+ output = builder.export_for_d3()
89
+ if args.output:
90
+ Path(args.output).write_text(output)
91
+ print(f"D3 graph data written to: {args.output}")
92
+ else:
93
+ print(output)
94
+
95
+ return 0
96
+
97
+ @staticmethod
98
+ def _print_summary(graph_data, builder):
99
+ """Print a text summary of the graph."""
100
+ meta = graph_data.metadata
101
+
102
+ print("\nKnowledge Graph Summary")
103
+ print("=" * 40)
104
+ print(f"Total files: {meta['total_nodes']}")
105
+ print(f"Total connections: {meta['total_edges']}")
106
+
107
+ print("\nBy Memory Type:")
108
+ for mtype, count in meta['memory_types'].items():
109
+ if count > 0:
110
+ print(f" {mtype}: {count}")
111
+
112
+ print("\nBy Edge Type:")
113
+ for etype, count in meta['edge_types'].items():
114
+ if count > 0:
115
+ print(f" {etype}: {count}")
116
+
117
+ # Find isolated nodes
118
+ isolated = builder.find_isolated_nodes()
119
+ if isolated:
120
+ print(f"\nIsolated files (no connections): {len(isolated)}")
121
+ for path in isolated[:5]:
122
+ print(f" - {path}")
123
+ if len(isolated) > 5:
124
+ print(f" ... and {len(isolated) - 5} more")
125
+
126
+ # Find potential contradictions
127
+ contradictions = builder.find_potential_contradictions()
128
+ if contradictions:
129
+ print(f"\nPotential contradictions: {len(contradictions)}")
130
+ for path1, path2, sim in contradictions[:3]:
131
+ print(f" - {path1} <-> {path2} (similarity: {sim:.2%})")
132
+
133
+ print("\nUse --format d3 --output graph.json to export for visualization")
134
+
135
+ @staticmethod
136
+ def _serve_graph(repo, graph_data):
137
+ """Start web server to view interactive graph."""
138
+ try:
139
+ import uvicorn
140
+ from ..integrations.web_ui.server import create_app
141
+ except ImportError:
142
+ print("Error: Web server requires fastapi and uvicorn.")
143
+ print("Install with: pip install agmem[web]")
144
+ return 1
145
+
146
+ print("Starting graph visualization server...")
147
+ print("Open http://localhost:8080/graph in your browser")
148
+
149
+ app = create_app(repo.root)
150
+ uvicorn.run(app, host="127.0.0.1", port=8080)
151
+ return 0
@@ -0,0 +1,61 @@
1
+ """
2
+ agmem init - Initialize a new memory repository.
3
+ """
4
+
5
+ import argparse
6
+ from pathlib import Path
7
+
8
+ from ..core.repository import Repository
9
+
10
+
11
+ class InitCommand:
12
+ """Initialize a new agmem repository."""
13
+
14
+ name = 'init'
15
+ help = 'Initialize a new memory repository'
16
+
17
+ @staticmethod
18
+ def add_arguments(parser: argparse.ArgumentParser):
19
+ parser.add_argument(
20
+ 'path',
21
+ nargs='?',
22
+ default='.',
23
+ help='Directory to initialize repository in (default: current directory)'
24
+ )
25
+ parser.add_argument(
26
+ '--author-name',
27
+ default='Agent',
28
+ help='Default author name'
29
+ )
30
+ parser.add_argument(
31
+ '--author-email',
32
+ default='agent@example.com',
33
+ help='Default author email'
34
+ )
35
+
36
+ @staticmethod
37
+ def execute(args) -> int:
38
+ path = Path(args.path).resolve()
39
+
40
+ try:
41
+ repo = Repository.init(
42
+ path=path,
43
+ author_name=args.author_name,
44
+ author_email=args.author_email
45
+ )
46
+
47
+ print(f"Initialized empty agmem repository in {repo.mem_dir}")
48
+ print(f"Author: {args.author_name} <{args.author_email}>")
49
+ print(f"\nNext steps:")
50
+ print(f" 1. Add memory files to {repo.current_dir}/")
51
+ print(f" 2. Run 'agmem add <file>' to stage changes")
52
+ print(f" 3. Run 'agmem commit -m \"message\"' to save snapshot")
53
+
54
+ return 0
55
+
56
+ except ValueError as e:
57
+ print(f"Error: {e}")
58
+ return 1
59
+ except Exception as e:
60
+ print(f"Error initializing repository: {e}")
61
+ return 1
memvcs/commands/log.py ADDED
@@ -0,0 +1,103 @@
1
+ """
2
+ agmem log - Show commit history.
3
+ """
4
+
5
+ import argparse
6
+ from datetime import datetime
7
+
8
+ from ..commands.base import require_repo
9
+ from ..core.repository import Repository
10
+
11
+
12
+ class LogCommand:
13
+ """Show commit history."""
14
+
15
+ name = 'log'
16
+ help = 'Show commit history'
17
+
18
+ @staticmethod
19
+ def add_arguments(parser: argparse.ArgumentParser):
20
+ parser.add_argument(
21
+ '--max-count', '-n',
22
+ type=int,
23
+ default=10,
24
+ help='Maximum number of commits to show'
25
+ )
26
+ parser.add_argument(
27
+ '--oneline',
28
+ action='store_true',
29
+ help='Show one commit per line'
30
+ )
31
+ parser.add_argument(
32
+ '--graph',
33
+ action='store_true',
34
+ help='Show ASCII graph of branch/merge history'
35
+ )
36
+ parser.add_argument(
37
+ '--all',
38
+ action='store_true',
39
+ help='Show all branches'
40
+ )
41
+ parser.add_argument(
42
+ 'ref',
43
+ nargs='?',
44
+ help='Start from this reference (branch, tag, or commit)'
45
+ )
46
+
47
+ @staticmethod
48
+ def execute(args) -> int:
49
+ repo, code = require_repo()
50
+ if code != 0:
51
+ return code
52
+
53
+ # Get commits
54
+ commits = repo.get_log(max_count=args.max_count)
55
+
56
+ if not commits:
57
+ print("No commits yet.")
58
+ return 0
59
+
60
+ if args.oneline:
61
+ for commit in commits:
62
+ print(f"{commit['short_hash']} {commit['message']}")
63
+ elif args.graph:
64
+ # Simple ASCII graph
65
+ for i, commit in enumerate(commits):
66
+ prefix = "* " if i == 0 else "| "
67
+ print(f"{prefix}{commit['short_hash']} {commit['message']}")
68
+ if i < len(commits) - 1:
69
+ print("|")
70
+ else:
71
+ for i, commit in enumerate(commits):
72
+ if i > 0:
73
+ print()
74
+
75
+ # Commit header
76
+ print(f"\033[33mcommit {commit['hash']}\033[0m")
77
+
78
+ # Show branch info if this is HEAD
79
+ head = repo.refs.get_head()
80
+ if head['type'] == 'branch':
81
+ head_commit = repo.refs.get_branch_commit(head['value'])
82
+ if head_commit == commit['hash']:
83
+ print(f"\033[36mHEAD -> {head['value']}\033[0m")
84
+
85
+ # Author and date
86
+ print(f"Author: {commit['author']}")
87
+
88
+ # Format timestamp
89
+ try:
90
+ ts = commit['timestamp']
91
+ if ts.endswith('Z'):
92
+ ts = ts[:-1]
93
+ dt = datetime.fromisoformat(ts)
94
+ date_str = dt.strftime('%a %b %d %H:%M:%S %Y')
95
+ print(f"Date: {date_str}")
96
+ except:
97
+ print(f"Date: {commit['timestamp']}")
98
+
99
+ # Message
100
+ print()
101
+ print(f" {commit['message']}")
102
+
103
+ return 0
memvcs/commands/mcp.py ADDED
@@ -0,0 +1,59 @@
1
+ """
2
+ agmem mcp - Run MCP server for Cursor/Claude integration.
3
+ """
4
+
5
+ import argparse
6
+ import sys
7
+
8
+
9
+ class McpCommand:
10
+ """Run the agmem MCP server for Cursor/Claude."""
11
+
12
+ name = "mcp"
13
+ help = "Run MCP server for Cursor/Claude memory integration"
14
+
15
+ @staticmethod
16
+ def add_arguments(parser: argparse.ArgumentParser):
17
+ parser.add_argument(
18
+ "--transport",
19
+ choices=["stdio", "streamable-http"],
20
+ default="stdio",
21
+ help="Transport: stdio (default for Cursor/Claude) or streamable-http",
22
+ )
23
+ parser.add_argument(
24
+ "--port",
25
+ type=int,
26
+ default=8000,
27
+ help="Port for streamable-http (default: 8000)",
28
+ )
29
+
30
+ @staticmethod
31
+ def execute(args) -> int:
32
+ try:
33
+ from memvcs.integrations.mcp_server import _create_mcp_server
34
+
35
+ mcp = _create_mcp_server()
36
+ except ImportError as e:
37
+ print(
38
+ "Error: MCP support requires 'mcp' package (Python 3.10+). "
39
+ "Install with: pip install agmem[mcp]",
40
+ file=sys.stderr,
41
+ )
42
+ return 1
43
+
44
+ try:
45
+ if args.transport == "streamable-http":
46
+ mcp.run(transport="streamable-http", port=args.port)
47
+ else:
48
+ mcp.run(transport="stdio")
49
+ except TypeError:
50
+ # Some MCP versions may not support port kwarg
51
+ if args.transport == "streamable-http":
52
+ mcp.run(transport="streamable-http")
53
+ else:
54
+ mcp.run(transport="stdio")
55
+ except Exception as e:
56
+ print(f"Error running MCP server: {e}", file=sys.stderr)
57
+ return 1
58
+
59
+ return 0