agmem 0.1.1__py3-none-any.whl → 0.1.3__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 (100) hide show
  1. {agmem-0.1.1.dist-info → agmem-0.1.3.dist-info}/METADATA +157 -16
  2. agmem-0.1.3.dist-info/RECORD +105 -0
  3. memvcs/__init__.py +1 -1
  4. memvcs/cli.py +45 -31
  5. memvcs/commands/__init__.py +9 -9
  6. memvcs/commands/add.py +83 -76
  7. memvcs/commands/audit.py +59 -0
  8. memvcs/commands/blame.py +46 -53
  9. memvcs/commands/branch.py +13 -33
  10. memvcs/commands/checkout.py +27 -32
  11. memvcs/commands/clean.py +18 -23
  12. memvcs/commands/clone.py +11 -1
  13. memvcs/commands/commit.py +40 -39
  14. memvcs/commands/daemon.py +109 -76
  15. memvcs/commands/decay.py +77 -0
  16. memvcs/commands/diff.py +56 -57
  17. memvcs/commands/distill.py +90 -0
  18. memvcs/commands/federated.py +53 -0
  19. memvcs/commands/fsck.py +86 -61
  20. memvcs/commands/garden.py +40 -35
  21. memvcs/commands/gc.py +51 -0
  22. memvcs/commands/graph.py +41 -48
  23. memvcs/commands/init.py +16 -24
  24. memvcs/commands/log.py +25 -40
  25. memvcs/commands/merge.py +69 -27
  26. memvcs/commands/pack.py +129 -0
  27. memvcs/commands/prove.py +66 -0
  28. memvcs/commands/pull.py +31 -1
  29. memvcs/commands/push.py +4 -2
  30. memvcs/commands/recall.py +145 -0
  31. memvcs/commands/reflog.py +13 -22
  32. memvcs/commands/remote.py +1 -0
  33. memvcs/commands/repair.py +66 -0
  34. memvcs/commands/reset.py +23 -33
  35. memvcs/commands/resolve.py +130 -0
  36. memvcs/commands/resurrect.py +82 -0
  37. memvcs/commands/search.py +3 -4
  38. memvcs/commands/serve.py +2 -1
  39. memvcs/commands/show.py +66 -36
  40. memvcs/commands/stash.py +34 -34
  41. memvcs/commands/status.py +27 -35
  42. memvcs/commands/tag.py +23 -47
  43. memvcs/commands/test.py +30 -44
  44. memvcs/commands/timeline.py +111 -0
  45. memvcs/commands/tree.py +26 -27
  46. memvcs/commands/verify.py +110 -0
  47. memvcs/commands/when.py +115 -0
  48. memvcs/core/access_index.py +167 -0
  49. memvcs/core/audit.py +124 -0
  50. memvcs/core/config_loader.py +3 -1
  51. memvcs/core/consistency.py +214 -0
  52. memvcs/core/crypto_verify.py +280 -0
  53. memvcs/core/decay.py +185 -0
  54. memvcs/core/diff.py +158 -143
  55. memvcs/core/distiller.py +277 -0
  56. memvcs/core/encryption.py +169 -0
  57. memvcs/core/federated.py +86 -0
  58. memvcs/core/gardener.py +176 -145
  59. memvcs/core/hooks.py +48 -14
  60. memvcs/core/ipfs_remote.py +39 -0
  61. memvcs/core/knowledge_graph.py +135 -138
  62. memvcs/core/llm/__init__.py +10 -0
  63. memvcs/core/llm/anthropic_provider.py +50 -0
  64. memvcs/core/llm/base.py +27 -0
  65. memvcs/core/llm/factory.py +30 -0
  66. memvcs/core/llm/openai_provider.py +36 -0
  67. memvcs/core/merge.py +260 -170
  68. memvcs/core/objects.py +110 -101
  69. memvcs/core/pack.py +92 -0
  70. memvcs/core/pii_scanner.py +147 -146
  71. memvcs/core/privacy_budget.py +63 -0
  72. memvcs/core/refs.py +132 -115
  73. memvcs/core/remote.py +38 -0
  74. memvcs/core/repository.py +254 -164
  75. memvcs/core/schema.py +155 -113
  76. memvcs/core/staging.py +60 -65
  77. memvcs/core/storage/__init__.py +20 -18
  78. memvcs/core/storage/base.py +74 -70
  79. memvcs/core/storage/gcs.py +70 -68
  80. memvcs/core/storage/local.py +42 -40
  81. memvcs/core/storage/s3.py +105 -110
  82. memvcs/core/temporal_index.py +121 -0
  83. memvcs/core/test_runner.py +101 -93
  84. memvcs/core/trust.py +103 -0
  85. memvcs/core/vector_store.py +56 -36
  86. memvcs/core/zk_proofs.py +26 -0
  87. memvcs/integrations/mcp_server.py +1 -3
  88. memvcs/integrations/web_ui/server.py +25 -26
  89. memvcs/retrieval/__init__.py +22 -0
  90. memvcs/retrieval/base.py +54 -0
  91. memvcs/retrieval/pack.py +128 -0
  92. memvcs/retrieval/recaller.py +105 -0
  93. memvcs/retrieval/strategies.py +314 -0
  94. memvcs/utils/__init__.py +3 -3
  95. memvcs/utils/helpers.py +52 -52
  96. agmem-0.1.1.dist-info/RECORD +0 -67
  97. {agmem-0.1.1.dist-info → agmem-0.1.3.dist-info}/WHEEL +0 -0
  98. {agmem-0.1.1.dist-info → agmem-0.1.3.dist-info}/entry_points.txt +0 -0
  99. {agmem-0.1.1.dist-info → agmem-0.1.3.dist-info}/licenses/LICENSE +0 -0
  100. {agmem-0.1.1.dist-info → agmem-0.1.3.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,82 @@
1
+ """
2
+ agmem resurrect - Restore archived (decayed) memories.
3
+ """
4
+
5
+ import argparse
6
+ import shutil
7
+ from pathlib import Path
8
+
9
+ from ..commands.base import require_repo
10
+
11
+
12
+ class ResurrectCommand:
13
+ """Restore memories from .mem/forgetting/."""
14
+
15
+ name = "resurrect"
16
+ help = "Restore archived (decayed) memories from forgetting/"
17
+
18
+ @staticmethod
19
+ def add_arguments(parser: argparse.ArgumentParser):
20
+ parser.add_argument(
21
+ "path",
22
+ nargs="?",
23
+ help="Path or pattern to restore (e.g., semantic/user-prefs.md)",
24
+ )
25
+ parser.add_argument(
26
+ "--list",
27
+ "-l",
28
+ action="store_true",
29
+ help="List archived memories",
30
+ )
31
+
32
+ @staticmethod
33
+ def execute(args) -> int:
34
+ repo, code = require_repo()
35
+ if code != 0:
36
+ return code
37
+
38
+ forgetting_dir = repo.mem_dir / "forgetting"
39
+ if not forgetting_dir.exists():
40
+ print("No forgotten memories found.")
41
+ return 0
42
+
43
+ if args.list:
44
+ for sub in sorted(forgetting_dir.iterdir()):
45
+ if sub.is_dir():
46
+ print(f"\n{sub.name}:")
47
+ for f in sorted(sub.glob("*")):
48
+ if f.is_file():
49
+ print(f" - {f.name}")
50
+ return 0
51
+
52
+ if not args.path:
53
+ print("Error: Path to restore is required.")
54
+ print("Usage: agmem resurrect <path>")
55
+ print(" agmem resurrect --list")
56
+ return 1
57
+
58
+ # Find archived file
59
+ pattern = args.path.replace("/", "_")
60
+ found = []
61
+ for sub in forgetting_dir.iterdir():
62
+ if sub.is_dir():
63
+ for f in sub.glob("*"):
64
+ if f.is_file() and (pattern in f.name or f.name == args.path):
65
+ found.append(f)
66
+
67
+ if not found:
68
+ print(f"No archived memory matching '{args.path}' found.")
69
+ print("Use 'agmem resurrect --list' to see available archives.")
70
+ return 1
71
+
72
+ for archived in found:
73
+ # Restore to current/
74
+ orig_path = (
75
+ archived.name.replace("_", "/", 1) if "_" in archived.name else archived.name
76
+ )
77
+ dest = repo.current_dir / orig_path
78
+ dest.parent.mkdir(parents=True, exist_ok=True)
79
+ shutil.copy2(str(archived), str(dest))
80
+ print(f"Restored {archived.name} -> {orig_path}")
81
+
82
+ return 0
memvcs/commands/search.py CHANGED
@@ -10,9 +10,7 @@ from pathlib import Path
10
10
  def _is_vector_unavailable_error(exc: Exception) -> bool:
11
11
  """True if the exception indicates vector deps are missing (fall back to text search)."""
12
12
  msg = str(exc).lower()
13
- return any(
14
- key in msg for key in ("sqlite-vec", "sentence-transformers", "vector search")
15
- )
13
+ return any(key in msg for key in ("sqlite-vec", "sentence-transformers", "vector search"))
16
14
 
17
15
 
18
16
  def _first_line_containing(content: str, query: str, max_len: int = 200) -> str:
@@ -38,7 +36,8 @@ class SearchCommand:
38
36
  help="Search query for semantic search",
39
37
  )
40
38
  parser.add_argument(
41
- "--limit", "-n",
39
+ "--limit",
40
+ "-n",
42
41
  type=int,
43
42
  default=10,
44
43
  help="Maximum results to return (default: 10)",
memvcs/commands/serve.py CHANGED
@@ -16,7 +16,8 @@ class ServeCommand:
16
16
  @staticmethod
17
17
  def add_arguments(parser: argparse.ArgumentParser):
18
18
  parser.add_argument(
19
- "--port", "-p",
19
+ "--port",
20
+ "-p",
20
21
  type=int,
21
22
  default=8765,
22
23
  help="Port to bind (default: 8765)",
memvcs/commands/show.py CHANGED
@@ -4,6 +4,7 @@ agmem show - Show various types of objects.
4
4
 
5
5
  import argparse
6
6
  import json
7
+ import sys
7
8
  from pathlib import Path
8
9
 
9
10
  from ..commands.base import require_repo
@@ -13,28 +14,27 @@ from ..core.repository import Repository
13
14
 
14
15
  class ShowCommand:
15
16
  """Show various types of objects."""
16
-
17
- name = 'show'
18
- help = 'Show various types of objects'
19
-
17
+
18
+ name = "show"
19
+ help = "Show various types of objects"
20
+
20
21
  @staticmethod
21
22
  def add_arguments(parser: argparse.ArgumentParser):
23
+ parser.add_argument("object", help="Object to show (commit, tree, blob, branch)")
22
24
  parser.add_argument(
23
- 'object',
24
- help='Object to show (commit, tree, blob, branch)'
25
+ "--type",
26
+ "-t",
27
+ choices=["commit", "tree", "blob", "auto"],
28
+ default="auto",
29
+ help="Type of object to show",
25
30
  )
31
+ parser.add_argument("--raw", action="store_true", help="Show raw object content")
26
32
  parser.add_argument(
27
- '--type', '-t',
28
- choices=['commit', 'tree', 'blob', 'auto'],
29
- default='auto',
30
- help='Type of object to show'
33
+ "--at",
34
+ metavar="TIMESTAMP",
35
+ help="View file at specific time (ISO 8601, e.g., 2025-12-01T14:00:00)",
31
36
  )
32
- parser.add_argument(
33
- '--raw',
34
- action='store_true',
35
- help='Show raw object content'
36
- )
37
-
37
+
38
38
  @staticmethod
39
39
  def execute(args) -> int:
40
40
  # Find repository
@@ -42,11 +42,41 @@ class ShowCommand:
42
42
  if code != 0:
43
43
  return code
44
44
 
45
-
46
45
  obj_ref = args.object
47
46
  obj_hash = None
48
47
  obj_type = args.type
49
-
48
+
49
+ # --at: show file at timestamp
50
+ if args.at:
51
+ commit_hash = repo.resolve_ref(args.at)
52
+ if not commit_hash:
53
+ print(f"Error: No commit found at {args.at}", file=sys.stderr)
54
+ return 1
55
+ # Treat object as file path under current/
56
+ file_path = obj_ref.replace("current/", "").lstrip("/")
57
+ tree = repo.get_commit_tree(commit_hash)
58
+ if not tree:
59
+ print(
60
+ f"Error: Tree not found for commit at {args.at}", file=__import__("sys").stderr
61
+ )
62
+ return 1
63
+ # Find blob for path (tree has flat entries with path)
64
+ blob_hash = None
65
+ for entry in tree.entries:
66
+ full_path = (
67
+ (entry.path + "/" + entry.name).lstrip("/") if entry.path else entry.name
68
+ )
69
+ if full_path == file_path:
70
+ blob_hash = entry.hash
71
+ break
72
+ if not blob_hash:
73
+ print(f"Error: File {file_path} not found at {args.at}", file=sys.stderr)
74
+ return 1
75
+ blob = Blob.load(repo.object_store, blob_hash)
76
+ if blob:
77
+ print(blob.content.decode("utf-8", errors="replace"))
78
+ return 0
79
+
50
80
  # Try to resolve as reference
51
81
  resolved = repo.resolve_ref(obj_ref)
52
82
  if resolved:
@@ -54,34 +84,34 @@ class ShowCommand:
54
84
  else:
55
85
  # Assume it's a hash
56
86
  obj_hash = obj_ref
57
-
87
+
58
88
  # Try to determine type if auto
59
- if obj_type == 'auto':
89
+ if obj_type == "auto":
60
90
  # Try commit first
61
91
  commit = Commit.load(repo.object_store, obj_hash)
62
92
  if commit:
63
- obj_type = 'commit'
93
+ obj_type = "commit"
64
94
  else:
65
95
  # Try tree
66
96
  tree = Tree.load(repo.object_store, obj_hash)
67
97
  if tree:
68
- obj_type = 'tree'
98
+ obj_type = "tree"
69
99
  else:
70
100
  # Try blob
71
101
  blob = Blob.load(repo.object_store, obj_hash)
72
102
  if blob:
73
- obj_type = 'blob'
103
+ obj_type = "blob"
74
104
  else:
75
105
  print(f"Error: Object not found: {obj_ref}")
76
106
  return 1
77
-
107
+
78
108
  # Display based on type
79
- if obj_type == 'commit':
109
+ if obj_type == "commit":
80
110
  commit = Commit.load(repo.object_store, obj_hash)
81
111
  if not commit:
82
112
  print(f"Error: Commit not found: {obj_ref}")
83
113
  return 1
84
-
114
+
85
115
  print(f"commit {obj_hash}")
86
116
  print(f"Author: {commit.author}")
87
117
  print(f"Date: {commit.timestamp}")
@@ -91,35 +121,35 @@ class ShowCommand:
91
121
  print(f"tree {commit.tree}")
92
122
  if commit.parents:
93
123
  print(f"parent {' '.join(commit.parents)}")
94
-
95
- elif obj_type == 'tree':
124
+
125
+ elif obj_type == "tree":
96
126
  tree = Tree.load(repo.object_store, obj_hash)
97
127
  if not tree:
98
128
  print(f"Error: Tree not found: {obj_ref}")
99
129
  return 1
100
-
130
+
101
131
  print(f"tree {obj_hash}")
102
132
  print()
103
133
  for entry in sorted(tree.entries, key=lambda e: e.name):
104
- path = entry.path + '/' + entry.name if entry.path else entry.name
134
+ path = entry.path + "/" + entry.name if entry.path else entry.name
105
135
  print(f"{entry.mode} {entry.type} {entry.hash[:8]}\t{path}")
106
-
107
- elif obj_type == 'blob':
136
+
137
+ elif obj_type == "blob":
108
138
  blob = Blob.load(repo.object_store, obj_hash)
109
139
  if not blob:
110
140
  print(f"Error: Blob not found: {obj_ref}")
111
141
  return 1
112
-
142
+
113
143
  if args.raw:
114
- print(blob.content.decode('utf-8', errors='replace'))
144
+ print(blob.content.decode("utf-8", errors="replace"))
115
145
  else:
116
146
  print(f"blob {obj_hash}")
117
147
  print(f"Size: {len(blob.content)} bytes")
118
148
  print()
119
- content = blob.content.decode('utf-8', errors='replace')
149
+ content = blob.content.decode("utf-8", errors="replace")
120
150
  if len(content) > 1000:
121
151
  print(content[:1000] + "\n... (truncated)")
122
152
  else:
123
153
  print(content)
124
-
154
+
125
155
  return 0
memvcs/commands/stash.py CHANGED
@@ -11,52 +11,51 @@ from ..core.repository import Repository
11
11
 
12
12
  class StashCommand:
13
13
  """Stash working directory changes."""
14
-
15
- name = 'stash'
16
- help = 'Stash changes for later (save work in progress)'
17
-
14
+
15
+ name = "stash"
16
+ help = "Stash changes for later (save work in progress)"
17
+
18
18
  @staticmethod
19
19
  def add_arguments(parser: argparse.ArgumentParser):
20
- subparsers = parser.add_subparsers(dest='stash_action', help='Stash action')
21
-
20
+ subparsers = parser.add_subparsers(dest="stash_action", help="Stash action")
21
+
22
22
  # Default: list (when no subcommand given)
23
- parser.set_defaults(stash_action='list')
24
-
23
+ parser.set_defaults(stash_action="list")
24
+
25
25
  # stash (push)
26
- push_p = subparsers.add_parser('push', help='Stash current changes')
27
- push_p.add_argument('-m', '--message', default='', help='Stash message')
28
-
26
+ push_p = subparsers.add_parser("push", help="Stash current changes")
27
+ push_p.add_argument("-m", "--message", default="", help="Stash message")
28
+
29
29
  # stash pop
30
- subparsers.add_parser('pop', help='Apply and remove most recent stash')
31
-
30
+ subparsers.add_parser("pop", help="Apply and remove most recent stash")
31
+
32
32
  # stash list (default)
33
- subparsers.add_parser('list', help='List stashes')
34
-
33
+ subparsers.add_parser("list", help="List stashes")
34
+
35
35
  # stash apply
36
- apply_p = subparsers.add_parser('apply', help='Apply stash without removing')
37
- apply_p.add_argument('stash_ref', nargs='?', default='stash@{0}', help='Stash reference')
38
-
36
+ apply_p = subparsers.add_parser("apply", help="Apply stash without removing")
37
+ apply_p.add_argument("stash_ref", nargs="?", default="stash@{0}", help="Stash reference")
38
+
39
39
  @staticmethod
40
40
  def execute(args) -> int:
41
41
  repo, code = require_repo()
42
42
  if code != 0:
43
43
  return code
44
44
 
45
-
46
- action = getattr(args, 'stash_action', None)
45
+ action = getattr(args, "stash_action", None)
47
46
  if action is None:
48
- action = 'list'
49
-
50
- if action == 'push':
51
- stash_hash = repo.stash_create(getattr(args, 'message', '') or '')
47
+ action = "list"
48
+
49
+ if action == "push":
50
+ stash_hash = repo.stash_create(getattr(args, "message", "") or "")
52
51
  if stash_hash:
53
52
  print(f"Stashed changes (stash@{0})")
54
53
  return 0
55
54
  else:
56
55
  print("No local changes to stash.")
57
56
  return 0
58
-
59
- elif action == 'pop':
57
+
58
+ elif action == "pop":
60
59
  stash_hash = repo.stash_pop(0)
61
60
  if stash_hash:
62
61
  print(f"Restored stashed changes")
@@ -64,25 +63,26 @@ class StashCommand:
64
63
  else:
65
64
  print("No stash entries found.")
66
65
  return 1
67
-
68
- elif action == 'list':
66
+
67
+ elif action == "list":
69
68
  stashes = repo.refs.stash_list()
70
69
  if not stashes:
71
70
  print("No stash entries found.")
72
71
  return 0
73
72
  for i, s in enumerate(stashes):
74
- msg = s.get('message', 'WIP')
75
- h = s.get('hash', '')[:8]
73
+ msg = s.get("message", "WIP")
74
+ h = s.get("hash", "")[:8]
76
75
  print(f"stash@{{{i}}}: {h} {msg}")
77
76
  return 0
78
-
79
- elif action == 'apply':
80
- ref = getattr(args, 'stash_ref', 'stash@{0}')
77
+
78
+ elif action == "apply":
79
+ ref = getattr(args, "stash_ref", "stash@{0}")
81
80
  commit_hash = repo.resolve_ref(ref)
82
81
  if not commit_hash:
83
82
  print(f"Error: Stash not found: {ref}")
84
83
  return 1
85
84
  from ..core.objects import Tree, Blob
85
+
86
86
  tree = repo.get_commit_tree(commit_hash)
87
87
  if tree:
88
88
  for entry in tree.entries:
@@ -93,5 +93,5 @@ class StashCommand:
93
93
  fp.write_bytes(blob.content)
94
94
  print("Applied stash (changes in working directory)")
95
95
  return 0
96
-
96
+
97
97
  return 1
memvcs/commands/status.py CHANGED
@@ -10,23 +10,15 @@ from ..core.repository import Repository
10
10
 
11
11
  class StatusCommand:
12
12
  """Show the working tree status."""
13
-
14
- name = 'status'
15
- help = 'Show the working tree status'
16
-
13
+
14
+ name = "status"
15
+ help = "Show the working tree status"
16
+
17
17
  @staticmethod
18
18
  def add_arguments(parser: argparse.ArgumentParser):
19
- parser.add_argument(
20
- '--short', '-s',
21
- action='store_true',
22
- help='Show short format'
23
- )
24
- parser.add_argument(
25
- '--branch', '-b',
26
- action='store_true',
27
- help='Show branch information'
28
- )
29
-
19
+ parser.add_argument("--short", "-s", action="store_true", help="Show short format")
20
+ parser.add_argument("--branch", "-b", action="store_true", help="Show branch information")
21
+
30
22
  @staticmethod
31
23
  def execute(args) -> int:
32
24
  repo, code = require_repo()
@@ -34,35 +26,35 @@ class StatusCommand:
34
26
  return code
35
27
 
36
28
  status = repo.get_status()
37
-
29
+
38
30
  # Show branch info
39
- branch = status.get('branch')
40
- head = status.get('head', {})
41
-
31
+ branch = status.get("branch")
32
+ head = status.get("head", {})
33
+
42
34
  if args.short:
43
35
  # Short format: XY filename
44
- for f in status.get('staged', []):
36
+ for f in status.get("staged", []):
45
37
  print(f"A {f}")
46
- for f in status.get('modified', []):
38
+ for f in status.get("modified", []):
47
39
  print(f" M {f}")
48
- for f in status.get('deleted', []):
40
+ for f in status.get("deleted", []):
49
41
  print(f" D {f}")
50
- for f in status.get('untracked', []):
42
+ for f in status.get("untracked", []):
51
43
  print(f"?? {f}")
52
44
  else:
53
45
  # Long format
54
46
  if branch:
55
47
  print(f"On branch {branch}")
56
- elif head.get('type') == 'commit':
48
+ elif head.get("type") == "commit":
57
49
  print(f"HEAD detached at {head['value'][:8]}")
58
-
50
+
59
51
  # Check for commits
60
52
  head_commit = repo.get_head_commit()
61
53
  if not head_commit:
62
54
  print("\nNo commits yet")
63
-
55
+
64
56
  # Staged changes
65
- staged = status.get('staged', [])
57
+ staged = status.get("staged", [])
66
58
  if staged:
67
59
  print(f"\nChanges to be committed:")
68
60
  print(f' (use "agmem reset HEAD <file>..." to unstage)')
@@ -70,9 +62,9 @@ class StatusCommand:
70
62
  for f in staged:
71
63
  print(f" new file: {f}")
72
64
  print()
73
-
65
+
74
66
  # Modified but not staged
75
- modified = status.get('modified', [])
67
+ modified = status.get("modified", [])
76
68
  if modified:
77
69
  print(f"Changes not staged for commit:")
78
70
  print(f' (use "agmem add <file>..." to update what will be committed)')
@@ -80,9 +72,9 @@ class StatusCommand:
80
72
  for f in modified:
81
73
  print(f" modified: {f}")
82
74
  print()
83
-
75
+
84
76
  # Deleted but not staged
85
- deleted = status.get('deleted', [])
77
+ deleted = status.get("deleted", [])
86
78
  if deleted:
87
79
  print(f"Deleted files:")
88
80
  print(f' (use "agmem add <file>..." to stage deletion)')
@@ -90,9 +82,9 @@ class StatusCommand:
90
82
  for f in deleted:
91
83
  print(f" deleted: {f}")
92
84
  print()
93
-
85
+
94
86
  # Untracked files
95
- untracked = status.get('untracked', [])
87
+ untracked = status.get("untracked", [])
96
88
  if untracked:
97
89
  print(f"Untracked files:")
98
90
  print(f' (use "agmem add <file>..." to include in what will be committed)')
@@ -100,7 +92,7 @@ class StatusCommand:
100
92
  for f in untracked:
101
93
  print(f" {f}")
102
94
  print()
103
-
95
+
104
96
  # Summary
105
97
  total_changes = len(staged) + len(modified) + len(deleted) + len(untracked)
106
98
  if total_changes == 0:
@@ -108,5 +100,5 @@ class StatusCommand:
108
100
  print("nothing to commit, working tree clean")
109
101
  else:
110
102
  print('nothing to commit (create/copy files and use "agmem add" to track)')
111
-
103
+
112
104
  return 0
memvcs/commands/tag.py CHANGED
@@ -13,42 +13,19 @@ from ..core.repository import Repository
13
13
 
14
14
  class TagCommand:
15
15
  """Manage tags."""
16
-
17
- name = 'tag'
18
- help = 'Create, list, delete or verify a tag'
19
-
16
+
17
+ name = "tag"
18
+ help = "Create, list, delete or verify a tag"
19
+
20
20
  @staticmethod
21
21
  def add_arguments(parser: argparse.ArgumentParser):
22
- parser.add_argument(
23
- 'name',
24
- nargs='?',
25
- help='Tag name'
26
- )
27
- parser.add_argument(
28
- 'commit',
29
- nargs='?',
30
- help='Commit to tag (default: HEAD)'
31
- )
32
- parser.add_argument(
33
- '--list', '-l',
34
- action='store_true',
35
- help='List tags'
36
- )
37
- parser.add_argument(
38
- '--delete', '-d',
39
- action='store_true',
40
- help='Delete a tag'
41
- )
42
- parser.add_argument(
43
- '-m', '--message',
44
- help='Tag message'
45
- )
46
- parser.add_argument(
47
- '--force', '-f',
48
- action='store_true',
49
- help='Force replace existing tag'
50
- )
51
-
22
+ parser.add_argument("name", nargs="?", help="Tag name")
23
+ parser.add_argument("commit", nargs="?", help="Commit to tag (default: HEAD)")
24
+ parser.add_argument("--list", "-l", action="store_true", help="List tags")
25
+ parser.add_argument("--delete", "-d", action="store_true", help="Delete a tag")
26
+ parser.add_argument("-m", "--message", help="Tag message")
27
+ parser.add_argument("--force", "-f", action="store_true", help="Force replace existing tag")
28
+
52
29
  @staticmethod
53
30
  def execute(args) -> int:
54
31
  # Find repository
@@ -56,35 +33,34 @@ class TagCommand:
56
33
  if code != 0:
57
34
  return code
58
35
 
59
-
60
36
  # List tags
61
37
  if args.list or (not args.name and not args.delete):
62
38
  tags = repo.refs.list_tags()
63
-
39
+
64
40
  if not tags:
65
41
  print("No tags yet.")
66
42
  return 0
67
-
43
+
68
44
  for tag in sorted(tags):
69
45
  commit_hash = repo.refs.get_tag_commit(tag)
70
46
  short_hash = commit_hash[:8] if commit_hash else "????????"
71
47
  print(f"{tag}\t{short_hash}")
72
-
48
+
73
49
  return 0
74
-
50
+
75
51
  # Delete tag
76
52
  if args.delete:
77
53
  if not args.name:
78
54
  print("Error: Tag name required for deletion")
79
55
  return 1
80
-
56
+
81
57
  if repo.refs.delete_tag(args.name):
82
58
  print(f"Deleted tag '{args.name}'")
83
59
  return 0
84
60
  else:
85
61
  print(f"Error: Tag '{args.name}' not found")
86
62
  return 1
87
-
63
+
88
64
  # Create tag
89
65
  if args.name:
90
66
  # Check if tag exists
@@ -92,19 +68,19 @@ class TagCommand:
92
68
  print(f"Error: Tag '{args.name}' already exists")
93
69
  print("Use -f to force replace")
94
70
  return 1
95
-
71
+
96
72
  # Get commit to tag
97
- commit_ref = args.commit or 'HEAD'
73
+ commit_ref = args.commit or "HEAD"
98
74
  commit_hash = repo.resolve_ref(commit_ref)
99
-
75
+
100
76
  if not commit_hash:
101
77
  print(f"Error: Unknown revision: {commit_ref}")
102
78
  return 1
103
-
79
+
104
80
  # Delete existing tag if forcing
105
81
  if args.force and repo.refs.tag_exists(args.name):
106
82
  repo.refs.delete_tag(args.name)
107
-
83
+
108
84
  # Create tag
109
85
  message = args.message or f"Tag {args.name}"
110
86
  if repo.refs.create_tag(args.name, commit_hash, message):
@@ -113,5 +89,5 @@ class TagCommand:
113
89
  else:
114
90
  print(f"Error: Could not create tag '{args.name}'")
115
91
  return 1
116
-
92
+
117
93
  return 0