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
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
agmem-0.1.1.dist-info/licenses/LICENSE,sha256=X_S6RBErW-F0IDbM3FAEoDB-zxExFnl2m8640rTXphM,1067
|
|
2
|
+
memvcs/__init__.py,sha256=Gs0A8GAivdcePvlse8yWE3t6-vutFO9tWl5dh8lun6I,193
|
|
3
|
+
memvcs/cli.py,sha256=ImQKXb423bHZ2iPr3GndHVojx2KcTuX8rXHwJV1cku0,5364
|
|
4
|
+
memvcs/commands/__init__.py,sha256=lPKiWp-ywEGk1JPK0DeiHSublDwwSD1pU_LyPT-2BhY,510
|
|
5
|
+
memvcs/commands/add.py,sha256=n0J0N_5Mk4uNMtsEt2podLqSKeenaOBRNWMUb9gsq70,8874
|
|
6
|
+
memvcs/commands/base.py,sha256=yWvIYuofRxbHXvChlSd_DL_hJMaQdbZwa2XBDWj5Bio,634
|
|
7
|
+
memvcs/commands/blame.py,sha256=NbaaL3kt9K2zRvYWX6mh-NmXNt5ZOdacSzHDmGY0_Oo,5874
|
|
8
|
+
memvcs/commands/branch.py,sha256=7ocWZVn-WJ1c9pR9Z493ytd_VE6CY6Lakgl5lIzoNeE,3418
|
|
9
|
+
memvcs/commands/checkout.py,sha256=8-fVF9HC5b0dkyO1u0wkGLUwGXrRBc8eSIDD9XBcnjI,3310
|
|
10
|
+
memvcs/commands/clean.py,sha256=SXQqnd31KwmuIFUc2tBVm8ZjZ_3bP2mhbOR9cPBdbIs,2090
|
|
11
|
+
memvcs/commands/clone.py,sha256=YYYURUiW850CHDgctlM4_ysQlPeh35BrS-UG2LSwWZ0,2872
|
|
12
|
+
memvcs/commands/commit.py,sha256=SmeU0MDIMSYQe9PHNZzOLEK1duvflqWdRMrQXkQ6zGM,6380
|
|
13
|
+
memvcs/commands/daemon.py,sha256=nTaWx19MN1IR4u293yna8fuwhHRiYstykSq_3zoUFPc,8385
|
|
14
|
+
memvcs/commands/diff.py,sha256=TxfeqQS84rG2wHRff_WxdwjsNBKRnMkkX4B-7rsVG9Q,5410
|
|
15
|
+
memvcs/commands/fsck.py,sha256=kvJYO0Kr073qZwGtvVqL1c7n7E34QuNzSc-HG3GpWZQ,6890
|
|
16
|
+
memvcs/commands/garden.py,sha256=P8rvC6xYu6q_8VfXEUvtOa2Wez3jHLGGArGeAbn_o4Y,3496
|
|
17
|
+
memvcs/commands/graph.py,sha256=caPyoo-myqmvtg_HEBUGBgQEnNFQXnCIkiNte4zQhqs,4993
|
|
18
|
+
memvcs/commands/init.py,sha256=2TfBniqqJ6PsyHSIgBmkdtpF-kU9UCsc-V4Ct4BSumM,1767
|
|
19
|
+
memvcs/commands/log.py,sha256=tY0Hrn2xkC1SWG_DT2VGbdK-W-oajrM_1r9NVLoEq5k,3141
|
|
20
|
+
memvcs/commands/mcp.py,sha256=PMfwVD6uHltN58Jh7IOiS1w7oND42tg14QKRCJNudmY,1740
|
|
21
|
+
memvcs/commands/merge.py,sha256=qUsJelBynpdv63tL9Oga3t29AnUQCMMJo_CNl3rNvaw,2631
|
|
22
|
+
memvcs/commands/pull.py,sha256=Sk0zfCloyksYbMGCSM9z91pH1gZJ-KLKZasckQhrNd4,2082
|
|
23
|
+
memvcs/commands/push.py,sha256=bCHuxrrBwRNhVDh9GVwhRICtaNLk_a_47EHix4Z66BQ,5081
|
|
24
|
+
memvcs/commands/reflog.py,sha256=AtfJ2dzhsgJVvdQlHU5LUEiAW310QI8yWeUD7GmLoco,1256
|
|
25
|
+
memvcs/commands/remote.py,sha256=sTXf-r6w4OsZgB68ffW9jUtP1xOL8p-kttxvApZNs08,1760
|
|
26
|
+
memvcs/commands/reset.py,sha256=vvHgSb79zVJizWyH-XN6u5jHK2e_gqrNI0JoIUi22es,2991
|
|
27
|
+
memvcs/commands/search.py,sha256=rg-cQfUDl6aNRFa64szpPbqzw508sL9a2u8YtHBhSRM,5324
|
|
28
|
+
memvcs/commands/serve.py,sha256=mhfsULAtU5Gxe43RVo94RRYT1ui4Zmg5Ptatj5rNIUc,1396
|
|
29
|
+
memvcs/commands/show.py,sha256=525-T_na_B3gX6lmFJ05Q4675XNyEd7FFrVl63X2X7Q,3932
|
|
30
|
+
memvcs/commands/stash.py,sha256=aLeeOLUXcFHQYzsYsi9KIg5e_8VFANHMAEvzaqTOgsg,3293
|
|
31
|
+
memvcs/commands/status.py,sha256=P02DFM2-gIf8-dkslzpnj6xNmpxhqmf8bOhHkxA3srw,3647
|
|
32
|
+
memvcs/commands/tag.py,sha256=UCTggFnzHXVMBYjX8usz58duqg4xyOHnIUdVNyoAlWQ,3409
|
|
33
|
+
memvcs/commands/test.py,sha256=xAIzN60U1xL1TieIXOQBsvTRRfZfElVqxEqqHUn72VU,4235
|
|
34
|
+
memvcs/commands/tree.py,sha256=GzA7RYH3bYgSg9QmztYpis6EZWEt6fzwBltoQjaukf4,5059
|
|
35
|
+
memvcs/core/__init__.py,sha256=dkIC-4tS0GhwV2mZIbofEe8xR8uiFwrxslGf1aXwhYg,493
|
|
36
|
+
memvcs/core/config_loader.py,sha256=ebljEMDyVJjQdlpP8if10Whg0I3DfMUBiuAl7S5NVNE,8264
|
|
37
|
+
memvcs/core/constants.py,sha256=WUjAb50BFcF0mbFi_GNteDLCxLihmViBm9Fb-JMPmbM,220
|
|
38
|
+
memvcs/core/diff.py,sha256=wZynRRAF08HyQ_SCcseC0vlmnkoaqoQfQWne3w1o__M,13390
|
|
39
|
+
memvcs/core/gardener.py,sha256=Fr_Zpo_3eZAtA_9zXVT_PpAHfhZkp2-ukLn69FV93rA,16335
|
|
40
|
+
memvcs/core/hooks.py,sha256=3PWqebAt2c6nPw7ZX3epcb5FGLJLrwxh_UMrQeFTyr0,4679
|
|
41
|
+
memvcs/core/knowledge_graph.py,sha256=Ntm4My73Ov3u9YKEgwvDonUVwewx-svCDJD4hGA0hPc,13572
|
|
42
|
+
memvcs/core/merge.py,sha256=i_X-0ye14r62dvlxRWZVkRbIvxd_MtfQSYwuZQEYaCA,17041
|
|
43
|
+
memvcs/core/objects.py,sha256=MvWhQ-L74Rl2lf0rnJE-N3qo78dWGoclJSQinTloNPg,10289
|
|
44
|
+
memvcs/core/pii_scanner.py,sha256=MOycqGTuLJaCNGkaMyMH1ju8gPxQTyLAONoIML7gHKU,11141
|
|
45
|
+
memvcs/core/refs.py,sha256=1-ELwSXWmqdivD_967zb3hw5V3gIiujyn4eRGf8e1u8,16831
|
|
46
|
+
memvcs/core/remote.py,sha256=MhQTfxpzmH0mAMb7hoQJrTOAoqX0tqZxx1Yq5Q5niS8,10117
|
|
47
|
+
memvcs/core/repository.py,sha256=fZ-UUh9EK9nmM8dk9y4TeipkhgGEp5JqDizR6QAEVe4,18119
|
|
48
|
+
memvcs/core/schema.py,sha256=oiStwqD1crbQJf86yJTMI50efd2SaDG3RIPyUxmcxEk,14322
|
|
49
|
+
memvcs/core/staging.py,sha256=EvACqotgi-LKgOdZr6-8Ud1Lkjnflam0KAzpq8eAAp0,7499
|
|
50
|
+
memvcs/core/test_runner.py,sha256=QfEPMjBebM4L4COn9p0XqMcq1Grvij5rOXByUWAcEAg,11823
|
|
51
|
+
memvcs/core/vector_store.py,sha256=Z_C1VZy3Ytv9dfEaqgE6zCb8nNabLaIsDcc1ATstDRI,10546
|
|
52
|
+
memvcs/core/storage/__init__.py,sha256=b2MLjyFgE6L6JAFoyEldxsrR16FGO65qAtxgRj5XFcM,1959
|
|
53
|
+
memvcs/core/storage/base.py,sha256=hHPXuJPR5X1bh7xYAqACHYRfzU4CmPx9trT5tgRghdI,10384
|
|
54
|
+
memvcs/core/storage/gcs.py,sha256=mjcuQHiK6goQ3opRDE6Ky2fSEA9W-MNTOShlGXhY0cM,10953
|
|
55
|
+
memvcs/core/storage/local.py,sha256=CHi3Ot0-u-pCJJOWKW3IP_UIkdz32QAkHcJl1DxatHY,6193
|
|
56
|
+
memvcs/core/storage/s3.py,sha256=rPxZ7avw5KXMSrJrsA2YpM6gVv7Qhn9IQjfoApZIyxE,14230
|
|
57
|
+
memvcs/integrations/__init__.py,sha256=hVtJoFaXt6ErAZwctcSBDZLXRHFs1CNgtltIBQiroQ0,103
|
|
58
|
+
memvcs/integrations/mcp_server.py,sha256=-si5ymayhiRTIfNJwc4EEp4kudav28x8RjzIEDN-n2c,9091
|
|
59
|
+
memvcs/integrations/web_ui/__init__.py,sha256=MQIfgDKDgPctlcTUjwkwueS_MDsDssVRmIUnpECGS0k,51
|
|
60
|
+
memvcs/integrations/web_ui/server.py,sha256=yqokrs6T9l4vAWdRHKMSFLUIkodTJFWihw5Aq_dhGPg,12782
|
|
61
|
+
memvcs/utils/__init__.py,sha256=h74jw3O37su7BFxp52IcZmVReLfd8yDcuk4VzBHbd_s,185
|
|
62
|
+
memvcs/utils/helpers.py,sha256=4k7Jdm-8vCWDASW2kST2vZs-vA9HX8UvqoCwIGsq49g,4273
|
|
63
|
+
agmem-0.1.1.dist-info/METADATA,sha256=8ovwtZcCCNfSeQA6IH5HfAY0MzQgeqxBo1BfRZIkLIU,25844
|
|
64
|
+
agmem-0.1.1.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
65
|
+
agmem-0.1.1.dist-info/entry_points.txt,sha256=at7eWycgjqOo1wbUMECnXUsNo3gpCkJTU71OzrGLHu0,42
|
|
66
|
+
agmem-0.1.1.dist-info/top_level.txt,sha256=HtMMsKuwLKLOdgF1GxqQztqFM54tTJctVdJuOec6B-4,7
|
|
67
|
+
agmem-0.1.1.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 agmem Team
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
memvcs
|
memvcs/__init__.py
ADDED
memvcs/cli.py
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
agmem - Agentic Memory Version Control System
|
|
4
|
+
|
|
5
|
+
A Git-inspired version control system for AI agent memory artifacts.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
agmem init Initialize a new repository
|
|
9
|
+
agmem add <file> Stage files for commit
|
|
10
|
+
agmem commit -m "message" Save staged changes
|
|
11
|
+
agmem status Show working tree status
|
|
12
|
+
agmem log Show commit history
|
|
13
|
+
agmem branch List branches
|
|
14
|
+
agmem checkout <branch> Switch branches
|
|
15
|
+
agmem merge <branch> Merge branches
|
|
16
|
+
agmem diff Show changes
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import argparse
|
|
20
|
+
import sys
|
|
21
|
+
from typing import List
|
|
22
|
+
|
|
23
|
+
from .commands.init import InitCommand
|
|
24
|
+
from .commands.add import AddCommand
|
|
25
|
+
from .commands.commit import CommitCommand
|
|
26
|
+
from .commands.status import StatusCommand
|
|
27
|
+
from .commands.log import LogCommand
|
|
28
|
+
from .commands.branch import BranchCommand
|
|
29
|
+
from .commands.checkout import CheckoutCommand
|
|
30
|
+
from .commands.merge import MergeCommand
|
|
31
|
+
from .commands.diff import DiffCommand
|
|
32
|
+
from .commands.show import ShowCommand
|
|
33
|
+
from .commands.reset import ResetCommand
|
|
34
|
+
from .commands.tag import TagCommand
|
|
35
|
+
from .commands.tree import TreeCommand
|
|
36
|
+
from .commands.stash import StashCommand
|
|
37
|
+
from .commands.clean import CleanCommand
|
|
38
|
+
from .commands.blame import BlameCommand
|
|
39
|
+
from .commands.reflog import ReflogCommand
|
|
40
|
+
from .commands.mcp import McpCommand
|
|
41
|
+
from .commands.search import SearchCommand
|
|
42
|
+
from .commands.clone import CloneCommand
|
|
43
|
+
from .commands.push import PushCommand
|
|
44
|
+
from .commands.pull import PullCommand
|
|
45
|
+
from .commands.remote import RemoteCommand
|
|
46
|
+
from .commands.serve import ServeCommand
|
|
47
|
+
from .commands.test import TestCommand
|
|
48
|
+
from .commands.fsck import FsckCommand
|
|
49
|
+
from .commands.graph import GraphCommand
|
|
50
|
+
from .commands.daemon import DaemonCommand
|
|
51
|
+
from .commands.garden import GardenCommand
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# List of available commands
|
|
55
|
+
COMMANDS = [
|
|
56
|
+
InitCommand,
|
|
57
|
+
AddCommand,
|
|
58
|
+
CommitCommand,
|
|
59
|
+
StatusCommand,
|
|
60
|
+
LogCommand,
|
|
61
|
+
BranchCommand,
|
|
62
|
+
CheckoutCommand,
|
|
63
|
+
MergeCommand,
|
|
64
|
+
DiffCommand,
|
|
65
|
+
ShowCommand,
|
|
66
|
+
ResetCommand,
|
|
67
|
+
TagCommand,
|
|
68
|
+
TreeCommand,
|
|
69
|
+
StashCommand,
|
|
70
|
+
CleanCommand,
|
|
71
|
+
BlameCommand,
|
|
72
|
+
ReflogCommand,
|
|
73
|
+
McpCommand,
|
|
74
|
+
SearchCommand,
|
|
75
|
+
CloneCommand,
|
|
76
|
+
PushCommand,
|
|
77
|
+
PullCommand,
|
|
78
|
+
RemoteCommand,
|
|
79
|
+
ServeCommand,
|
|
80
|
+
TestCommand,
|
|
81
|
+
FsckCommand,
|
|
82
|
+
GraphCommand,
|
|
83
|
+
DaemonCommand,
|
|
84
|
+
GardenCommand,
|
|
85
|
+
]
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def create_parser() -> argparse.ArgumentParser:
|
|
89
|
+
"""Create the main argument parser."""
|
|
90
|
+
parser = argparse.ArgumentParser(
|
|
91
|
+
prog='agmem',
|
|
92
|
+
description='agmem - Agentic Memory Version Control System',
|
|
93
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
94
|
+
epilog="""
|
|
95
|
+
Examples:
|
|
96
|
+
agmem init Initialize a new repository
|
|
97
|
+
agmem add episodic/session.md Stage a file
|
|
98
|
+
agmem add . Stage all changes
|
|
99
|
+
agmem commit -m "Learned user prefs" Save snapshot
|
|
100
|
+
agmem status Show current status
|
|
101
|
+
agmem log Show commit history
|
|
102
|
+
agmem branch experiment Create a branch
|
|
103
|
+
agmem checkout experiment Switch to branch
|
|
104
|
+
agmem merge experiment Merge branch into current
|
|
105
|
+
agmem diff Show unstaged changes
|
|
106
|
+
agmem diff HEAD~1 HEAD Show changes between commits
|
|
107
|
+
agmem show HEAD Show commit details
|
|
108
|
+
agmem tag v1.0 Create a tag
|
|
109
|
+
agmem reset --hard HEAD~1 Reset to previous commit
|
|
110
|
+
agmem tree Show directory tree visually
|
|
111
|
+
|
|
112
|
+
For more information: https://github.com/vivek-tiwari-vt/agmem
|
|
113
|
+
"""
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
parser.add_argument(
|
|
117
|
+
'--version', '-v',
|
|
118
|
+
action='version',
|
|
119
|
+
version='%(prog)s 0.1.0'
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
parser.add_argument(
|
|
123
|
+
'--verbose',
|
|
124
|
+
action='store_true',
|
|
125
|
+
help='Enable verbose output'
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
# Create subparsers for commands
|
|
129
|
+
subparsers = parser.add_subparsers(
|
|
130
|
+
dest='command',
|
|
131
|
+
help='Available commands',
|
|
132
|
+
metavar='COMMAND'
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
# Add each command
|
|
136
|
+
for cmd_class in COMMANDS:
|
|
137
|
+
cmd_parser = subparsers.add_parser(
|
|
138
|
+
cmd_class.name,
|
|
139
|
+
help=cmd_class.help
|
|
140
|
+
)
|
|
141
|
+
cmd_class.add_arguments(cmd_parser)
|
|
142
|
+
|
|
143
|
+
return parser
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def main(args: List[str] = None) -> int:
|
|
147
|
+
"""Main entry point."""
|
|
148
|
+
parser = create_parser()
|
|
149
|
+
parsed_args = parser.parse_args(args)
|
|
150
|
+
|
|
151
|
+
# No command specified
|
|
152
|
+
if not parsed_args.command:
|
|
153
|
+
parser.print_help()
|
|
154
|
+
return 0
|
|
155
|
+
|
|
156
|
+
# Find and execute the command
|
|
157
|
+
for cmd_class in COMMANDS:
|
|
158
|
+
if cmd_class.name == parsed_args.command:
|
|
159
|
+
try:
|
|
160
|
+
return cmd_class.execute(parsed_args)
|
|
161
|
+
except KeyboardInterrupt:
|
|
162
|
+
print("\nOperation cancelled.")
|
|
163
|
+
return 130
|
|
164
|
+
except Exception as e:
|
|
165
|
+
if parsed_args.verbose:
|
|
166
|
+
import traceback
|
|
167
|
+
traceback.print_exc()
|
|
168
|
+
else:
|
|
169
|
+
print(f"Error: {e}")
|
|
170
|
+
return 1
|
|
171
|
+
|
|
172
|
+
# Unknown command
|
|
173
|
+
print(f"Unknown command: {parsed_args.command}")
|
|
174
|
+
return 1
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
if __name__ == '__main__':
|
|
178
|
+
sys.exit(main())
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""agmem CLI commands."""
|
|
2
|
+
|
|
3
|
+
from .init import InitCommand
|
|
4
|
+
from .add import AddCommand
|
|
5
|
+
from .commit import CommitCommand
|
|
6
|
+
from .status import StatusCommand
|
|
7
|
+
from .log import LogCommand
|
|
8
|
+
from .branch import BranchCommand
|
|
9
|
+
from .checkout import CheckoutCommand
|
|
10
|
+
from .merge import MergeCommand
|
|
11
|
+
from .diff import DiffCommand
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
'InitCommand',
|
|
15
|
+
'AddCommand',
|
|
16
|
+
'CommitCommand',
|
|
17
|
+
'StatusCommand',
|
|
18
|
+
'LogCommand',
|
|
19
|
+
'BranchCommand',
|
|
20
|
+
'CheckoutCommand',
|
|
21
|
+
'MergeCommand',
|
|
22
|
+
'DiffCommand',
|
|
23
|
+
]
|
memvcs/commands/add.py
ADDED
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
"""
|
|
2
|
+
agmem add - Add files to the staging area with file type validation.
|
|
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
|
+
# Default allowed file extensions for memory files
|
|
13
|
+
DEFAULT_ALLOWED_EXTENSIONS = {'.md', '.txt', '.json', '.yaml', '.yml'}
|
|
14
|
+
|
|
15
|
+
# Binary file signatures (magic bytes) to detect binary files
|
|
16
|
+
BINARY_SIGNATURES = [
|
|
17
|
+
b'\x89PNG', # PNG
|
|
18
|
+
b'\xff\xd8\xff', # JPEG
|
|
19
|
+
b'GIF8', # GIF
|
|
20
|
+
b'%PDF', # PDF
|
|
21
|
+
b'PK\x03\x04', # ZIP
|
|
22
|
+
b'\x1f\x8b', # GZIP
|
|
23
|
+
b'BM', # BMP
|
|
24
|
+
b'\x00\x00\x01\x00', # ICO
|
|
25
|
+
b'RIFF', # WAV, AVI, etc.
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class AddCommand:
|
|
30
|
+
"""Add files to the staging area."""
|
|
31
|
+
|
|
32
|
+
name = 'add'
|
|
33
|
+
help = 'Add memory files to staging area'
|
|
34
|
+
|
|
35
|
+
@staticmethod
|
|
36
|
+
def add_arguments(parser: argparse.ArgumentParser):
|
|
37
|
+
parser.add_argument(
|
|
38
|
+
'paths',
|
|
39
|
+
nargs='+',
|
|
40
|
+
help='Files or directories to stage'
|
|
41
|
+
)
|
|
42
|
+
parser.add_argument(
|
|
43
|
+
'--all', '-A',
|
|
44
|
+
action='store_true',
|
|
45
|
+
help='Stage all changes (including modifications and deletions)'
|
|
46
|
+
)
|
|
47
|
+
parser.add_argument(
|
|
48
|
+
'--force', '-f',
|
|
49
|
+
action='store_true',
|
|
50
|
+
help='Force add even if file type is not recommended'
|
|
51
|
+
)
|
|
52
|
+
parser.add_argument(
|
|
53
|
+
'--allow-binary',
|
|
54
|
+
action='store_true',
|
|
55
|
+
help='Allow staging binary files (not recommended)'
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
@staticmethod
|
|
59
|
+
def _is_binary_file(filepath: Path) -> bool:
|
|
60
|
+
"""Check if a file is binary by looking at magic bytes."""
|
|
61
|
+
try:
|
|
62
|
+
with open(filepath, 'rb') as f:
|
|
63
|
+
header = f.read(16)
|
|
64
|
+
|
|
65
|
+
for signature in BINARY_SIGNATURES:
|
|
66
|
+
if header.startswith(signature):
|
|
67
|
+
return True
|
|
68
|
+
|
|
69
|
+
# Also check for null bytes (common in binary files)
|
|
70
|
+
if b'\x00' in header:
|
|
71
|
+
return True
|
|
72
|
+
|
|
73
|
+
return False
|
|
74
|
+
except Exception:
|
|
75
|
+
return False
|
|
76
|
+
|
|
77
|
+
@staticmethod
|
|
78
|
+
def _is_allowed_extension(filepath: Path, config: dict) -> bool:
|
|
79
|
+
"""Check if file extension is in allowed list."""
|
|
80
|
+
allowed = config.get('allowed_extensions', list(DEFAULT_ALLOWED_EXTENSIONS))
|
|
81
|
+
allowed_set = set(allowed)
|
|
82
|
+
|
|
83
|
+
ext = filepath.suffix.lower()
|
|
84
|
+
return ext in allowed_set or not ext # Allow files without extension
|
|
85
|
+
|
|
86
|
+
@staticmethod
|
|
87
|
+
def _validate_file(filepath: Path, config: dict, force: bool, allow_binary: bool) -> tuple:
|
|
88
|
+
"""
|
|
89
|
+
Validate a file for staging.
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
Tuple of (is_valid, warning_message)
|
|
93
|
+
"""
|
|
94
|
+
# Check for binary files
|
|
95
|
+
if AddCommand._is_binary_file(filepath):
|
|
96
|
+
if allow_binary:
|
|
97
|
+
return True, f"Warning: {filepath} appears to be binary"
|
|
98
|
+
else:
|
|
99
|
+
return False, f"Rejected: {filepath} is a binary file. Use --allow-binary to override."
|
|
100
|
+
|
|
101
|
+
# Check extension
|
|
102
|
+
if not AddCommand._is_allowed_extension(filepath, config):
|
|
103
|
+
ext = filepath.suffix or '(no extension)'
|
|
104
|
+
allowed = config.get('allowed_extensions', list(DEFAULT_ALLOWED_EXTENSIONS))
|
|
105
|
+
|
|
106
|
+
if force:
|
|
107
|
+
return True, f"Warning: {filepath} has extension '{ext}' which may not be optimal"
|
|
108
|
+
else:
|
|
109
|
+
return False, (
|
|
110
|
+
f"Rejected: {filepath} has extension '{ext}'.\n"
|
|
111
|
+
f" Recommended: {', '.join(sorted(allowed))}\n"
|
|
112
|
+
f" Use --force to override."
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
return True, None
|
|
116
|
+
|
|
117
|
+
@staticmethod
|
|
118
|
+
def execute(args) -> int:
|
|
119
|
+
repo, code = require_repo()
|
|
120
|
+
if code != 0:
|
|
121
|
+
return code
|
|
122
|
+
|
|
123
|
+
staged_count = 0
|
|
124
|
+
rejected_count = 0
|
|
125
|
+
config = repo.get_config()
|
|
126
|
+
|
|
127
|
+
for path_str in args.paths:
|
|
128
|
+
path = Path(path_str)
|
|
129
|
+
|
|
130
|
+
# Handle '.' to stage all
|
|
131
|
+
if path_str == '.':
|
|
132
|
+
staged, rejected = AddCommand._stage_directory_with_validation(
|
|
133
|
+
repo, None, config, args.force, args.allow_binary
|
|
134
|
+
)
|
|
135
|
+
staged_count += staged
|
|
136
|
+
rejected_count += rejected
|
|
137
|
+
continue
|
|
138
|
+
|
|
139
|
+
# Resolve path relative to current/
|
|
140
|
+
if path.is_absolute():
|
|
141
|
+
try:
|
|
142
|
+
rel_path = path.relative_to(repo.current_dir)
|
|
143
|
+
except ValueError:
|
|
144
|
+
print(f"Error: Path {path} is outside repository")
|
|
145
|
+
continue
|
|
146
|
+
else:
|
|
147
|
+
# Check if it's in current/ or needs to be resolved
|
|
148
|
+
if (repo.current_dir / path).exists():
|
|
149
|
+
rel_path = path
|
|
150
|
+
elif path.exists():
|
|
151
|
+
# Path exists outside current/, copy it in
|
|
152
|
+
target = repo.current_dir / path.name
|
|
153
|
+
if path.is_file():
|
|
154
|
+
target.write_bytes(path.read_bytes())
|
|
155
|
+
rel_path = Path(path.name)
|
|
156
|
+
else:
|
|
157
|
+
print(f"Error: Path not found: {path}")
|
|
158
|
+
continue
|
|
159
|
+
|
|
160
|
+
full_path = repo.current_dir / rel_path
|
|
161
|
+
|
|
162
|
+
if not full_path.exists():
|
|
163
|
+
print(f"Error: Path not found: {path}")
|
|
164
|
+
continue
|
|
165
|
+
|
|
166
|
+
if full_path.is_file():
|
|
167
|
+
# Validate file
|
|
168
|
+
is_valid, message = AddCommand._validate_file(
|
|
169
|
+
full_path, config, args.force, args.allow_binary
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
if not is_valid:
|
|
173
|
+
print(message)
|
|
174
|
+
rejected_count += 1
|
|
175
|
+
continue
|
|
176
|
+
|
|
177
|
+
if message: # Warning
|
|
178
|
+
print(message)
|
|
179
|
+
|
|
180
|
+
try:
|
|
181
|
+
blob_hash = repo.stage_file(str(rel_path))
|
|
182
|
+
print(f" staged: {rel_path}")
|
|
183
|
+
staged_count += 1
|
|
184
|
+
except Exception as e:
|
|
185
|
+
print(f"Error staging {rel_path}: {e}")
|
|
186
|
+
|
|
187
|
+
elif full_path.is_dir():
|
|
188
|
+
staged, rejected = AddCommand._stage_directory_with_validation(
|
|
189
|
+
repo, str(rel_path), config, args.force, args.allow_binary
|
|
190
|
+
)
|
|
191
|
+
staged_count += staged
|
|
192
|
+
rejected_count += rejected
|
|
193
|
+
|
|
194
|
+
if staged_count > 0 or rejected_count > 0:
|
|
195
|
+
print(f"\nStaged {staged_count} file(s)")
|
|
196
|
+
if rejected_count > 0:
|
|
197
|
+
print(f"Rejected {rejected_count} file(s) - use --force to override")
|
|
198
|
+
if staged_count > 0:
|
|
199
|
+
print("Run 'agmem commit -m \"message\"' to save snapshot")
|
|
200
|
+
else:
|
|
201
|
+
print("No files staged")
|
|
202
|
+
|
|
203
|
+
return 0
|
|
204
|
+
|
|
205
|
+
@staticmethod
|
|
206
|
+
def _stage_directory_with_validation(repo, subdir: str, config: dict, force: bool, allow_binary: bool) -> tuple:
|
|
207
|
+
"""
|
|
208
|
+
Stage a directory with file validation.
|
|
209
|
+
|
|
210
|
+
Returns:
|
|
211
|
+
Tuple of (staged_count, rejected_count)
|
|
212
|
+
"""
|
|
213
|
+
staged_count = 0
|
|
214
|
+
rejected_count = 0
|
|
215
|
+
|
|
216
|
+
if subdir:
|
|
217
|
+
dir_path = repo.current_dir / subdir
|
|
218
|
+
else:
|
|
219
|
+
dir_path = repo.current_dir
|
|
220
|
+
|
|
221
|
+
if not dir_path.exists():
|
|
222
|
+
return 0, 0
|
|
223
|
+
|
|
224
|
+
for file_path in dir_path.rglob('*'):
|
|
225
|
+
if not file_path.is_file():
|
|
226
|
+
continue
|
|
227
|
+
|
|
228
|
+
# Skip hidden files and .mem directory
|
|
229
|
+
rel_to_current = file_path.relative_to(repo.current_dir)
|
|
230
|
+
if any(part.startswith('.') for part in rel_to_current.parts):
|
|
231
|
+
continue
|
|
232
|
+
|
|
233
|
+
# Validate file
|
|
234
|
+
is_valid, message = AddCommand._validate_file(
|
|
235
|
+
file_path, config, force, allow_binary
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
if not is_valid:
|
|
239
|
+
if not force:
|
|
240
|
+
# Only print first few rejections to avoid spam
|
|
241
|
+
if rejected_count < 5:
|
|
242
|
+
print(f" {message}")
|
|
243
|
+
elif rejected_count == 5:
|
|
244
|
+
print(" ... (more files rejected)")
|
|
245
|
+
rejected_count += 1
|
|
246
|
+
continue
|
|
247
|
+
|
|
248
|
+
if message: # Warning
|
|
249
|
+
print(message)
|
|
250
|
+
|
|
251
|
+
try:
|
|
252
|
+
repo.stage_file(str(rel_to_current))
|
|
253
|
+
print(f" staged: {rel_to_current}")
|
|
254
|
+
staged_count += 1
|
|
255
|
+
except Exception as e:
|
|
256
|
+
print(f"Error staging {rel_to_current}: {e}")
|
|
257
|
+
|
|
258
|
+
return staged_count, rejected_count
|
memvcs/commands/base.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Base command - shared logic for agmem commands.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Optional, Tuple
|
|
7
|
+
|
|
8
|
+
from memvcs.core.repository import Repository
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def require_repo(repo_path: Optional[Path] = None) -> Tuple[Optional[Repository], int]:
|
|
12
|
+
"""
|
|
13
|
+
Resolve repository and validate it exists.
|
|
14
|
+
|
|
15
|
+
Returns:
|
|
16
|
+
Tuple of (Repository or None, exit_code). If invalid, returns (None, 1).
|
|
17
|
+
"""
|
|
18
|
+
path = (repo_path or Path(".")).resolve()
|
|
19
|
+
repo = Repository(path)
|
|
20
|
+
if not repo.is_valid_repo():
|
|
21
|
+
print("Error: Not an agmem repository. Run 'agmem init' first.")
|
|
22
|
+
return None, 1
|
|
23
|
+
return repo, 0
|