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/fsck.py
ADDED
|
@@ -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
|
memvcs/commands/graph.py
ADDED
|
@@ -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
|
memvcs/commands/init.py
ADDED
|
@@ -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
|