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,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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.10.2)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ agmem = memvcs.cli:main
@@ -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
@@ -0,0 +1,9 @@
1
+ """
2
+ agmem - Agentic Memory Version Control System
3
+
4
+ A Git-inspired version control system for AI agent memory artifacts.
5
+ """
6
+
7
+ __version__ = "0.1.1"
8
+ __author__ = "agmem Team"
9
+ __license__ = "MIT"
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
@@ -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