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,88 @@
1
+ """
2
+ agmem merge - Join two or more development histories together.
3
+ """
4
+
5
+ import argparse
6
+
7
+ from ..commands.base import require_repo
8
+ from ..core.merge import MergeEngine
9
+ from ..core.repository import Repository
10
+
11
+
12
+ class MergeCommand:
13
+ """Merge branches."""
14
+
15
+ name = 'merge'
16
+ help = 'Join two or more development histories together'
17
+
18
+ @staticmethod
19
+ def add_arguments(parser: argparse.ArgumentParser):
20
+ parser.add_argument(
21
+ 'branch',
22
+ help='Branch to merge into current branch'
23
+ )
24
+ parser.add_argument(
25
+ '-m', '--message',
26
+ help='Merge commit message'
27
+ )
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
+
39
+ @staticmethod
40
+ def execute(args) -> int:
41
+ repo, code = require_repo()
42
+ if code != 0:
43
+ return code
44
+
45
+ # Abort merge
46
+ if args.abort:
47
+ # TODO: Implement merge abort
48
+ print("Merge abort not yet implemented")
49
+ return 0
50
+
51
+ # Check if we're on a branch
52
+ current_branch = repo.refs.get_current_branch()
53
+ if not current_branch:
54
+ print("Error: Not currently on any branch.")
55
+ print("Cannot merge when HEAD is detached.")
56
+ return 1
57
+
58
+ # Check if trying to merge current branch
59
+ if args.branch == current_branch:
60
+ print(f"Error: Cannot merge '{args.branch}' into itself")
61
+ return 1
62
+
63
+ # Check if branch exists
64
+ if not repo.refs.branch_exists(args.branch):
65
+ print(f"Error: Branch '{args.branch}' not found.")
66
+ return 1
67
+
68
+ # Perform merge
69
+ engine = MergeEngine(repo)
70
+ result = engine.merge(args.branch, message=args.message)
71
+
72
+ if result.success:
73
+ print(f"Merge successful: {result.message}")
74
+ if result.commit_hash:
75
+ print(f" Commit: {result.commit_hash[:8]}")
76
+ return 0
77
+ else:
78
+ print(f"Merge failed: {result.message}")
79
+
80
+ if result.conflicts:
81
+ print()
82
+ print("Conflicts:")
83
+ for conflict in result.conflicts:
84
+ print(f" {conflict.path}")
85
+ print()
86
+ print("Resolve conflicts and run 'agmem commit' to complete the merge.")
87
+
88
+ return 1
@@ -0,0 +1,65 @@
1
+ """
2
+ agmem pull - Pull memory from remote.
3
+ """
4
+
5
+ import argparse
6
+ from pathlib import Path
7
+
8
+
9
+ class PullCommand:
10
+ """Pull memory from remote repository."""
11
+
12
+ name = "pull"
13
+ help = "Pull memory from remote repository"
14
+
15
+ @staticmethod
16
+ def add_arguments(parser: argparse.ArgumentParser):
17
+ parser.add_argument(
18
+ "remote",
19
+ nargs="?",
20
+ default="origin",
21
+ help="Remote name (default: origin)",
22
+ )
23
+ parser.add_argument(
24
+ "branch",
25
+ nargs="?",
26
+ help="Branch to pull (default: all)",
27
+ )
28
+
29
+ @staticmethod
30
+ def execute(args) -> int:
31
+ from memvcs.commands.base import require_repo
32
+ from memvcs.core.remote import Remote
33
+
34
+ repo, code = require_repo()
35
+ if code != 0:
36
+ return code
37
+
38
+ remote = Remote(repo.root, args.remote)
39
+ if not remote.get_remote_url():
40
+ print(f"Error: Remote '{args.remote}' has no URL. Set with: agmem remote add {args.remote} <url>")
41
+ return 1
42
+
43
+ try:
44
+ msg = remote.fetch(branch=args.branch)
45
+ print(msg)
46
+ # Merge fetched refs into current branch
47
+ current_branch = repo.refs.get_current_branch()
48
+ if current_branch is not None:
49
+ remote_ref = f"{args.remote}/{current_branch}"
50
+ remote_hash = repo.resolve_ref(remote_ref)
51
+ if remote_hash:
52
+ from memvcs.core.merge import MergeEngine
53
+ merge_engine = MergeEngine(repo)
54
+ try:
55
+ result = merge_engine.merge(remote_ref)
56
+ if result.success:
57
+ print(f"Merged {remote_ref} into {current_branch}.")
58
+ else:
59
+ print("Merge had conflicts. Resolve and commit.")
60
+ except Exception as e:
61
+ print(f"Merge note: {e}")
62
+ return 0
63
+ except ValueError as e:
64
+ print(f"Error: {e}")
65
+ return 1
@@ -0,0 +1,143 @@
1
+ """
2
+ agmem push - Push memory to remote with auto-rebase support.
3
+ """
4
+
5
+ import argparse
6
+ from pathlib import Path
7
+
8
+
9
+ class MemoryConflictError(Exception):
10
+ """Exception raised when push fails due to conflicts."""
11
+ pass
12
+
13
+
14
+ class PushCommand:
15
+ """Push memory repository to remote with auto-rebase."""
16
+
17
+ name = "push"
18
+ help = "Push memory to remote repository"
19
+
20
+ @staticmethod
21
+ def add_arguments(parser: argparse.ArgumentParser):
22
+ parser.add_argument(
23
+ "remote",
24
+ nargs="?",
25
+ default="origin",
26
+ help="Remote name (default: origin)",
27
+ )
28
+ parser.add_argument(
29
+ "branch",
30
+ nargs="?",
31
+ help="Branch to push (default: current)",
32
+ )
33
+ parser.add_argument(
34
+ "--force", "-f",
35
+ action="store_true",
36
+ help="Force push (WARNING: may overwrite remote changes)",
37
+ )
38
+ parser.add_argument(
39
+ "--no-rebase",
40
+ action="store_true",
41
+ help="Don't attempt auto-rebase on conflicts",
42
+ )
43
+
44
+ @staticmethod
45
+ def execute(args) -> int:
46
+ from memvcs.commands.base import require_repo
47
+ from memvcs.core.remote import Remote
48
+ from memvcs.core.merge import MergeEngine
49
+
50
+ repo, code = require_repo()
51
+ if code != 0:
52
+ return code
53
+
54
+ remote = Remote(repo.root, args.remote)
55
+ remote_url = remote.get_remote_url()
56
+
57
+ if not remote_url:
58
+ print(f"Error: Remote '{args.remote}' has no URL.")
59
+ print(f"Set with: agmem remote add {args.remote} <url>")
60
+ return 1
61
+
62
+ # Get current branch
63
+ branch = args.branch or repo.refs.get_current_branch()
64
+ if not branch:
65
+ print("Error: Not on a branch and no branch specified")
66
+ return 1
67
+
68
+ # Force push warning
69
+ if args.force:
70
+ print("WARNING: Force push may overwrite remote changes!")
71
+ local_hash = repo.refs.get_branch_commit(branch)
72
+ try:
73
+ msg = remote.push(branch=branch)
74
+ print(msg)
75
+ return 0
76
+ except ValueError as e:
77
+ print(f"Error: {e}")
78
+ return 1
79
+
80
+ # Auto-rebase workflow
81
+ if not args.no_rebase:
82
+ # Fetch remote state
83
+ try:
84
+ print(f"Fetching from {args.remote}...")
85
+ remote.fetch()
86
+ except Exception as e:
87
+ print(f"Note: Could not fetch remote ({e}), attempting direct push...")
88
+
89
+ # Check if we're behind remote
90
+ local_hash = repo.refs.get_branch_commit(branch)
91
+ remote_branch = f"{args.remote}/{branch}"
92
+ remote_hash = repo.resolve_ref(remote_branch)
93
+
94
+ if remote_hash and remote_hash != local_hash:
95
+ # Check if we can fast-forward or need rebase
96
+ merge_engine = MergeEngine(repo)
97
+ ancestor = merge_engine.find_common_ancestor(local_hash, remote_hash)
98
+
99
+ if ancestor == local_hash:
100
+ # We're behind - need to pull first
101
+ print("Local is behind remote. Pull first with: agmem pull")
102
+ return 1
103
+
104
+ elif ancestor != remote_hash:
105
+ # Diverged - need to merge/rebase
106
+ print("Local and remote have diverged.")
107
+ print("Attempting auto-merge...")
108
+
109
+ try:
110
+ result = merge_engine.merge(remote_branch)
111
+
112
+ if result.success:
113
+ print(f"Auto-merged with {remote_branch}")
114
+ # Update local hash after merge
115
+ local_hash = repo.refs.get_branch_commit(branch)
116
+ else:
117
+ print("Auto-merge failed with conflicts:")
118
+ for conflict in result.conflicts:
119
+ print(f" - {conflict.path}")
120
+ print("\nResolve conflicts, commit, and try again.")
121
+ print("Or use --force to overwrite (not recommended).")
122
+ raise MemoryConflictError(result.message)
123
+
124
+ except MemoryConflictError:
125
+ return 1
126
+ except Exception as e:
127
+ print(f"Merge failed: {e}")
128
+ print("Use --no-rebase to skip auto-merge or --force to overwrite")
129
+ return 1
130
+
131
+ # Push
132
+ try:
133
+ msg = remote.push(branch=branch)
134
+ print(msg)
135
+ return 0
136
+ except ValueError as e:
137
+ error_msg = str(e)
138
+ if "non-fast-forward" in error_msg.lower() or "rejected" in error_msg.lower():
139
+ print("Push rejected: remote has changes you don't have.")
140
+ print("Run 'agmem pull' first, or use --force to overwrite.")
141
+ return 1
142
+ print(f"Error: {e}")
143
+ return 1
@@ -0,0 +1,52 @@
1
+ """
2
+ agmem reflog - Show reference history (Git-like).
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
+ class ReflogCommand:
13
+ """Show reflog - history of HEAD changes."""
14
+
15
+ name = 'reflog'
16
+ help = 'Show reference log (history of HEAD changes)'
17
+
18
+ @staticmethod
19
+ def add_arguments(parser: argparse.ArgumentParser):
20
+ parser.add_argument(
21
+ 'ref',
22
+ nargs='?',
23
+ default='HEAD',
24
+ help='Reference to show log for'
25
+ )
26
+ parser.add_argument(
27
+ '-n', '--max-count',
28
+ type=int,
29
+ default=20,
30
+ help='Maximum number of entries'
31
+ )
32
+
33
+ @staticmethod
34
+ def execute(args) -> int:
35
+ repo, code = require_repo()
36
+ if code != 0:
37
+ return code
38
+
39
+
40
+ entries = repo.refs.get_reflog(args.ref, args.max_count)
41
+
42
+ if not entries:
43
+ print("No reflog entries found.")
44
+ return 0
45
+
46
+ for e in entries:
47
+ h = e['hash'][:8]
48
+ ts = e.get('timestamp', '')[:19]
49
+ msg = e.get('message', '')
50
+ print(f"{h} {ts} {msg}")
51
+
52
+ return 0
@@ -0,0 +1,51 @@
1
+ """
2
+ agmem remote - Manage remote URLs.
3
+ """
4
+
5
+ import argparse
6
+ from pathlib import Path
7
+
8
+
9
+ class RemoteCommand:
10
+ """Manage remote repository URLs."""
11
+
12
+ name = "remote"
13
+ help = "Manage remote URLs (add, set-url, show)"
14
+
15
+ @staticmethod
16
+ def add_arguments(parser: argparse.ArgumentParser):
17
+ subparsers = parser.add_subparsers(dest="remote_action", required=True)
18
+ add_p = subparsers.add_parser("add", help="Add a remote")
19
+ add_p.add_argument("name", help="Remote name (e.g. origin)")
20
+ add_p.add_argument("url", help="Remote URL (e.g. file:///path)")
21
+ set_p = subparsers.add_parser("set-url", help="Set remote URL")
22
+ set_p.add_argument("name", help="Remote name")
23
+ set_p.add_argument("url", help="New URL")
24
+ subparsers.add_parser("show", help="Show remotes")
25
+
26
+ @staticmethod
27
+ def execute(args) -> int:
28
+ from memvcs.commands.base import require_repo
29
+ from memvcs.core.remote import Remote
30
+
31
+ repo, code = require_repo()
32
+ if code != 0:
33
+ return code
34
+
35
+ remote = Remote(repo.root, getattr(args, "name", "origin"))
36
+
37
+ if args.remote_action == "add" or args.remote_action == "set-url":
38
+ r = Remote(repo.root, args.name)
39
+ r.set_remote_url(args.url)
40
+ print(f"Remote '{args.name}' set to {args.url}")
41
+ elif args.remote_action == "show":
42
+ import json
43
+ config = json.loads((repo.root / ".mem" / "config.json").read_text())
44
+ remotes = config.get("remotes", {})
45
+ if remotes:
46
+ for name, info in remotes.items():
47
+ print(f"{name}\t{info.get('url', '')}")
48
+ else:
49
+ print("No remotes configured.")
50
+
51
+ return 0
@@ -0,0 +1,98 @@
1
+ """
2
+ agmem reset - Reset current HEAD to the specified state.
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
+ class ResetCommand:
13
+ """Reset current HEAD to the specified state."""
14
+
15
+ name = 'reset'
16
+ help = 'Reset current HEAD to the specified state'
17
+
18
+ @staticmethod
19
+ def add_arguments(parser: argparse.ArgumentParser):
20
+ parser.add_argument(
21
+ 'commit',
22
+ nargs='?',
23
+ default='HEAD',
24
+ help='Commit to reset to (default: HEAD)'
25
+ )
26
+ parser.add_argument(
27
+ '--soft',
28
+ action='store_true',
29
+ help='Reset HEAD but keep staged changes'
30
+ )
31
+ parser.add_argument(
32
+ '--mixed',
33
+ action='store_true',
34
+ help='Reset HEAD and unstaged changes (default)'
35
+ )
36
+ parser.add_argument(
37
+ '--hard',
38
+ action='store_true',
39
+ help='Reset HEAD, index, and working tree'
40
+ )
41
+
42
+ @staticmethod
43
+ def execute(args) -> int:
44
+ # Find repository
45
+ repo, code = require_repo()
46
+ if code != 0:
47
+ return code
48
+
49
+
50
+ # Determine mode
51
+ if args.soft:
52
+ mode = 'soft'
53
+ elif args.hard:
54
+ mode = 'hard'
55
+ else:
56
+ mode = 'mixed'
57
+
58
+ # Resolve commit
59
+ commit_hash = repo.resolve_ref(args.commit)
60
+ if not commit_hash:
61
+ print(f"Error: Unknown revision: {args.commit}")
62
+ return 1
63
+
64
+ # Get current branch
65
+ current_branch = repo.refs.get_current_branch()
66
+
67
+ try:
68
+ if mode == 'soft':
69
+ # Just move HEAD
70
+ if current_branch:
71
+ repo.refs.set_branch_commit(current_branch, commit_hash)
72
+ else:
73
+ repo.refs.set_head_detached(commit_hash)
74
+ print(f"HEAD is now at {commit_hash[:8]}")
75
+
76
+ elif mode == 'mixed':
77
+ # Move HEAD and clear staging
78
+ if current_branch:
79
+ repo.refs.set_branch_commit(current_branch, commit_hash)
80
+ else:
81
+ repo.refs.set_head_detached(commit_hash)
82
+
83
+ # Keep staged files but mark them as unstaged
84
+ # (In a full implementation, we'd restore the tree state)
85
+ print(f"HEAD is now at {commit_hash[:8]}")
86
+ print("Staged changes have been unstaged.")
87
+
88
+ elif mode == 'hard':
89
+ # Move HEAD, clear staging, and restore working tree
90
+ repo.checkout(commit_hash, force=True)
91
+ print(f"HEAD is now at {commit_hash[:8]}")
92
+ print("Working tree has been reset.")
93
+
94
+ return 0
95
+
96
+ except Exception as e:
97
+ print(f"Error during reset: {e}")
98
+ return 1
@@ -0,0 +1,163 @@
1
+ """
2
+ agmem search - Semantic search over memory.
3
+ """
4
+
5
+ import argparse
6
+ import sys
7
+ from pathlib import Path
8
+
9
+
10
+ def _is_vector_unavailable_error(exc: Exception) -> bool:
11
+ """True if the exception indicates vector deps are missing (fall back to text search)."""
12
+ msg = str(exc).lower()
13
+ return any(
14
+ key in msg for key in ("sqlite-vec", "sentence-transformers", "vector search")
15
+ )
16
+
17
+
18
+ def _first_line_containing(content: str, query: str, max_len: int = 200) -> str:
19
+ """Return the first line that contains query (lowercased), trimmed to max_len, or empty string."""
20
+ for line in content.splitlines():
21
+ if query in line.lower():
22
+ return line.strip()[:max_len]
23
+ return ""
24
+
25
+
26
+ class SearchCommand:
27
+ """Semantic search over memory files."""
28
+
29
+ name = "search"
30
+ help = "Search memory (semantic with agmem[vector], else plain text)"
31
+
32
+ @staticmethod
33
+ def add_arguments(parser: argparse.ArgumentParser):
34
+ parser.add_argument(
35
+ "query",
36
+ nargs="?",
37
+ default="",
38
+ help="Search query for semantic search",
39
+ )
40
+ parser.add_argument(
41
+ "--limit", "-n",
42
+ type=int,
43
+ default=10,
44
+ help="Maximum results to return (default: 10)",
45
+ )
46
+ parser.add_argument(
47
+ "--rebuild",
48
+ action="store_true",
49
+ help="Rebuild vector index from current/ before searching",
50
+ )
51
+ parser.add_argument(
52
+ "--index-only",
53
+ action="store_true",
54
+ help="Only build/rebuild index, do not search",
55
+ )
56
+
57
+ @staticmethod
58
+ def execute(args) -> int:
59
+ from memvcs.commands.base import require_repo
60
+ from memvcs.core.repository import Repository
61
+
62
+ repo, code = require_repo()
63
+ if code != 0:
64
+ return code
65
+
66
+ try:
67
+ from memvcs.core.vector_store import VectorStore
68
+
69
+ store = VectorStore(repo.mem_dir)
70
+ except ImportError:
71
+ return SearchCommand._text_search(repo, args)
72
+
73
+ try:
74
+ if args.rebuild or args.index_only:
75
+ count = store.rebuild_index(repo.current_dir)
76
+ print(f"Indexed {count} file(s).")
77
+ if args.index_only:
78
+ return 0
79
+
80
+ # Lazy index on first search if index is empty
81
+ if args.query and not store.db_path.exists():
82
+ print("Building index on first search...")
83
+ count = store.index_directory(repo.current_dir)
84
+ print(f"Indexed {count} file(s).\n")
85
+
86
+ if not args.query:
87
+ if not args.index_only:
88
+ print("Usage: agmem search <query> [--limit N] [--rebuild]")
89
+ return 0
90
+
91
+ results = store.search(args.query, limit=args.limit)
92
+
93
+ if not results:
94
+ print(f"No results for '{args.query}'.")
95
+ print("Try --rebuild to index current/ files.")
96
+ return 0
97
+
98
+ for path, snippet, distance in results:
99
+ print(f"\n--- {path} (distance: {distance:.4f}) ---")
100
+ print(snippet)
101
+ print()
102
+
103
+ return 0
104
+ except Exception as e:
105
+ if _is_vector_unavailable_error(e):
106
+ return SearchCommand._text_search(repo, args)
107
+ print(f"Error: {e}", file=sys.stderr)
108
+ return 1
109
+ finally:
110
+ store.close()
111
+
112
+ @staticmethod
113
+ def _text_search(repo, args) -> int:
114
+ """Plain text search when vector store is not available."""
115
+ if args.index_only or args.rebuild:
116
+ print("Note: Install agmem[vector] for index/rebuild. Using plain text search.")
117
+ if not args.query:
118
+ print("Usage: agmem search <query> [--limit N]")
119
+ return 0
120
+
121
+ query_lower = args.query.lower()
122
+ results = []
123
+ current_dir = repo.current_dir
124
+ if not current_dir.exists():
125
+ print("No current/ directory.")
126
+ return 0
127
+
128
+ for ext in ("*.md", "*.txt"):
129
+ for f in current_dir.rglob(ext):
130
+ if not f.is_file():
131
+ continue
132
+ try:
133
+ content = f.read_text(encoding="utf-8", errors="replace")
134
+ except Exception:
135
+ continue
136
+ if query_lower not in content.lower():
137
+ continue
138
+ try:
139
+ rel = str(f.relative_to(current_dir))
140
+ except ValueError:
141
+ continue
142
+ # Snippet: line containing query
143
+ for line in content.splitlines():
144
+ if query_lower in line.lower():
145
+ snippet = line.strip()[:200]
146
+ break
147
+ else:
148
+ snippet = content[:200].strip()
149
+ results.append((rel, snippet, 0.0))
150
+ if len(results) >= args.limit:
151
+ break
152
+ if len(results) >= args.limit:
153
+ break
154
+
155
+ if not results:
156
+ print(f"No results for '{args.query}'.")
157
+ return 0
158
+
159
+ for path, snippet, _ in results:
160
+ print(f"\n--- {path} ---")
161
+ print(snippet)
162
+ print()
163
+ return 0