diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..5a9df81 --- /dev/null +++ b/flake.lock @@ -0,0 +1,61 @@ +{ + "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1768564909, + "narHash": "sha256-Kell/SpJYVkHWMvnhqJz/8DqQg2b6PguxVWOuadbHCc=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "e4bae1bd10c9c57b2cf517953ab70060a828ee6f", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..cf592ec --- /dev/null +++ b/flake.nix @@ -0,0 +1,108 @@ +{ + description = "RepoMapper (repomap CLI + MCP server)"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + flake-utils.url = "github:numtide/flake-utils"; + }; + + outputs = + { + self, + nixpkgs, + flake-utils, + }: + flake-utils.lib.eachDefaultSystem ( + system: + let + pkgs = import nixpkgs { inherit system; }; + + python = pkgs.python313; + pythonEnv = python.withPackages ( + ps: with ps; [ + diskcache + fastmcp + grep-ast + networkx + pygments + tiktoken + tree-sitter + ] + ); + + repomapper = pkgs.stdenvNoCC.mkDerivation { + pname = "repomapper"; + version = "0.1.0"; + src = self; + + nativeBuildInputs = [ pkgs.makeWrapper ]; + dontBuild = true; + + installPhase = '' + runHook preInstall + + appDir="$out/share/repomapper" + mkdir -p "$appDir" + + cp -r \ + repomap.py \ + repomap_server.py \ + repomap_class.py \ + utils.py \ + importance.py \ + scm.py \ + queries \ + "$appDir/" + + mkdir -p "$out/bin" + + makeWrapper "${pythonEnv}/bin/python" "$out/bin/repomap" \ + --set PYTHONPATH "$appDir" \ + --add-flags "-m repomap" + + makeWrapper "${pythonEnv}/bin/python" "$out/bin/repomap-mcp" \ + --set PYTHONPATH "$appDir" \ + --add-flags "-m repomap_server" + + runHook postInstall + ''; + + meta = with pkgs.lib; { + description = "RepoMap command-line tool and MCP server"; + license = licenses.mit; + platforms = platforms.all; + mainProgram = "repomap"; + }; + }; + in + { + packages = { + inherit repomapper; + default = repomapper; + }; + + apps = { + repomap = flake-utils.lib.mkApp { + drv = repomapper; + exePath = "/bin/repomap"; + }; + + repomap-mcp = flake-utils.lib.mkApp { + drv = repomapper; + exePath = "/bin/repomap-mcp"; + }; + + default = flake-utils.lib.mkApp { + drv = repomapper; + exePath = "/bin/repomap"; + }; + }; + + devShells.default = pkgs.mkShell { + packages = [ + pythonEnv + ]; + }; + } + ); +} diff --git a/repomap.py b/repomap.py index b7b9f8d..b9e9183 100755 --- a/repomap.py +++ b/repomap.py @@ -23,17 +23,22 @@ def find_src_files(directory: str) -> List[str]: """Find source files in a directory.""" if not os.path.isdir(directory): return [directory] if os.path.isfile(directory) else [] - + src_files = [] for root, dirs, files in os.walk(directory): # Skip hidden directories and common non-source directories - dirs[:] = [d for d in dirs if not d.startswith('.') and d not in {'node_modules', '__pycache__', 'venv', 'env'}] - + dirs[:] = [ + d + for d in dirs + if not d.startswith(".") + and d not in {"node_modules", "__pycache__", "venv", "env"} + ] + for file in files: - if not file.startswith('.'): + if not file.startswith("."): full_path = os.path.join(root, file) src_files.append(full_path) - + return src_files @@ -63,98 +68,88 @@ def main(): %(prog)s src/ --map-tokens 2048 # Map src/ with 2048 token limit %(prog)s file1.py file2.py # Map specific files %(prog)s --chat-files main.py --other-files src/ # Specify chat vs other files - """ + """, ) - + parser.add_argument( - "paths", - nargs="*", - help="Files or directories to include in the map" + "paths", nargs="*", help="Files or directories to include in the map" ) - + parser.add_argument( "--root", default=".", - help="Repository root directory (default: current directory)" + help="Repository root directory (default: current directory)", ) - + parser.add_argument( "--map-tokens", type=int, default=8192, - help="Maximum tokens for the generated map (default: 8192)" + help="Maximum tokens for the generated map (default: 8192)", ) - + parser.add_argument( "--chat-files", nargs="*", - help="Files currently being edited (given higher priority)" + help="Files currently being edited (given higher priority)", ) - + parser.add_argument( - "--other-files", - nargs="*", - help="Other files to consider for the map" + "--other-files", nargs="*", help="Other files to consider for the map" ) - + parser.add_argument( "--mentioned-files", nargs="*", - help="Files explicitly mentioned (given higher priority)" + help="Files explicitly mentioned (given higher priority)", ) - + parser.add_argument( "--mentioned-idents", nargs="*", - help="Identifiers explicitly mentioned (given higher priority)" - ) - - parser.add_argument( - "--verbose", - action="store_true", - help="Enable verbose output" + help="Identifiers explicitly mentioned (given higher priority)", ) - + + parser.add_argument("--verbose", action="store_true", help="Enable verbose output") + parser.add_argument( "--model", default="gpt-4", - help="Model name for token counting (default: gpt-4)" + help="Model name for token counting (default: gpt-4)", ) - + parser.add_argument( - "--max-context-window", - type=int, - help="Maximum context window size" + "--max-context-window", type=int, help="Maximum context window size" ) - + parser.add_argument( - "--force-refresh", - action="store_true", - help="Force refresh of caches" + "--force-refresh", action="store_true", help="Force refresh of caches" ) parser.add_argument( "--exclude-unranked", action="store_true", - help="Exclude files with Page Rank 0 from the map" + help="Exclude files with Page Rank 0 from the map", ) - + args = parser.parse_args() - + # Set up token counter with specified model def token_counter(text: str) -> int: return count_tokens(text, args.model) - + # Set up output handlers output_handlers = { - 'info': tool_output, - 'warning': tool_warning, - 'error': tool_error + "info": tool_output, + "warning": tool_warning, + "error": tool_error, } - + # Process file arguments - chat_files_from_args = args.chat_files or [] # These are the paths as strings from the CLI - + chat_files_from_args = ( + args.chat_files or [] + ) # These are the paths as strings from the CLI + # Determine the list of unresolved path specifications that will form the 'other_files' # These can be files or directories. find_src_files will expand them. unresolved_paths_for_other_files_specs = [] @@ -169,7 +164,7 @@ def token_counter(text: str) -> int: effective_other_files_unresolved = [] for path_spec_str in unresolved_paths_for_other_files_specs: effective_other_files_unresolved.extend(find_src_files(path_spec_str)) - + # Convert to absolute paths root_path = Path(args.root).resolve() # chat_files for RepoMap are from --chat-files argument, resolved. @@ -178,11 +173,11 @@ def token_counter(text: str) -> int: other_files = [str(Path(f).resolve()) for f in effective_other_files_unresolved] print(f"Chat files: {chat_files}") - + # Convert mentioned files to sets mentioned_fnames = set(args.mentioned_files) if args.mentioned_files else None mentioned_idents = set(args.mentioned_idents) if args.mentioned_idents else None - + # Create RepoMap instance repo_map = RepoMap( map_tokens=args.map_tokens, @@ -192,28 +187,30 @@ def token_counter(text: str) -> int: output_handler_funcs=output_handlers, verbose=args.verbose, max_context_window=args.max_context_window, - exclude_unranked=args.exclude_unranked + exclude_unranked=args.exclude_unranked, ) - + # Generate the map try: - map_content = repo_map.get_repo_map( + map_content, _file_report = repo_map.get_repo_map( chat_files=chat_files, other_files=other_files, mentioned_fnames=mentioned_fnames, mentioned_idents=mentioned_idents, - force_refresh=args.force_refresh + force_refresh=args.force_refresh, ) - + if map_content: if args.verbose: tokens = repo_map.token_count(map_content) - tool_output(f"Generated map: {len(map_content)} chars, ~{tokens} tokens") - + tool_output( + f"Generated map: {len(map_content)} chars, ~{tokens} tokens" + ) + print(map_content) else: tool_output("No repository map generated.") - + except KeyboardInterrupt: tool_error("Interrupted by user") sys.exit(1) @@ -221,6 +218,7 @@ def token_counter(text: str) -> int: tool_error(f"Error generating repository map: {e}") if args.verbose: import traceback + traceback.print_exc() sys.exit(1)