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
memvcs/commands/graph.py CHANGED
@@ -11,109 +11,102 @@ from ..commands.base import require_repo
11
11
 
12
12
  class GraphCommand:
13
13
  """Visualize connections between memory files."""
14
-
15
- name = 'graph'
16
- help = 'Visualize the knowledge graph of memory files'
17
-
14
+
15
+ name = "graph"
16
+ help = "Visualize the knowledge graph of memory files"
17
+
18
18
  @staticmethod
19
19
  def add_arguments(parser: argparse.ArgumentParser):
20
+ parser.add_argument("--output", "-o", help="Output file for graph data (JSON)")
20
21
  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)'
22
+ "--format",
23
+ choices=["json", "d3", "summary"],
24
+ default="summary",
25
+ help="Output format (default: summary)",
29
26
  )
30
27
  parser.add_argument(
31
- '--no-similarity',
32
- action='store_true',
33
- help='Skip similarity-based edges (faster)'
28
+ "--no-similarity", action="store_true", help="Skip similarity-based edges (faster)"
34
29
  )
35
30
  parser.add_argument(
36
- '--threshold',
31
+ "--threshold",
37
32
  type=float,
38
33
  default=0.7,
39
- help='Similarity threshold for edges (default: 0.7)'
34
+ help="Similarity threshold for edges (default: 0.7)",
40
35
  )
41
36
  parser.add_argument(
42
- '--serve',
43
- action='store_true',
44
- help='Start web server to view interactive graph'
37
+ "--serve", action="store_true", help="Start web server to view interactive graph"
45
38
  )
46
-
39
+
47
40
  @staticmethod
48
41
  def execute(args) -> int:
49
42
  repo, code = require_repo()
50
43
  if code != 0:
51
44
  return code
52
-
45
+
53
46
  # Try to get vector store for similarity
54
47
  vector_store = None
55
48
  if not args.no_similarity:
56
49
  try:
57
50
  from ..core.vector_store import VectorStore
58
- vector_store = VectorStore(repo.root / '.mem')
51
+
52
+ vector_store = VectorStore(repo.root / ".mem")
59
53
  except ImportError:
60
54
  print("Note: Vector store not available, skipping similarity edges")
61
-
55
+
62
56
  # Build graph
63
57
  from ..core.knowledge_graph import KnowledgeGraphBuilder
64
-
58
+
65
59
  builder = KnowledgeGraphBuilder(repo, vector_store)
66
-
60
+
67
61
  print("Building knowledge graph...")
68
62
  graph_data = builder.build_graph(
69
- include_similarity=not args.no_similarity,
70
- similarity_threshold=args.threshold
63
+ include_similarity=not args.no_similarity, similarity_threshold=args.threshold
71
64
  )
72
-
65
+
73
66
  if args.serve:
74
67
  return GraphCommand._serve_graph(repo, graph_data)
75
-
76
- if args.format == 'summary':
68
+
69
+ if args.format == "summary":
77
70
  GraphCommand._print_summary(graph_data, builder)
78
-
79
- elif args.format == 'json':
71
+
72
+ elif args.format == "json":
80
73
  output = graph_data.to_json()
81
74
  if args.output:
82
75
  Path(args.output).write_text(output)
83
76
  print(f"Graph data written to: {args.output}")
84
77
  else:
85
78
  print(output)
86
-
87
- elif args.format == 'd3':
79
+
80
+ elif args.format == "d3":
88
81
  output = builder.export_for_d3()
89
82
  if args.output:
90
83
  Path(args.output).write_text(output)
91
84
  print(f"D3 graph data written to: {args.output}")
92
85
  else:
93
86
  print(output)
94
-
87
+
95
88
  return 0
96
-
89
+
97
90
  @staticmethod
98
91
  def _print_summary(graph_data, builder):
99
92
  """Print a text summary of the graph."""
100
93
  meta = graph_data.metadata
101
-
94
+
102
95
  print("\nKnowledge Graph Summary")
103
96
  print("=" * 40)
104
97
  print(f"Total files: {meta['total_nodes']}")
105
98
  print(f"Total connections: {meta['total_edges']}")
106
-
99
+
107
100
  print("\nBy Memory Type:")
108
- for mtype, count in meta['memory_types'].items():
101
+ for mtype, count in meta["memory_types"].items():
109
102
  if count > 0:
110
103
  print(f" {mtype}: {count}")
111
-
104
+
112
105
  print("\nBy Edge Type:")
113
- for etype, count in meta['edge_types'].items():
106
+ for etype, count in meta["edge_types"].items():
114
107
  if count > 0:
115
108
  print(f" {etype}: {count}")
116
-
109
+
117
110
  # Find isolated nodes
118
111
  isolated = builder.find_isolated_nodes()
119
112
  if isolated:
@@ -122,16 +115,16 @@ class GraphCommand:
122
115
  print(f" - {path}")
123
116
  if len(isolated) > 5:
124
117
  print(f" ... and {len(isolated) - 5} more")
125
-
118
+
126
119
  # Find potential contradictions
127
120
  contradictions = builder.find_potential_contradictions()
128
121
  if contradictions:
129
122
  print(f"\nPotential contradictions: {len(contradictions)}")
130
123
  for path1, path2, sim in contradictions[:3]:
131
124
  print(f" - {path1} <-> {path2} (similarity: {sim:.2%})")
132
-
125
+
133
126
  print("\nUse --format d3 --output graph.json to export for visualization")
134
-
127
+
135
128
  @staticmethod
136
129
  def _serve_graph(repo, graph_data):
137
130
  """Start web server to view interactive graph."""
@@ -142,10 +135,10 @@ class GraphCommand:
142
135
  print("Error: Web server requires fastapi and uvicorn.")
143
136
  print("Install with: pip install agmem[web]")
144
137
  return 1
145
-
138
+
146
139
  print("Starting graph visualization server...")
147
140
  print("Open http://localhost:8080/graph in your browser")
148
-
141
+
149
142
  app = create_app(repo.root)
150
143
  uvicorn.run(app, host="127.0.0.1", port=8080)
151
144
  return 0
memvcs/commands/init.py CHANGED
@@ -10,49 +10,41 @@ from ..core.repository import Repository
10
10
 
11
11
  class InitCommand:
12
12
  """Initialize a new agmem repository."""
13
-
14
- name = 'init'
15
- help = 'Initialize a new memory repository'
16
-
13
+
14
+ name = "init"
15
+ help = "Initialize a new memory repository"
16
+
17
17
  @staticmethod
18
18
  def add_arguments(parser: argparse.ArgumentParser):
19
19
  parser.add_argument(
20
- 'path',
21
- nargs='?',
22
- default='.',
23
- help='Directory to initialize repository in (default: current directory)'
20
+ "path",
21
+ nargs="?",
22
+ default=".",
23
+ help="Directory to initialize repository in (default: current directory)",
24
24
  )
25
+ parser.add_argument("--author-name", default="Agent", help="Default author name")
25
26
  parser.add_argument(
26
- '--author-name',
27
- default='Agent',
28
- help='Default author name'
27
+ "--author-email", default="agent@example.com", help="Default author email"
29
28
  )
30
- parser.add_argument(
31
- '--author-email',
32
- default='agent@example.com',
33
- help='Default author email'
34
- )
35
-
29
+
36
30
  @staticmethod
37
31
  def execute(args) -> int:
38
32
  path = Path(args.path).resolve()
39
-
33
+
40
34
  try:
41
35
  repo = Repository.init(
42
- path=path,
43
- author_name=args.author_name,
44
- author_email=args.author_email
36
+ path=path, author_name=args.author_name, author_email=args.author_email
45
37
  )
46
-
38
+
47
39
  print(f"Initialized empty agmem repository in {repo.mem_dir}")
48
40
  print(f"Author: {args.author_name} <{args.author_email}>")
49
41
  print(f"\nNext steps:")
50
42
  print(f" 1. Add memory files to {repo.current_dir}/")
51
43
  print(f" 2. Run 'agmem add <file>' to stage changes")
52
44
  print(f" 3. Run 'agmem commit -m \"message\"' to save snapshot")
53
-
45
+
54
46
  return 0
55
-
47
+
56
48
  except ValueError as e:
57
49
  print(f"Error: {e}")
58
50
  return 1
memvcs/commands/log.py CHANGED
@@ -11,39 +11,24 @@ from ..core.repository import Repository
11
11
 
12
12
  class LogCommand:
13
13
  """Show commit history."""
14
-
15
- name = 'log'
16
- help = 'Show commit history'
17
-
14
+
15
+ name = "log"
16
+ help = "Show commit history"
17
+
18
18
  @staticmethod
19
19
  def add_arguments(parser: argparse.ArgumentParser):
20
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'
21
+ "--max-count", "-n", type=int, default=10, help="Maximum number of commits to show"
30
22
  )
23
+ parser.add_argument("--oneline", action="store_true", help="Show one commit per line")
31
24
  parser.add_argument(
32
- '--graph',
33
- action='store_true',
34
- help='Show ASCII graph of branch/merge history'
25
+ "--graph", action="store_true", help="Show ASCII graph of branch/merge history"
35
26
  )
27
+ parser.add_argument("--all", action="store_true", help="Show all branches")
36
28
  parser.add_argument(
37
- '--all',
38
- action='store_true',
39
- help='Show all branches'
29
+ "ref", nargs="?", help="Start from this reference (branch, tag, or commit)"
40
30
  )
41
- parser.add_argument(
42
- 'ref',
43
- nargs='?',
44
- help='Start from this reference (branch, tag, or commit)'
45
- )
46
-
31
+
47
32
  @staticmethod
48
33
  def execute(args) -> int:
49
34
  repo, code = require_repo()
@@ -52,11 +37,11 @@ class LogCommand:
52
37
 
53
38
  # Get commits
54
39
  commits = repo.get_log(max_count=args.max_count)
55
-
40
+
56
41
  if not commits:
57
42
  print("No commits yet.")
58
43
  return 0
59
-
44
+
60
45
  if args.oneline:
61
46
  for commit in commits:
62
47
  print(f"{commit['short_hash']} {commit['message']}")
@@ -71,33 +56,33 @@ class LogCommand:
71
56
  for i, commit in enumerate(commits):
72
57
  if i > 0:
73
58
  print()
74
-
59
+
75
60
  # Commit header
76
61
  print(f"\033[33mcommit {commit['hash']}\033[0m")
77
-
62
+
78
63
  # Show branch info if this is HEAD
79
64
  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']:
65
+ if head["type"] == "branch":
66
+ head_commit = repo.refs.get_branch_commit(head["value"])
67
+ if head_commit == commit["hash"]:
83
68
  print(f"\033[36mHEAD -> {head['value']}\033[0m")
84
-
69
+
85
70
  # Author and date
86
71
  print(f"Author: {commit['author']}")
87
-
72
+
88
73
  # Format timestamp
89
74
  try:
90
- ts = commit['timestamp']
91
- if ts.endswith('Z'):
75
+ ts = commit["timestamp"]
76
+ if ts.endswith("Z"):
92
77
  ts = ts[:-1]
93
78
  dt = datetime.fromisoformat(ts)
94
- date_str = dt.strftime('%a %b %d %H:%M:%S %Y')
79
+ date_str = dt.strftime("%a %b %d %H:%M:%S %Y")
95
80
  print(f"Date: {date_str}")
96
- except:
81
+ except Exception:
97
82
  print(f"Date: {commit['timestamp']}")
98
-
83
+
99
84
  # Message
100
85
  print()
101
86
  print(f" {commit['message']}")
102
-
87
+
103
88
  return 0
memvcs/commands/merge.py CHANGED
@@ -11,31 +11,24 @@ from ..core.repository import Repository
11
11
 
12
12
  class MergeCommand:
13
13
  """Merge branches."""
14
-
15
- name = 'merge'
16
- help = 'Join two or more development histories together'
17
-
14
+
15
+ name = "merge"
16
+ help = "Join two or more development histories together"
17
+
18
18
  @staticmethod
19
19
  def add_arguments(parser: argparse.ArgumentParser):
20
+ parser.add_argument("branch", help="Branch to merge into current branch")
21
+ parser.add_argument("-m", "--message", help="Merge commit message")
20
22
  parser.add_argument(
21
- 'branch',
22
- help='Branch to merge into current branch'
23
+ "--no-commit", action="store_true", help="Perform merge but do not commit"
23
24
  )
25
+ parser.add_argument("--abort", action="store_true", help="Abort the current merge")
24
26
  parser.add_argument(
25
- '-m', '--message',
26
- help='Merge commit message'
27
+ "--yes",
28
+ action="store_true",
29
+ help="Accept conditionally trusted branch commits without prompting",
27
30
  )
28
- parser.add_argument(
29
- '--no-commit',
30
- action='store_true',
31
- help='Perform merge but do not commit'
32
- )
33
- parser.add_argument(
34
- '--abort',
35
- action='store_true',
36
- help='Abort the current merge'
37
- )
38
-
31
+
39
32
  @staticmethod
40
33
  def execute(args) -> int:
41
34
  repo, code = require_repo()
@@ -47,42 +40,91 @@ class MergeCommand:
47
40
  # TODO: Implement merge abort
48
41
  print("Merge abort not yet implemented")
49
42
  return 0
50
-
43
+
51
44
  # Check if we're on a branch
52
45
  current_branch = repo.refs.get_current_branch()
53
46
  if not current_branch:
54
47
  print("Error: Not currently on any branch.")
55
48
  print("Cannot merge when HEAD is detached.")
56
49
  return 1
57
-
50
+
58
51
  # Check if trying to merge current branch
59
52
  if args.branch == current_branch:
60
53
  print(f"Error: Cannot merge '{args.branch}' into itself")
61
54
  return 1
62
-
55
+
63
56
  # Check if branch exists
64
57
  if not repo.refs.branch_exists(args.branch):
65
58
  print(f"Error: Branch '{args.branch}' not found.")
66
59
  return 1
67
-
60
+
61
+ # Trust check for branch tip (may be from another agent)
62
+ other_commit_hash = repo.refs.get_branch_commit(args.branch)
63
+ if other_commit_hash:
64
+ from ..core.objects import Commit
65
+ from ..core.trust import find_verifying_key, get_trust_level
66
+
67
+ other_commit = Commit.load(repo.object_store, other_commit_hash)
68
+ if other_commit and other_commit.metadata:
69
+ key_pem = find_verifying_key(repo.mem_dir, other_commit.metadata)
70
+ if key_pem is not None:
71
+ level = get_trust_level(repo.mem_dir, key_pem)
72
+ if level == "untrusted":
73
+ print(f"Merge blocked: branch '{args.branch}' signed by untrusted key.")
74
+ return 1
75
+ if level == "conditional" and not getattr(args, "yes", False):
76
+ print("Branch signed by conditionally trusted key. Use --yes to merge.")
77
+ return 1
78
+
68
79
  # Perform merge
69
80
  engine = MergeEngine(repo)
70
81
  result = engine.merge(args.branch, message=args.message)
71
-
82
+
72
83
  if result.success:
73
84
  print(f"Merge successful: {result.message}")
74
85
  if result.commit_hash:
75
86
  print(f" Commit: {result.commit_hash[:8]}")
87
+ try:
88
+ from ..core.audit import append_audit
89
+
90
+ append_audit(
91
+ repo.mem_dir, "merge", {"branch": args.branch, "commit": result.commit_hash}
92
+ )
93
+ except Exception:
94
+ pass
76
95
  return 0
77
96
  else:
78
97
  print(f"Merge failed: {result.message}")
79
-
98
+
80
99
  if result.conflicts:
100
+ # Persist conflicts for agmem resolve
101
+ try:
102
+ import json
103
+
104
+ merge_dir = repo.mem_dir / "merge"
105
+ merge_dir.mkdir(parents=True, exist_ok=True)
106
+ conflicts_data = [
107
+ {
108
+ "path": c.path,
109
+ "message": c.message,
110
+ "memory_type": getattr(c, "memory_type", None),
111
+ "payload": getattr(c, "payload", None),
112
+ "ours_content": c.ours_content,
113
+ "theirs_content": c.theirs_content,
114
+ "base_content": c.base_content,
115
+ }
116
+ for c in result.conflicts
117
+ ]
118
+ (merge_dir / "conflicts.json").write_text(json.dumps(conflicts_data, indent=2))
119
+ except Exception:
120
+ pass
81
121
  print()
82
122
  print("Conflicts:")
83
123
  for conflict in result.conflicts:
84
124
  print(f" {conflict.path}")
85
125
  print()
86
- print("Resolve conflicts and run 'agmem commit' to complete the merge.")
87
-
126
+ print(
127
+ "Resolve conflicts with 'agmem resolve' or edit files and run 'agmem commit'."
128
+ )
129
+
88
130
  return 1
@@ -0,0 +1,129 @@
1
+ """
2
+ agmem pack - Context window budget manager.
3
+
4
+ Packs recalled memories into token budget for LLM injection.
5
+ """
6
+
7
+ import argparse
8
+ import sys
9
+ from pathlib import Path
10
+
11
+ from ..commands.base import require_repo
12
+ from ..core.access_index import AccessIndex
13
+ from ..retrieval import RecallEngine
14
+ from ..retrieval.pack import PackEngine
15
+
16
+
17
+ class PackCommand:
18
+ """Context window budget manager."""
19
+
20
+ name = "pack"
21
+ help = "Pack recalled memories into token budget for LLM context"
22
+
23
+ @staticmethod
24
+ def add_arguments(parser: argparse.ArgumentParser):
25
+ parser.add_argument(
26
+ "--context",
27
+ "-c",
28
+ default="",
29
+ help="Current task description for recall",
30
+ )
31
+ parser.add_argument(
32
+ "--budget",
33
+ "-b",
34
+ type=int,
35
+ default=4000,
36
+ help="Max tokens (default: 4000)",
37
+ )
38
+ parser.add_argument(
39
+ "--strategy",
40
+ "-s",
41
+ choices=["relevance", "recency", "importance", "balanced"],
42
+ default="relevance",
43
+ help="Packing strategy (default: relevance)",
44
+ )
45
+ parser.add_argument(
46
+ "--exclude",
47
+ "-e",
48
+ action="append",
49
+ default=[],
50
+ help="Paths to exclude; repeatable",
51
+ )
52
+ parser.add_argument(
53
+ "--model",
54
+ "-m",
55
+ default="gpt-4o-mini",
56
+ help="Model for token counting (default: gpt-4o-mini)",
57
+ )
58
+ parser.add_argument(
59
+ "--format",
60
+ "-f",
61
+ choices=["text", "json"],
62
+ default="text",
63
+ help="Output format (default: text)",
64
+ )
65
+
66
+ @staticmethod
67
+ def execute(args) -> int:
68
+ repo, code = require_repo()
69
+ if code != 0:
70
+ return code
71
+
72
+ vector_store = None
73
+ try:
74
+ from ..core.vector_store import VectorStore
75
+
76
+ vs = VectorStore(repo.mem_dir)
77
+ vs._get_connection() # ensure sqlite-vec is usable; may raise
78
+ vector_store = vs
79
+ except Exception:
80
+ pass
81
+
82
+ access_index = AccessIndex(repo.mem_dir)
83
+ recall_engine = RecallEngine(
84
+ repo=repo,
85
+ vector_store=vector_store,
86
+ access_index=access_index,
87
+ use_cache=True,
88
+ )
89
+
90
+ strategy = "hybrid" if args.strategy == "balanced" else args.strategy
91
+ pack_engine = PackEngine(
92
+ recall_engine=recall_engine,
93
+ model=args.model,
94
+ summarization_cascade=False,
95
+ )
96
+
97
+ result = pack_engine.pack(
98
+ context=args.context,
99
+ budget=args.budget,
100
+ strategy=strategy,
101
+ exclude=args.exclude,
102
+ )
103
+
104
+ if args.format == "json":
105
+ import json
106
+
107
+ print(
108
+ json.dumps(
109
+ {
110
+ "content": result.content,
111
+ "total_tokens": result.total_tokens,
112
+ "budget": result.budget,
113
+ "items_used": result.items_used,
114
+ "items_total": result.items_total,
115
+ },
116
+ indent=2,
117
+ )
118
+ )
119
+ else:
120
+ print(result.content)
121
+ print(
122
+ f"\n# Pack stats: {result.total_tokens}/{result.budget} tokens, "
123
+ f"{result.items_used}/{result.items_total} items",
124
+ file=sys.stderr,
125
+ )
126
+
127
+ if vector_store and hasattr(vector_store, "close"):
128
+ vector_store.close()
129
+ return 0