From 745a89a89552fbe4c0d4d7cd7cfff0bbe93f40e7 Mon Sep 17 00:00:00 2001 From: Vlad Shurupov Date: Wed, 27 May 2026 18:18:47 +1000 Subject: [PATCH 1/7] Implemented high-performance graph evolution features Added git-accelerated deltas via the --git flag to bypass full filesystem scans. Implemented robust node pruning for deleted files and hot-start PageRank for faster incremental convergence. Added a background watch command for real-time synchronization. Bumped version to 0.1.3. --- README.md | 16 +++- codeloom/__init__.py | 2 +- codeloom/cli/main.py | 81 ++++++++++++++++++ codeloom/core/build.py | 17 +++- codeloom/core/detect.py | 97 ++++++++++++++-------- codeloom/core/git.py | 72 ++++++++++++++++ codeloom/core/pipeline.py | 79 +++++++++++++----- codeloom/storage/store.py | 65 +++++++++++++++ pyproject.toml | 2 +- tests/test_evolution.py | 168 ++++++++++++++++++++++++++++++++++++++ 10 files changed, 538 insertions(+), 61 deletions(-) create mode 100644 codeloom/core/git.py create mode 100644 tests/test_evolution.py diff --git a/README.md b/README.md index ad13bbb..b80edf2 100644 --- a/README.md +++ b/README.md @@ -84,9 +84,13 @@ Text nodes (docs, comments, markdown) are embedded with `intfloat/multilingual-e When integrated with AI coding agents (Claude Code, Codex, etc.), codeloom **automatically rebuilds** the graph when code changes. The Stop/SessionEnd hook detects modified files via `git diff` and triggers an incremental rebuild in the background — zero manual intervention. -### Smart Ignore +### Git-Accelerated Deltas -codeloom respects ignore patterns from three sources, all using **full gitignore spec** (negation `!`, `**` globs, directory-only patterns): +Uses `git diff` to bypass heavy I/O of full filesystem scans. Only changed files are processed, enabling sub-second updates for typical logic changes. Toggle with `--git`. + +### Smart Ignore & Pruning + +codeloom respects ignore patterns from three sources and explicitly **prunes deleted files**, ensuring no "ghost nodes" remain in your graph after files are removed or renamed. | Source | Description | |--------|-------------| @@ -96,7 +100,7 @@ codeloom respects ignore patterns from three sources, all using **full gitignore ### Incremental Builds -SHA-256 content hashing per file. Only changed files are re-extracted and re-embedded. Unchanged files are merged from the existing graph — typically **95%+ faster** than a full rebuild. +SHA-256 content hashing per file and **hot-start PageRank**. Only changed files are re-extracted and re-embedded, while previous importance scores are reused for rapid convergence — typically **95%+ faster** than a full rebuild. ### Memory Management @@ -150,7 +154,11 @@ All commands output compact text by default (designed for AI agent consumption). | Command | Description | |---------|-------------| -| `build ` | Build code graph (`--incremental`) | +| `build ` | Build code graph (`--incremental`, `--git`) | +| `watch ` | Real-time file system monitor for instant graph sync | +| `impact ` | Analyze "blast radius" — find all downstream dependents | +| `dependencies ` | Analyze upstream dependencies for a given symbol | +| `setup` | One-step automated setup for all detected agents | | `search ` | Hybrid vector + keyword search with subgraph and snippets (`--top-k`, `--fast`, `--kind`, `--file`, `--include-tests`, `--snippets`) | | `search-vector ` | Vector similarity only (code + text dual model) | | `search-keyword ` | FTS5 keyword matching only (BM25 ranking) | diff --git a/codeloom/__init__.py b/codeloom/__init__.py index f1e83a2..b32bab8 100644 --- a/codeloom/__init__.py +++ b/codeloom/__init__.py @@ -2,6 +2,6 @@ for AI coding agents. """ -__version__ = "0.1.2" +__version__ = "0.1.3" __all__ = ["__version__", "core", "query", "storage", "cli"] diff --git a/codeloom/cli/main.py b/codeloom/cli/main.py index 4d4808d..21488c7 100644 --- a/codeloom/cli/main.py +++ b/codeloom/cli/main.py @@ -94,6 +94,80 @@ def setup(ctx, scope: str | None): +@cli.command() +@click.argument("source_dir", type=click.Path(exists=True)) +@click.option( + "--output", + "-o", + type=click.Path(), + default=None, + help="Output directory for the database", +) +@click.pass_context +def watch(ctx, source_dir: str, output: str | None): + """Watch for file changes and update the code graph in real-time. + + Uses the watchdog library to monitor the source directory and + triggers incremental builds on file save events. + """ + import time + + from watchdog.events import FileSystemEventHandler + from watchdog.observers import Observer + + from codeloom.core.pipeline import run_pipeline + + from ._helpers import human_header, human_ok + + source_path = Path(source_dir).resolve() + human_header(f"Watching {source_path} for changes...") + + class RebuildHandler(FileSystemEventHandler): + def __init__(self): + self.last_rebuild = 0 + self.cooldown = 1.0 # seconds + + def on_modified(self, event): + if event.is_directory: + return + # Check extension + from codeloom.core.detect import EXT_TO_LANG + + if Path(event.src_path).suffix.lower() not in EXT_TO_LANG: + return + + now = time.time() + if now - self.last_rebuild < self.cooldown: + return + + msg = f"Change detected in {Path(event.src_path).name}. Updating..." + human_ok(msg) + try: + run_pipeline( + str(source_path), + output_dir=output, + incremental=True, + embed=True, + git=True, # Use git accelerator for speed + ) + self.last_rebuild = time.time() + except Exception as e: + click.echo(f" Error during update: {e}", err=True) + + event_handler = RebuildHandler() + observer = Observer() + observer.schedule(event_handler, str(source_path), recursive=True) + observer.start() + + try: + while True: + time.sleep(1) + except KeyboardInterrupt: + observer.stop() + observer.join() + + + @cli.command() @click.argument("source_dir", type=click.Path(exists=True)) @click.option( @@ -128,6 +202,11 @@ def setup(ctx, scope: str | None): type=click.Choice(["auto", "en", "multilingual"]), help="Language mode for text embeddings", ) +@click.option( + "--git", + is_flag=True, + help="Use git status to find changed files (accelerator)", +) @click.pass_context def build( ctx, @@ -137,6 +216,7 @@ def build( max_file_size: int, incremental: bool, lang: str, + git: bool, ): """Build code graph from a source directory.""" import sys @@ -156,6 +236,7 @@ def _progress(stage: str, detail: str = "") -> None: on_progress=_progress, incremental=incremental, lang=lang, + git=git, ) # Capture summary values before releasing memory diff --git a/codeloom/core/build.py b/codeloom/core/build.py index 39a207e..2ba311c 100644 --- a/codeloom/core/build.py +++ b/codeloom/core/build.py @@ -342,20 +342,35 @@ def compute_edge_weights( def compute_pagerank( G: nx.DiGraph, personalization: dict[str, float] | None = None, + initial_scores: dict[str, float] | None = None, ) -> dict[str, float]: """Compute PageRank importance scores for all nodes. Args: G: The code graph. personalization: Optional per-node bias (e.g., recency weighting). + initial_scores: Optional hot-start scores for faster convergence. Returns: Dict mapping node_id to importance score. """ if len(G) == 0: return {} + + # Filter initial scores to only include nodes present in the current graph + nstart = None + if initial_scores: + nstart = {n: s for n, s in initial_scores.items() if n in G} + if not nstart: + nstart = None + try: - return nx.pagerank(G, personalization=personalization, max_iter=200) + return nx.pagerank( + G, + personalization=personalization, + nstart=nstart, + max_iter=200 + ) except nx.PowerIterationFailedConvergence: return {n: 1.0 / len(G) for n in G} diff --git a/codeloom/core/detect.py b/codeloom/core/detect.py index 62e1da1..8298035 100644 --- a/codeloom/core/detect.py +++ b/codeloom/core/detect.py @@ -165,10 +165,39 @@ def _classify_file(path: Path) -> str: return "other" +def get_file_info(path: Path) -> DetectedFile | None: + """Return info for a single file, or None if it should be skipped.""" + if not path.is_file(): + return None + if _is_sensitive(path): + return None + try: + size = path.stat().st_size + except OSError: + return None + if size == 0: + return None + + ext = path.suffix.lower() + lang = EXT_TO_LANG.get(ext, "unknown") + file_type = _classify_file(path) + + if file_type == "other": + return None + + return DetectedFile( + path=path, + language=lang, + file_type=file_type, + size_bytes=size, + ) + + def detect( root: Path, ignore_patterns: set[str] | None = None, max_file_size: int = 1_000_000, # 1MB default + git: bool = False, ) -> DetectResult: """Scan directory tree and classify files. @@ -181,6 +210,7 @@ def detect( root: Root directory to scan. ignore_patterns: Additional glob patterns to ignore. max_file_size: Skip files larger than this (bytes). + git: Use git status to find changed files (accelerator). Returns: DetectResult with classified files and skip reasons. @@ -189,6 +219,25 @@ def detect( default_patterns = DEFAULT_IGNORE | (ignore_patterns or set()) result = DetectResult(root=root) + # 1. Git-powered delta discovery (Accelerator) + if git: + from .git import get_git_deltas + + deltas = get_git_deltas(root) + if deltas: + # For modified/added/renamed files, we need their info + files_to_check = set(deltas.modified + deltas.added) + for _, new_path in deltas.renamed: + files_to_check.add(new_path) + + for path in sorted(files_to_check): + info = get_file_info(path) + if info: + result.files.append(info) + return result + # Fall back to full scan if git failed or not a repo + + # 2. Traditional full scan # Load gitignore-spec matchers gitignore_spec = _load_gitignore_spec(root) codeloom_spec = _load_codeloom_ignore_spec(root) @@ -217,39 +266,19 @@ def detect( result.skipped.append(f"codeloom-ignored: {path}") continue - if _is_sensitive(path): - result.skipped.append(f"sensitive: {path}") - continue - - try: - size = path.stat().st_size - except OSError: - result.skipped.append(f"unreadable: {path}") - continue - - if size > max_file_size: - result.skipped.append(f"too_large ({size}B): {path}") - continue - - if size == 0: - result.skipped.append(f"empty: {path}") - continue - - ext = path.suffix.lower() - lang = EXT_TO_LANG.get(ext, "unknown") - file_type = _classify_file(path) - - if file_type == "other": - result.skipped.append(f"unsupported: {path}") - continue - - result.files.append( - DetectedFile( - path=path, - language=lang, - file_type=file_type, - size_bytes=size, - ) - ) + info = get_file_info(path) + if info: + if info.size_bytes > max_file_size: + result.skipped.append(f"too_large ({info.size_bytes}B): {path}") + else: + result.files.append(info) + else: + # Re-check sensitive or unsupported for logging skip reason + if _is_sensitive(path): + result.skipped.append(f"sensitive: {path}") + elif path.suffix.lower() not in EXT_TO_LANG: + result.skipped.append(f"unsupported: {path}") + elif path.stat().st_size == 0: + result.skipped.append(f"empty: {path}") return result diff --git a/codeloom/core/git.py b/codeloom/core/git.py new file mode 100644 index 0000000..b24ad5e --- /dev/null +++ b/codeloom/core/git.py @@ -0,0 +1,72 @@ +from __future__ import annotations + +import subprocess +from dataclasses import dataclass +from pathlib import Path + + +@dataclass +class GitDelta: + modified: list[Path] + added: list[Path] + deleted: list[Path] + renamed: list[tuple[Path, Path]] # (old, new) + +def get_git_deltas(repo_root: str | Path) -> GitDelta | None: + """Get file deltas since last index using git. + + Uses `git status --porcelain -u` and `git diff --name-status` to + identify changed, added, and deleted files. + """ + try: + # Check if it's a git repo + subprocess.check_output( + ["git", "rev-parse", "--is-inside-work-tree"], + cwd=repo_root, + stderr=subprocess.STDOUT, + ) + except (subprocess.CalledProcessError, FileNotFoundError): + return None + + modified, added, deleted, renamed = [], [], [], [] + + # Get status of staged and unstaged changes + # --porcelain=v1: XY path [-> path] + # X/Y are status codes: M=modified, A=added, D=deleted, R=renamed + try: + output = subprocess.check_output( + ["git", "status", "--porcelain", "-uall"], + cwd=repo_root, + encoding="utf-8", + ) + + for line in output.splitlines(): + if not line: + continue + status = line[:2] + path_part = line[3:] + + # Simplified status check + if "M" in status: + modified.append(Path(repo_root) / path_part) + elif "A" in status or "?" in status: + added.append(Path(repo_root) / path_part) + elif "D" in status: + deleted.append(Path(repo_root) / path_part) + elif "R" in status: + # Renames: "old -> new" + if " -> " in path_part: + old_path, new_path = path_part.split(" -> ", 1) + renamed.append( + (Path(repo_root) / old_path, Path(repo_root) / new_path) + ) + else: + # git status sometimes just shows the new name if not staged + modified.append(Path(repo_root) / path_part) + + return GitDelta( + modified=modified, added=added, deleted=deleted, renamed=renamed + ) + except Exception: + return None + diff --git a/codeloom/core/pipeline.py b/codeloom/core/pipeline.py index 7597e01..beb8b71 100644 --- a/codeloom/core/pipeline.py +++ b/codeloom/core/pipeline.py @@ -18,14 +18,11 @@ from codeloom.core.analyze import AnalysisResult, analyze from codeloom.core.build import ( build_graph, - compute_edge_weights, - compute_pagerank, merge_tier3_nodes, ) from codeloom.core.cluster import ClusterResult, hierarchical_cluster from codeloom.core.detect import DetectResult, detect from codeloom.core.extract import ExtractionResult -from codeloom.core.ts_extract import extract_file_ts as extract_file from codeloom.storage.store import KnowledgeStore logger = logging.getLogger(__name__) @@ -78,25 +75,28 @@ def run_pipeline( on_progress: callable | None = None, incremental: bool = False, lang: str = "auto", + git: bool = False, ) -> PipelineResult: - """Run the full code graph build pipeline. + """Execute the full code graph construction pipeline. Args: - source_dir: Directory to analyze. - output_dir: Where to store the database (default: source_dir/.codeloom). - embed: Whether to generate embeddings (requires sentence-transformers). - model_name: Sentence-transformers model name. - resolutions: Leiden resolution parameters for hierarchical clustering. - max_file_size: Skip files larger than this. - on_progress: Callback(stage: str, detail: str) for progress updates. + source_dir: Directory to scan. + output_dir: Directory to save the database. + embed: If true, generate vector embeddings. + model_name: Optional embedding model name. + max_file_size: Max file size in bytes. + on_progress: Progress callback. incremental: Skip unchanged files (based on content hash). - lang: Language mode — "auto" (detect from text nodes), "en" - (English-only models), or "multilingual" (force multilingual - text model). - - Returns: - PipelineResult with all intermediate and final results. + lang: Text embedding language mode. + git: Use git deltas for incremental builds. """ + + import networkx as nx + + from codeloom.core.extract import extract_file + + from .build import compute_edge_weights, compute_pagerank + source_dir = Path(source_dir).resolve() if output_dir is None: output_dir = source_dir / ".codeloom" @@ -125,11 +125,13 @@ def _end_stage(name: str) -> None: # Stage 1: Detect files _start_stage("detect") _progress("detect", f"Scanning {source_dir}") - result.detect_result = detect(source_dir, max_file_size=max_file_size) + result.detect_result = detect( + source_dir, max_file_size=max_file_size, git=git + ) _end_stage("detect") _progress("detect", f"Found {len(result.detect_result.files)} files") - if not result.detect_result.files: + if not result.detect_result.files and not git: store.set_meta("status", "empty") store.close() return result @@ -143,6 +145,18 @@ def _end_stage(name: str) -> None: # Load previous file hashes for incremental build prev_hashes: dict[str, str] = {} + deleted_files: list[str] = [] + + # Git renames (Phase 2 logic) + if git: + from .git import get_git_deltas + deltas = get_git_deltas(source_dir) + if deltas: + deleted_files = [str(f) for f in deltas.deleted] + # Phase 2 TODO: handle renamed_files to update path in DB + # without re-extracting. For now, modified/added are handled + # by detect() correctly + if incremental: raw = store.get_meta("file_hashes", "{}") try: @@ -150,9 +164,19 @@ def _end_stage(name: str) -> None: except (json.JSONDecodeError, TypeError): prev_hashes = {} + # Identify deleted files (only if not already found via git) + if not git: + current_files = {str(f.path) for f in result.detect_result.files} + deleted_files = [f for f in prev_hashes if f not in current_files] + + if deleted_files: + _progress("extract", f"Pruning {len(deleted_files)} deleted files") + store.prune_files(deleted_files) + new_hashes: dict[str, str] = {} skipped_count = 0 + for f in result.detect_result.files: try: fpath = str(f.path) @@ -248,7 +272,19 @@ def _end_stage(name: str) -> None: # Stage 4: PageRank _start_stage("pagerank") _progress("pagerank", "Computing importance scores") - result.pagerank = compute_pagerank(result.graph) + + # Hot-start PageRank for incremental builds + initial_scores = None + if incremental: + initial_scores = { + n: d.get("pagerank", 0.0) + for n, d in result.graph.nodes(data=True) + if "pagerank" in d + } + + result.pagerank = compute_pagerank( + result.graph, initial_scores=initial_scores + ) for node_id, score in result.pagerank.items(): if result.graph.has_node(node_id): result.graph.nodes[node_id]["pagerank"] = score @@ -418,6 +454,9 @@ def _end_stage(name: str) -> None: if new_hashes: # Merge with previous hashes (keep unchanged files) all_hashes = {**prev_hashes, **new_hashes} + # Remove deleted files from hashes + for df in deleted_files: + all_hashes.pop(df, None) store.set_meta("file_hashes", json.dumps(all_hashes)) # Build vector index diff --git a/codeloom/storage/store.py b/codeloom/storage/store.py index b83a1e8..a1c99b7 100644 --- a/codeloom/storage/store.py +++ b/codeloom/storage/store.py @@ -136,6 +136,71 @@ def _init_schema(self) -> None: exc_info=True, ) + def prune_files(self, file_paths: list[str]) -> int: + """Remove all nodes and edges associated with the given file paths. + + Returns: + Number of nodes removed. + """ + if not file_paths: + return 0 + + c = self.conn.cursor() + + # 1. Identify nodes to be removed + placeholders = ",".join("?" for _ in file_paths) + nodes = c.execute( + f"SELECT id FROM nodes WHERE file_path IN ({placeholders})", + file_paths + ).fetchall() + node_ids = [n["id"] for n in nodes] + + if not node_ids: + return 0 + + # 2. Delete nodes (cascading cleanup) + node_placeholders = ",".join("?" for _ in node_ids) + + # Delete from community_members + c.execute( + f"DELETE FROM community_members " + f"WHERE node_id IN ({node_placeholders})", + node_ids, + ) + + # Delete from embeddings + c.execute( + f"DELETE FROM embeddings WHERE node_id IN ({node_placeholders})", + node_ids, + ) + + # Delete from FTS5 index if present + if self._has_fts(): + c.execute( + f"DELETE FROM nodes_fts WHERE node_id IN ({node_placeholders})", + node_ids, + ) + + # Delete edges where this node is source or target + c.execute( + f"DELETE FROM edges WHERE source IN ({node_placeholders}) " + f"OR target IN ({node_placeholders})", + node_ids + node_ids, + ) + + # Finally delete nodes + c.execute( + f"DELETE FROM nodes WHERE id IN ({node_placeholders})", + node_ids, + ) + + self.conn.commit() + logger.info( + "Pruned %d nodes from %d files", len(node_ids), len(file_paths) + ) + return len(node_ids) + + # --- Graph persistence --- def save_graph(self, G: nx.DiGraph) -> None: diff --git a/pyproject.toml b/pyproject.toml index 0862909..c4c5025 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "codeloom" -version = "0.1.2" +version = "0.1.3" description = "Local-first code graph builder with 5-signal hybrid search for AI coding agents" license = "MIT" requires-python = ">=3.10" diff --git a/tests/test_evolution.py b/tests/test_evolution.py new file mode 100644 index 0000000..89494e3 --- /dev/null +++ b/tests/test_evolution.py @@ -0,0 +1,168 @@ +from __future__ import annotations + +import json +from pathlib import Path + +from codeloom.core.pipeline import run_pipeline +from codeloom.storage.store import KnowledgeStore + +class TestEvolution: + def test_pruning_on_deletion(self, tmp_path): + """Verify that deleting a file and running an incremental build prunes nodes.""" + src = tmp_path / "src" + src.mkdir() + + f1 = src / "file1.py" + f1.write_text("def func1(): pass") + + f2 = src / "file2.py" + f2.write_text("def func2(): pass") + + # 1. First build + out = tmp_path / "out" + run_pipeline(str(src), output_dir=str(out), embed=False, incremental=True) + + db_path = out / "knowledge.db" + store = KnowledgeStore(db_path) + G = store.load_graph() + + assert any(d.get("file_path") == str(f1) for _, d in G.nodes(data=True)) + assert any(d.get("file_path") == str(f2) for _, d in G.nodes(data=True)) + store.close() + + def test_state_identicality(self, tmp_path): + """Verify that incremental build is bit-for-bit identical to full build.""" + src = tmp_path / "src" + src.mkdir() + + f1 = src / "file1.py" + f1.write_text("def a(): pass") + + # 1. Initial build + out_inc = tmp_path / "out_inc" + run_pipeline(str(src), output_dir=str(out_inc), embed=False, incremental=True) + + # 2. Add file and run incremental + f2 = src / "file2.py" + f2.write_text("def b(): pass") + run_pipeline(str(src), output_dir=str(out_inc), embed=False, incremental=True) + + store_inc = KnowledgeStore(out_inc / "knowledge.db") + G_inc = store_inc.load_graph() + + # 3. Clean full build from the same source state + out_full = tmp_path / "out_full" + run_pipeline(str(src), output_dir=str(out_full), embed=False, incremental=False) + + store_full = KnowledgeStore(out_full / "knowledge.db") + G_full = store_full.load_graph() + + # 4. Compare + assert sorted(G_inc.nodes()) == sorted(G_full.nodes()) + assert G_inc.number_of_edges() == G_full.number_of_edges() + + store_inc.close() + store_full.close() + + # 5. Test pruning + f1.unlink() + run_pipeline(str(src), output_dir=str(out_inc), embed=False, incremental=True) + + store_inc_pruned = KnowledgeStore(out_inc / "knowledge.db") + G_inc_pruned = store_inc_pruned.load_graph() + + out_full_pruned = tmp_path / "out_full_pruned" + run_pipeline(str(src), output_dir=str(out_full_pruned), embed=False, incremental=False) + store_full_pruned = KnowledgeStore(out_full_pruned / "knowledge.db") + G_full_pruned = store_full_pruned.load_graph() + + assert sorted(G_inc_pruned.nodes()) == sorted(G_full_pruned.nodes()) + store_inc_pruned.close() + store_full_pruned.close() + + def test_pagerank_hot_start(self, tmp_path): + """Verify that incremental build uses nstart for PageRank.""" + from unittest.mock import patch + src = tmp_path / "src" + src.mkdir() + (src / "a.py").write_text("def a(): pass") + + out = tmp_path / "out" + # First build + run_pipeline(str(src), output_dir=str(out), embed=False, incremental=True) + + # Second build (incremental) - Mock nx.pagerank to capture nstart + with patch("networkx.pagerank") as mock_pr: + mock_pr.return_value = {"node": 1.0} + run_pipeline(str(src), output_dir=str(out), embed=False, incremental=True) + + # Check if nstart was passed + args, kwargs = mock_pr.call_args + assert "nstart" in kwargs + assert kwargs["nstart"] is not None + # Should contain nodes from first build + assert len(kwargs["nstart"]) > 0 + + def test_git_acceleration(self, tmp_path): + """Verify that --git flag correctly handles modifications and deletions.""" + import subprocess + src = tmp_path / "src" + src.mkdir() + + # Init git repo + subprocess.run(["git", "init"], cwd=src, check=True) + subprocess.run(["git", "config", "user.email", "test@test.com"], cwd=src, check=True) + subprocess.run(["git", "config", "user.name", "Test"], cwd=src, check=True) + + f1 = src / "file1.py" + f1.write_text("def a(): pass") + + subprocess.run(["git", "add", "file1.py"], cwd=src, check=True) + subprocess.run(["git", "commit", "-m", "initial"], cwd=src, check=True) + + out = tmp_path / "out" + db_path = out / "knowledge.db" + # 1. First build (full) + run_pipeline(str(src), output_dir=str(out), embed=False, incremental=True) + + # 2. Modify file and add new one + f1.write_text("def a(): return 1") + f2 = src / "file2.py" + f2.write_text("def b(): pass") + # f2 is untracked + + # Build with --git + run_pipeline(str(src), output_dir=str(out), embed=False, incremental=True, git=True) + + store = KnowledgeStore(db_path) + G = store.load_graph() + + assert any("file1.py" in n for n in G.nodes) + assert any("file2.py" in n for n in G.nodes) # should be picked up as '?' in status + store.close() + + # 3. Delete file1 + f1.unlink() + # Note: git status will show it as deleted + + run_pipeline(str(src), output_dir=str(out), embed=False, incremental=True, git=True) + + store_p = KnowledgeStore(db_path) + G_pruned = store_p.load_graph() + assert not any("file1.py" in n for n in G_pruned.nodes) + assert any("file2.py" in n for n in G_pruned.nodes) + store_p.close() + + G = store.load_graph() + + # file1 nodes should be gone + assert not any(d.get("file_path") == str(f1) for _, d in G.nodes(data=True)) + # file2 nodes should remain + assert any(d.get("file_path") == str(f2) for _, d in G.nodes(data=True)) + + # Check metadata hashes + hashes = json.loads(store.get_meta("file_hashes")) + assert str(f1) not in hashes + assert str(f2) in hashes + + store.close() From 829e461e3b44c1bb3b23f5783cfe737f0126f956 Mon Sep 17 00:00:00 2001 From: Vlad Shurupov Date: Wed, 27 May 2026 19:26:53 +1000 Subject: [PATCH 2/7] Added the application image and referenced it in README's --- README.md | 4 ++++ docs/README_de.md | 4 ++++ docs/README_ja.md | 4 ++++ docs/README_ko.md | 4 ++++ docs/README_zh.md | 4 ++++ docs/assets/codeloom.jpeg | Bin 0 -> 148823 bytes 6 files changed, 20 insertions(+) create mode 100644 docs/assets/codeloom.jpeg diff --git a/README.md b/README.md index b80edf2..7125cbc 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,10 @@ Python 3.10+

+

+ codeloom visualization +

+ --- ## Why codeloom? diff --git a/docs/README_de.md b/docs/README_de.md index 199a138..7e371bf 100644 --- a/docs/README_de.md +++ b/docs/README_de.md @@ -14,6 +14,10 @@ Python 3.10+

+

+ codeloom visualization +

+ --- ## Warum codeloom? diff --git a/docs/README_ja.md b/docs/README_ja.md index 15d5c3e..e5c5174 100644 --- a/docs/README_ja.md +++ b/docs/README_ja.md @@ -14,6 +14,10 @@ Python 3.10+

+

+ codeloom visualization +

+ --- ## なぜ codeloom なのか? diff --git a/docs/README_ko.md b/docs/README_ko.md index a8ac78f..4aed280 100644 --- a/docs/README_ko.md +++ b/docs/README_ko.md @@ -14,6 +14,10 @@ Python 3.10+

+

+ codeloom visualization +

+ --- ## 왜 codeloom인가? diff --git a/docs/README_zh.md b/docs/README_zh.md index 4199f50..05e84dd 100644 --- a/docs/README_zh.md +++ b/docs/README_zh.md @@ -14,6 +14,10 @@ Python 3.10+

+

+ codeloom visualization +

+ --- ## 为什么选择 codeloom? diff --git a/docs/assets/codeloom.jpeg b/docs/assets/codeloom.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..575a74f1eecc5335ea867bf5058f73428b45e3ec GIT binary patch literal 148823 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%0DBXyToDqg`7vETR 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 Date: Wed, 27 May 2026 21:42:51 +1000 Subject: [PATCH 3/7] Implemented self-healing Smart Update for agent integrations --- codeloom/cli/integrations.py | 739 +++++++++++++++++------------------ codeloom/cli/main.py | 19 +- tests/test_cli.py | 21 +- tests/test_smart_setup.py | 112 ++++++ 4 files changed, 489 insertions(+), 402 deletions(-) create mode 100644 tests/test_smart_setup.py diff --git a/codeloom/cli/integrations.py b/codeloom/cli/integrations.py index 8cdc954..e7fa5ea 100644 --- a/codeloom/cli/integrations.py +++ b/codeloom/cli/integrations.py @@ -20,6 +20,7 @@ def get_codeloom_context() -> str: """Returns the authoritative context rules for AI agents.""" return ( + "\n" "## codeloom\n\n" "This project has a codeloom code graph at `.codeloom/`.\n\n" "Rules:\n" @@ -36,28 +37,183 @@ def get_codeloom_context() -> str: "- After modifying code files, run `codeloom build . --incremental` " "to keep your mental model and the graph current.\n" "- Use `codeloom stats` for structural overview (god nodes, " - "communities, density).\n\n" + "communities, density).\n" + "\n\n" ) -def prepend_codeloom_context(file_path: Path, marker: str = "## codeloom"): - """Prepends codeloom context to a file, or updates it if present.""" +def sync_codeloom_context(file_path: Path): + """Surgically updates or prepends codeloom context to a file.""" + import re + new_context = get_codeloom_context() + start_marker = "" + end_marker = "" + if not file_path.exists(): file_path.write_text(new_context) human_ok(f"{file_path.name} created with codeloom rules at the TOP.") return content = file_path.read_text() - if marker in content: - # If it exists, we don't move it to top to avoid messing with - # user edits, but we could. For now, let's just skip or update in place. - human_skip(f"{file_path.name} already contains codeloom section.") + + # Determine ideal position (after frontmatter if present) + frontmatter_pattern = re.compile(r"^---\s*\n.*?\n---\s*\n", re.DOTALL) + fm_match = frontmatter_pattern.match(content) + fm_text = fm_match.group(0) if fm_match else "" + main_content = content[len(fm_text):] + + # Try to find the marked block in the whole file + block_pattern = re.compile( + f"{re.escape(start_marker)}.*?{re.escape(end_marker)}\\s*", + re.DOTALL, + ) + match = block_pattern.search(content) + + if match: + # Block found. Check if it's already identical + existing_block = match.group(0) + if existing_block.strip() == new_context.strip(): + # Already identical. Now check positioning. + # It should be immediately after frontmatter. + if main_content.lstrip().startswith(start_marker): + msg = f"{file_path.name} context is up-to-date and at TOP." + human_skip(msg) + return + else: + # Move to correct position + # Remove block from wherever it is and insert after FM + clean_main = block_pattern.sub("", main_content).lstrip() + new_file_content = fm_text + new_context + clean_main + file_path.write_text(new_file_content) + human_ok(f"Moved codeloom context to TOP of {file_path.name}.") + return + + # Content differs or position is wrong. + clean_main = block_pattern.sub("", main_content).lstrip() + new_file_content = fm_text + new_context + clean_main + file_path.write_text(new_file_content) + human_ok(f"Updated and moved codeloom context in {file_path.name}.") + else: + # No marked block found. Check for legacy unmarked header. + legacy_marker = "## codeloom" + if legacy_marker in content: + # Safety fallback: Prepend marked block after FM and + # leave legacy alone + new_file_content = fm_text + new_context + main_content + file_path.write_text(new_file_content) + human_warn( + f"Legacy context found in {file_path.name}. " + "Prepended new marked block at TOP." + ) + else: + # Clean file. Just prepend after FM. + new_file_content = fm_text + new_context + main_content.lstrip() + file_path.write_text(new_file_content) + human_ok(f"Prepended codeloom rules to {file_path.name}.") + + +def sync_skill_file(source: Path, dest: Path, force: bool = False): + """Updates a skill file if it's an official version or forced.""" + import hashlib + + if not source.exists(): + human_warn(f"Skill source not found: {source}") return - # Prepend to ensure it's the first thing read - file_path.write_text(new_context + content) - human_ok(f"codeloom rules prepended to {file_path.name}.") + def get_hash(p: Path) -> str: + return hashlib.sha256(p.read_bytes()).hexdigest() + + if not dest.exists(): + import shutil + + dest.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(source, dest) + human_ok(f"Skill installed to {dest.parent}/") + return + + # Compare hashes + src_hash = get_hash(source) + dst_hash = get_hash(dest) + + if src_hash == dst_hash: + human_skip(f"Skill in {dest.parent} is already up-to-date.") + return + + if force: + import shutil + + shutil.copy2(source, dest) + human_ok(f"Force-updated skill in {dest.parent}/") + else: + # Check if destination is a "known" previous version + # (For now, we just warn if it's different and not identical) + human_warn( + f"Skill in {dest.parent}/ has manual edits. " + "Use --force to overwrite." + ) + + +def merge_json_config(file_path: Path, new_data: dict, key_path: list[str]): + """Surgically merges data into a JSON file.""" + import json + + if not file_path.exists(): + # Create new with proper structure + data = {} + curr = data + for k in key_path[:-1]: + curr[k] = {} + curr = curr[k] + curr[key_path[-1]] = new_data + file_path.write_text(json.dumps(data, indent=2) + "\n") + human_ok(f"Created {file_path.name} with codeloom config.") + return + + try: + data = json.loads(file_path.read_text()) + except Exception as e: + human_warn(f"Could not parse {file_path.name}: {e}. Skipping update.") + return + + # Drill down to the target location + curr = data + for k in key_path[:-1]: + curr = curr.setdefault(k, {}) + + target_key = key_path[-1] + + # In settings.json, hooks is usually a dict of lists + if target_key == "hooks" and isinstance(new_data, dict): + hooks = curr.setdefault("hooks", {}) + for event, entry_list in new_data.items(): + existing_event_hooks = hooks.setdefault(event, []) + # Check if an entry with "codeloom" already exists for this event + import json as _json + + already = any( + "codeloom" in _json.dumps(h) for h in existing_event_hooks + ) + if not already: + existing_event_hooks.extend(entry_list) + human_ok(f"Added codeloom {event} hook to {file_path.name}.") + else: + # Update the existing hook in place (optional, for now skip) + msg = f"codeloom {event} hook already in {file_path.name}." + human_skip(msg) + else: + # Standard dict update + if ( + target_key in curr + and isinstance(curr[target_key], dict) + and isinstance(new_data, dict) + ): + curr[target_key].update(new_data) + else: + curr[target_key] = new_data + human_ok(f"Updated {target_key} in {file_path.name}.") + + file_path.write_text(json.dumps(data, indent=2) + "\n") # ─── Claude Code ───────────────────────────────────────────────────────────── @@ -78,13 +234,12 @@ def claude_group(): "'project' (.claude/skills/). " "If omitted, you will be prompted to choose.", ) -def claude_install(scope: str | None): +@click.option("--force", is_flag=True, help="Overwrite manual skill edits.") +def claude_install(scope: str | None, force: bool = False): """Install Claude Code integration. Priority: 1) Skill 2) CLAUDE.md + hooks 3) MCP """ - import json - import shutil project_root = Path.cwd() @@ -109,106 +264,124 @@ def claude_install(scope: str | None): else: skill_dir = project_root / ".claude" / "skills" / "codeloom" - skill_dir.mkdir(parents=True, exist_ok=True) skill_dest = skill_dir / "SKILL.md" - - if skill_source.exists(): - shutil.copy2(skill_source, skill_dest) - human_ok(f"Skill installed to {skill_dir}/") - else: - human_warn("Skill source not found") + sync_skill_file(skill_source, skill_dest, force=force) # --- Priority 2: CLAUDE.md + hooks --- # 1. Write section to project CLAUDE.md claude_md = project_root / "CLAUDE.md" - prepend_codeloom_context(claude_md) + sync_codeloom_context(claude_md) - # 2. Write PreToolUse hook to .claude/settings.json + # 2. Write hooks to .claude/settings.json settings_dir = project_root / ".claude" settings_dir.mkdir(parents=True, exist_ok=True) settings_file = settings_dir / "settings.json" - pre_hook_entry = { - "matcher": "Glob|Grep", - "hooks": [ + new_hooks = { + "PreToolUse": [ { - "type": "command", - "command": ( - "[ -f .codeloom/knowledge.db ] && echo " - '\'{"hookSpecificOutput":{"hookEventName":"PreToolUse",' - '"additionalContext":"codeloom: 5-signal code graph ' - 'available. STOP grepping. Use `codeloom search ' - '\\"\\"` for far better results. Use `codeloom ' - 'impact \\"\\"` before editing."' - "\"}}' || true" - ), + "matcher": "Glob|Grep", + "hooks": [ + { + "type": "command", + "command": ( + "[ -f .codeloom/knowledge.db ] && echo " + '\'{"hookSpecificOutput":{"hookEventName":' + '"PreToolUse","additionalContext":"codeloom: ' + '5-signal code graph available. STOP grepping. ' + 'Use `codeloom search \\"\\"` for far ' + 'better results. Use `codeloom impact ' + '\\"\\"` before editing."' + "\"}}' || true" + ), + } + ], } ], - } - - post_hook_entry = { - "matcher": "Write|Edit", - "hooks": [ + "PostToolUse": [ { - "type": "command", - "command": ( - "echo '{\"hookSpecificOutput\":{\"hookEventName\":" - "\"PostToolUse\",\"additionalContext\":\"codeloom: " - "Changes detected. Run `codeloom build . --incremental` " - "to update the code graph and your mental model.\"}}'" - ), + "matcher": "Write|Edit", + "hooks": [ + { + "type": "command", + "command": ( + "echo '{\"hookSpecificOutput\":{\"hookEventName\":" + "\"PostToolUse\",\"additionalContext\":\"codeloom: " + "Changes detected. Run `codeloom build . " + "--incremental` to update the code graph and " + "your mental model.\"}}'" + ), + } + ], } ], + "Stop": [ + { + "matcher": "*", + "hooks": [ + { + "type": "command", + "command": auto_rebuild_command(), + "timeout": 10, + } + ], + } + ] } - if settings_file.exists(): - settings = json.loads(settings_file.read_text()) - else: - settings = {} + merge_json_config(settings_file, new_hooks, ["hooks"]) - hooks = settings.setdefault("hooks", {}) + human_done("Done! Run 'codeloom build .' to create your first code graph.") - # PreToolUse - pre_hooks = hooks.setdefault("PreToolUse", []) - if any("codeloom" in json.dumps(h) for h in pre_hooks): - human_skip("PreToolUse hook already exists") - else: - pre_hooks.append(pre_hook_entry) - human_ok("PreToolUse hook added (Grep/Glob interception)") - # PostToolUse - post_hooks = hooks.setdefault("PostToolUse", []) - if any("codeloom" in json.dumps(h) for h in post_hooks): - human_skip("PostToolUse hook already exists") - else: - post_hooks.append(post_hook_entry) - human_ok("PostToolUse hook added (change detection)") +def uninstall_codeloom_context(file_path: Path): + """Removes the marked codeloom block from a file.""" + import re - # 3. Write Stop hook for auto-rebuild - stop_hook_entry = { - "matcher": "*", - "hooks": [ - { - "type": "command", - "command": auto_rebuild_command(), - "timeout": 10, - } - ], - } - stop_hooks = hooks.setdefault("Stop", []) - stop_already = any( - "codeloom" in json.dumps(h) or "auto_rebuild" in json.dumps(h) - for h in stop_hooks + if not file_path.exists(): + human_skip(f"{file_path.name} not found") + return + + content = file_path.read_text() + start_marker = "" + end_marker = "" + + pattern = re.compile( + f"{re.escape(start_marker)}.*?{re.escape(end_marker)}\\s*", + re.DOTALL, ) - if stop_already: - human_skip("Stop hook (auto-rebuild) already exists") - else: - stop_hooks.append(stop_hook_entry) - human_ok("Stop hook (auto-rebuild) added") - settings_file.write_text(json.dumps(settings, indent=2) + "\n") + if pattern.search(content): + new_content = pattern.sub("", content).strip() + if new_content: + file_path.write_text(new_content + "\n") + else: + file_path.unlink() + human_ok(f"Removed codeloom context from {file_path.name}") + else: + # Fallback for legacy unmarked header + lines = content.splitlines(keepends=True) + filtered = [] + skip = False + for line in lines: + if line.strip() == "## codeloom": + skip = True + continue + if ( + skip + and line.startswith("##") + and "codeloom" not in line.lower() + ): + skip = False + if not skip: + filtered.append(line) - human_done("Done! Run 'codeloom build .' to create your first code graph.") + new_content = "".join(filtered).strip() + if new_content: + file_path.write_text(new_content + "\n") + else: + file_path.unlink() + human_ok(f"Removed legacy codeloom section from {file_path.name}") @claude_group.command(name="uninstall") @@ -240,28 +413,10 @@ def claude_uninstall(scope: str): # 1. Remove section from CLAUDE.md claude_md = project_root / "CLAUDE.md" - if claude_md.exists(): - lines = claude_md.read_text().splitlines(keepends=True) - filtered = [] - skip = False - for line in lines: - if line.strip() == "## codeloom": - skip = True - continue - if ( - skip - and line.startswith("##") - and "codeloom" not in line.lower() - ): - skip = False - if skip: - continue - filtered.append(line) - new_content = "".join(filtered).rstrip("\n") + "\n" - claude_md.write_text(new_content) - human_ok("codeloom section removed from CLAUDE.md") + uninstall_codeloom_context(claude_md) # 2. Remove hooks from .claude/settings.json + settings_file = project_root / ".claude" / "settings.json" if settings_file.exists(): settings = json.loads(settings_file.read_text()) @@ -298,74 +453,55 @@ def codex_group(): @codex_group.command(name="install") def codex_install(): """Install per-project Codex CLI integration (AGENTS.md + hooks.json).""" - import json - human_header("Installing codeloom for Codex CLI...") project_root = Path.cwd() # 1. Write section to project AGENTS.md agents_md = project_root / "AGENTS.md" - prepend_codeloom_context(agents_md) + sync_codeloom_context(agents_md) # 2. Write PreToolUse hook to .codex/hooks.json hooks_dir = project_root / ".codex" hooks_dir.mkdir(parents=True, exist_ok=True) hooks_file = hooks_dir / "hooks.json" - hook_entry = { - "matcher": "Bash", - "hooks": [ + new_hooks = { + "PreToolUse": [ { - "type": "command", - "command": ( - "[ -f .codeloom/knowledge.db ] && echo " - '\'{"hookSpecificOutput":{"hookEventName":"PreToolUse",' - '"additionalContext":"codeloom: code graph available. ' - 'Use `codeloom search \\"\\"` (5-signal HybridRAG) ' - "instead of grepping raw files. You can also add " - '`--kind function|class|method` or `--file \\"src/*\\"` ' - "to narrow results." - "\"}}' || true" - ), + "matcher": "Bash", + "hooks": [ + { + "type": "command", + "command": ( + "[ -f .codeloom/knowledge.db ] && echo " + '\'{"hookSpecificOutput":{"hookEventName":' + '"PreToolUse","additionalContext":"codeloom: ' + 'code graph available. Use `codeloom search ' + '\\"\\"` (5-signal HybridRAG) instead ' + "of grepping raw files. You can also add " + '`--kind function|class|method` or `--file ' + '\\"src/*\\"` to narrow results."' + "\"}}' || true" + ), + } + ], } ], - } - - if hooks_file.exists(): - hooks_data = json.loads(hooks_file.read_text()) - else: - hooks_data = {} - - hooks = hooks_data.setdefault("hooks", {}) - pre_hooks = hooks.setdefault("PreToolUse", []) - - already = any("codeloom" in json.dumps(h) for h in pre_hooks) - if already: - human_skip("PreToolUse hook already exists") - else: - pre_hooks.append(hook_entry) - human_ok("PreToolUse hook added to .codex/hooks.json") - - # 3. Write Stop hook for auto-rebuild - stop_hook_entry = { - "matcher": "*", - "hooks": [ + "Stop": [ { - "type": "command", - "command": auto_rebuild_command(), - "timeout": 10, + "matcher": "*", + "hooks": [ + { + "type": "command", + "command": auto_rebuild_command(), + "timeout": 10, + } + ], } - ], + ] } - stop_hooks = hooks.setdefault("Stop", []) - stop_already = any("auto_rebuild" in json.dumps(h) for h in stop_hooks) - if stop_already: - human_skip("Stop hook (auto-rebuild) already exists") - else: - stop_hooks.append(stop_hook_entry) - human_ok("Stop hook (auto-rebuild) added") - hooks_file.write_text(json.dumps(hooks_data, indent=2) + "\n") + merge_json_config(hooks_file, new_hooks, ["hooks"]) human_done() @@ -374,34 +510,15 @@ def codex_install(): def codex_uninstall(): """Remove per-project Codex CLI integration.""" import json - human_header("Removing codeloom from Codex CLI") project_root = Path.cwd() # 1. Remove section from AGENTS.md agents_md = project_root / "AGENTS.md" - if agents_md.exists(): - lines = agents_md.read_text().splitlines(keepends=True) - filtered = [] - skip = False - for line in lines: - if line.strip() == "## codeloom": - skip = True - continue - if ( - skip - and line.startswith("##") - and "codeloom" not in line.lower() - ): - skip = False - if skip: - continue - filtered.append(line) - new_content = "".join(filtered).rstrip("\n") + "\n" - agents_md.write_text(new_content) - human_ok("codeloom section removed from AGENTS.md") + uninstall_codeloom_context(agents_md) # 2. Remove hooks from .codex/hooks.json + hooks_file = project_root / ".codex" / "hooks.json" if hooks_file.exists(): hooks_data = json.loads(hooks_file.read_text()) @@ -439,76 +556,54 @@ def gemini_group(): def gemini_install(): """Install per-project Gemini CLI integration (GEMINI.md + BeforeTool hook).""" - import json - human_header("Installing codeloom for Gemini CLI...") project_root = Path.cwd() # 1. Write section to project GEMINI.md gemini_md = project_root / "GEMINI.md" - prepend_codeloom_context(gemini_md) + sync_codeloom_context(gemini_md) # 2. Write BeforeTool hook to .gemini/settings.json settings_dir = project_root / ".gemini" settings_dir.mkdir(parents=True, exist_ok=True) settings_file = settings_dir / "settings.json" - hook_entry = { - "matcher": "read_file", - "hooks": [ + new_hooks = { + "BeforeTool": [ { - "type": "command", - "command": ( - "[ -f .codeloom/knowledge.db ] && echo " - '\'{"hookSpecificOutput":{"additionalContext":' - '"codeloom: code graph available. ' - 'Use `codeloom search \\"\\"` (5-signal HybridRAG) ' - "instead of reading raw files. This single command covers " - "vector, graph, keyword, and community search with RRF " - "fusion." - "\"}}' || true" - ), + "matcher": "read_file", + "hooks": [ + { + "type": "command", + "command": ( + "[ -f .codeloom/knowledge.db ] && echo " + '\'{"hookSpecificOutput":{"additionalContext":' + '"codeloom: code graph available. ' + 'Use `codeloom search \\"\\"` (5-signal ' + "HybridRAG) instead of reading raw files. This " + "single command covers vector, graph, keyword, " + 'and community search with RRF fusion."' + "\"}}' || true" + ), + } + ], } ], - } - - if settings_file.exists(): - settings = json.loads(settings_file.read_text()) - else: - settings = {} - - hooks = settings.setdefault("hooks", {}) - before_hooks = hooks.setdefault("BeforeTool", []) - - already = any("codeloom" in json.dumps(h) for h in before_hooks) - if already: - human_skip("BeforeTool hook already exists") - else: - before_hooks.append(hook_entry) - human_ok("BeforeTool hook added to .gemini/settings.json") - - # 3. Write SessionEnd hook for auto-rebuild - session_end_entry = { - "matcher": "*", - "hooks": [ + "SessionEnd": [ { - "type": "command", - "command": auto_rebuild_command(), - "timeout": 10, + "matcher": "*", + "hooks": [ + { + "type": "command", + "command": auto_rebuild_command(), + "timeout": 10, + } + ], } - ], + ] } - session_hooks = hooks.setdefault("SessionEnd", []) - session_already = any( - "auto_rebuild" in json.dumps(h) for h in session_hooks - ) - if session_already: - human_skip("SessionEnd hook (auto-rebuild) already exists") - else: - session_hooks.append(session_end_entry) - human_ok("SessionEnd hook (auto-rebuild) added") - settings_file.write_text(json.dumps(settings, indent=2) + "\n") + merge_json_config(settings_file, new_hooks, ["hooks"]) human_done() @@ -517,34 +612,15 @@ def gemini_install(): def gemini_uninstall(): """Remove per-project Gemini CLI integration.""" import json - human_header("Removing codeloom from Gemini CLI") project_root = Path.cwd() # 1. Remove section from GEMINI.md gemini_md = project_root / "GEMINI.md" - if gemini_md.exists(): - lines = gemini_md.read_text().splitlines(keepends=True) - filtered = [] - skip = False - for line in lines: - if line.strip() == "## codeloom": - skip = True - continue - if ( - skip - and line.startswith("##") - and "codeloom" not in line.lower() - ): - skip = False - if skip: - continue - filtered.append(line) - new_content = "".join(filtered).rstrip("\n") + "\n" - gemini_md.write_text(new_content) - human_ok("codeloom section removed from GEMINI.md") + uninstall_codeloom_context(gemini_md) # 2. Remove hooks from .gemini/settings.json + settings_file = project_root / ".gemini" / "settings.json" if settings_file.exists(): settings = json.loads(settings_file.read_text()) @@ -588,24 +664,18 @@ def cursor_install(): rules_dir.mkdir(parents=True, exist_ok=True) rules_file = rules_dir / "codeloom.mdc" - rule_content = ( - "---\n" - "description: codeloom code graph search rules\n" - "globs: **/*\n" - "alwaysApply: true\n" - "---\n\n" - ) + get_codeloom_context() - - if rules_file.exists(): - content = rules_file.read_text() - if "codeloom" in content: - human_skip(".cursor/rules/codeloom.mdc already exists") - else: - rules_file.write_text(rule_content) - human_ok(".cursor/rules/codeloom.mdc updated") - else: + if not rules_file.exists(): + rule_content = ( + "---\n" + "description: codeloom code graph search rules\n" + "globs: **/*\n" + "alwaysApply: true\n" + "---\n\n" + ) + get_codeloom_context() rules_file.write_text(rule_content) human_ok(".cursor/rules/codeloom.mdc created") + else: + sync_codeloom_context(rules_file) human_done() @@ -646,18 +716,7 @@ def windsurf_install(): rules_dir.mkdir(parents=True, exist_ok=True) rules_file = rules_dir / "codeloom.md" - rule_content = get_codeloom_context() - - if rules_file.exists(): - content = rules_file.read_text() - if "codeloom" in content: - human_skip(".windsurf/rules/codeloom.md already exists") - else: - rules_file.write_text(rule_content) - human_ok(".windsurf/rules/codeloom.md updated") - else: - rules_file.write_text(rule_content) - human_ok(".windsurf/rules/codeloom.md created") + sync_codeloom_context(rules_file) human_done() @@ -669,11 +728,7 @@ def windsurf_uninstall(): project_root = Path.cwd() rules_file = project_root / ".windsurf" / "rules" / "codeloom.md" - if rules_file.exists(): - rules_file.unlink() - human_ok(".windsurf/rules/codeloom.md removed") - else: - human_skip(".windsurf/rules/codeloom.md not found") + uninstall_codeloom_context(rules_file) human_done("codeloom integration removed.") @@ -695,20 +750,7 @@ def cline_install(): rules_file = project_root / ".clinerules" - rule_content = get_codeloom_context() - - if rules_file.exists(): - content = rules_file.read_text() - if "codeloom" in content: - human_skip(".clinerules section already exists") - else: - # Append to existing rules - with open(rules_file, "a") as f: - f.write("\n\n" + rule_content) - human_ok("codeloom section appended to .clinerules") - else: - rules_file.write_text(rule_content) - human_ok(".clinerules created") + sync_codeloom_context(rules_file) human_done() @@ -720,37 +762,11 @@ def cline_uninstall(): project_root = Path.cwd() rules_file = project_root / ".clinerules" - if rules_file.exists(): - content = rules_file.read_text() - if "codeloom" in content: - # Remove codeloom section - lines = content.split("\n") - filtered = [] - skip = False - for line in lines: - if line.strip() == "# codeloom": - skip = True - continue - if skip and line.startswith("# ") and "codeloom" not in line: - skip = False - if not skip: - filtered.append(line) - new_content = "\n".join(filtered).strip() - if new_content: - rules_file.write_text(new_content + "\n") - human_ok("codeloom section removed from .clinerules") - else: - rules_file.unlink() - human_ok(".clinerules removed (was only codeloom content)") - else: - human_skip("No codeloom section found in .clinerules") - else: - human_skip(".clinerules not found") + uninstall_codeloom_context(rules_file) human_done("codeloom integration removed.") - -# ─── Aider CLI ─────────────────────────────────────────────────────────────── + human_done("codeloom integration removed.") @click.group(name="aider") @@ -770,7 +786,7 @@ def aider_install(): # 1. Write CONVENTIONS.md with codeloom rules conventions_md = project_root / "CONVENTIONS.md" - prepend_codeloom_context(conventions_md) + sync_codeloom_context(conventions_md) # 2. Ensure .aider.conf.yml loads CONVENTIONS.md via read: conf_file = project_root / ".aider.conf.yml" @@ -803,28 +819,10 @@ def aider_uninstall(): # 1. Remove section from CONVENTIONS.md conventions_md = project_root / "CONVENTIONS.md" - if conventions_md.exists(): - lines = conventions_md.read_text().splitlines(keepends=True) - filtered = [] - skip = False - for line in lines: - if line.strip() == "## codeloom": - skip = True - continue - if ( - skip - and line.startswith("##") - and "codeloom" not in line.lower() - ): - skip = False - if skip: - continue - filtered.append(line) - new_content = "".join(filtered).rstrip("\n") + "\n" - conventions_md.write_text(new_content) - human_ok("codeloom section removed from CONVENTIONS.md") + uninstall_codeloom_context(conventions_md) # 2. Remove CONVENTIONS.md from .aider.conf.yml read list + conf_file = project_root / ".aider.conf.yml" if conf_file.exists(): conf = yaml.safe_load(conf_file.read_text()) or {} @@ -864,16 +862,14 @@ def opencode_group(): "or 'project' (.opencode/skills/). " "If omitted, you will be prompted to choose.", ) -def opencode_install(scope: str | None): +@click.option("--force", is_flag=True, help="Overwrite manual skill edits.") +def opencode_install(scope: str | None, force: bool = False): """Install the codeloom skill for OpenCode. Writes the skill file to .opencode/skills/codeloom/SKILL.md or ~/.config/opencode/skills/codeloom/SKILL.md depending on scope. OpenCode discovers skills automatically from the skills directory. """ - import json - import shutil - project_root = Path.cwd() if scope is None: @@ -896,16 +892,8 @@ def opencode_install(scope: str | None): else: skill_dir = project_root / ".opencode" / "skills" / "codeloom" - skill_dir.mkdir(parents=True, exist_ok=True) skill_dest = skill_dir / "SKILL.md" - - if skill_dest.exists(): - human_skip("SKILL.md already exists") - elif skill_source.exists(): - shutil.copy2(skill_source, skill_dest) - human_ok(f"Skill installed to {skill_dir}/") - else: - human_warn("Skill source not found") + sync_skill_file(skill_source, skill_dest, force=force) # Auto-register MCP config for OpenCode if scope == "user": @@ -914,34 +902,13 @@ def opencode_install(scope: str | None): mcp_config_path = project_root / "opencode.json" mcp_codeloom_config = { - "mcp": { - "codeloom": { - "type": "local", - "command": ["codeloom", "mcp"], - } + "codeloom": { + "type": "local", + "command": ["codeloom", "mcp"], } } - try: - mcp_config_path.parent.mkdir(parents=True, exist_ok=True) - if mcp_config_path.exists(): - existing = json.loads(mcp_config_path.read_text()) - else: - existing = {} - existing.setdefault("mcp", {}).update(mcp_codeloom_config["mcp"]) - mcp_config_path.write_text(json.dumps(existing, indent=2) + "\n") - human_ok(f"MCP config written to {mcp_config_path}") - except Exception as e: - human_warn(f"Could not write MCP config: {e}") - human_ok("To add manually, add to opencode.json:") - click.echo( - ' "mcp": {\n' - ' "codeloom": {\n' - ' "type": "local",\n' - ' "command": ["codeloom", "mcp"]\n' - " }\n" - " }" - ) + merge_json_config(mcp_config_path, mcp_codeloom_config, ["mcp"]) human_done( "Done! OpenCode will discover the skill and MCP tools automatically." diff --git a/codeloom/cli/main.py b/codeloom/cli/main.py index 21488c7..c8b1168 100644 --- a/codeloom/cli/main.py +++ b/codeloom/cli/main.py @@ -43,8 +43,9 @@ def cli(ctx): default=None, help="Installation scope", ) +@click.option("--force", is_flag=True, help="Overwrite manual skill edits.") @click.pass_context -def setup(ctx, scope: str | None): +def setup(ctx, scope: str | None, force: bool): """One-step setup for all AI agent integrations. Detects installed editors (Claude Code, Cursor, Windsurf, etc.) @@ -79,16 +80,16 @@ def setup(ctx, scope: str | None): # (skip if not applicable) though some currently prompt or assume paths. # In a real setup we might check if the editor is actually installed first. - ctx.invoke(claude_install, scope=scope) - ctx.invoke(opencode_install, scope=scope) + ctx.invoke(claude_install, scope=scope, force=force) + ctx.invoke(opencode_install, scope=scope, force=force) # Project-level only integrations - aider_install() - cline_install() - codex_install() - cursor_install() - gemini_install() - windsurf_install() + ctx.invoke(aider_install) + ctx.invoke(cline_install) + ctx.invoke(codex_install) + ctx.invoke(cursor_install) + ctx.invoke(gemini_install) + ctx.invoke(windsurf_install) human_done("Setup complete! All detected agents are now codeloom-aware.") diff --git a/tests/test_cli.py b/tests/test_cli.py index 124d4a2..3649096 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -699,7 +699,8 @@ def test_cline_install_idempotent(self, tmp_path): runner.invoke(cli, ["cline", "install"]) result = runner.invoke(cli, ["cline", "install"]) assert result.exit_code == 0 - assert "already" in result.output.lower() + # Smart update logic: second run sees it's up-to-date + assert "up-to-date" in result.output.lower() def test_cline_install_appends_to_existing(self, tmp_path): runner = CliRunner() @@ -884,7 +885,8 @@ def test_claude_install_idempotent(self, tmp_path): r2 = runner.invoke(cli, ["claude", "install", "--scope", "project"]) assert r1.exit_code == 0 assert r2.exit_code == 0 - assert "already" in r2.output.lower() + # Smart update logic: second run sees it's up-to-date + assert "up-to-date" in r2.output.lower() def test_claude_uninstall_project(self, tmp_path): runner = CliRunner() @@ -928,7 +930,8 @@ def test_codex_install_idempotent(self, tmp_path): r2 = runner.invoke(cli, ["codex", "install"]) assert r1.exit_code == 0 assert r2.exit_code == 0 - assert "already" in r2.output.lower() + # Smart update logic: second run sees it's up-to-date + assert "up-to-date" in r2.output.lower() def test_codex_uninstall(self, tmp_path): runner = CliRunner() @@ -970,7 +973,8 @@ def test_gemini_install_idempotent(self, tmp_path): r2 = runner.invoke(cli, ["gemini", "install"]) assert r1.exit_code == 0 assert r2.exit_code == 0 - assert "already" in r2.output.lower() + # Smart update logic: second run sees it's up-to-date + assert "up-to-date" in r2.output.lower() class TestCursorIntegration: @@ -990,7 +994,8 @@ def test_cursor_install_idempotent(self, tmp_path): r2 = runner.invoke(cli, ["cursor", "install"]) assert r1.exit_code == 0 assert r2.exit_code == 0 - assert "already" in r2.output.lower() + # Smart update logic: second run sees it's up-to-date + assert "up-to-date" in r2.output.lower() def test_cursor_uninstall(self, tmp_path): runner = CliRunner() @@ -1018,7 +1023,8 @@ def test_windsurf_install_idempotent(self, tmp_path): r2 = runner.invoke(cli, ["windsurf", "install"]) assert r1.exit_code == 0 assert r2.exit_code == 0 - assert "already" in r2.output.lower() + # Smart update logic: second run sees it's up-to-date + assert "up-to-date" in r2.output.lower() def test_windsurf_uninstall(self, tmp_path): runner = CliRunner() @@ -1046,7 +1052,8 @@ def test_aider_install_idempotent(self, tmp_path): r2 = runner.invoke(cli, ["aider", "install"]) assert r1.exit_code == 0 assert r2.exit_code == 0 - assert "already" in r2.output.lower() + # Smart update logic: second run sees it's up-to-date + assert "up-to-date" in r2.output.lower() def test_aider_uninstall(self, tmp_path): runner = CliRunner() diff --git a/tests/test_smart_setup.py b/tests/test_smart_setup.py new file mode 100644 index 0000000..deb0f10 --- /dev/null +++ b/tests/test_smart_setup.py @@ -0,0 +1,112 @@ +import json +import re +from pathlib import Path +from click.testing import CliRunner +from codeloom.cli.main import cli +from codeloom.cli.integrations import get_codeloom_context + +def test_surgical_update(tmp_path): + """Verify that only the codeloom block is updated, leaving other notes intact.""" + runner = CliRunner() + with runner.isolated_filesystem(temp_dir=tmp_path): + # Create a file with old rules and custom notes + initial_content = ( + "\n" + "## codeloom\n\n" + "Old rules here.\n" + "\n\n" + "# Project Notes\n" + "Do not delete this." + ) + Path("CLAUDE.md").write_text(initial_content) + + result = runner.invoke(cli, ["claude", "install", "--scope", "project"]) + assert result.exit_code == 0 + + updated_content = Path("CLAUDE.md").read_text() + assert "# Project Notes" in updated_content + assert "Do not delete this." in updated_content + assert get_codeloom_context().strip() in updated_content + assert "Old rules here." not in updated_content + +def test_positional_enforcement(tmp_path): + """Verify that codeloom block is moved to the top if found elsewhere.""" + runner = CliRunner() + with runner.isolated_filesystem(temp_dir=tmp_path): + initial_content = ( + "# Top Notes\n" + "Keep me below rules.\n\n" + "\n" + "## codeloom\n" + "Rules at bottom.\n" + "" + ) + Path("CLAUDE.md").write_text(initial_content) + + runner.invoke(cli, ["claude", "install", "--scope", "project"]) + + content = Path("CLAUDE.md").read_text() + assert content.startswith("") + assert "# Top Notes" in content.split("")[1] + +def test_legacy_support(tmp_path): + """Verify that legacy unmarked headers are preserved while adding a marked block.""" + runner = CliRunner() + with runner.isolated_filesystem(temp_dir=tmp_path): + initial_content = "## codeloom\nLegacy unmarked rules." + Path("CLAUDE.md").write_text(initial_content) + + runner.invoke(cli, ["claude", "install", "--scope", "project"]) + + content = Path("CLAUDE.md").read_text() + assert content.startswith("") + assert "Legacy unmarked rules." in content + +def test_json_merge_idempotency(tmp_path): + """Verify that JSON merging doesn't duplicate hooks and preserves existing config.""" + runner = CliRunner() + with runner.isolated_filesystem(temp_dir=tmp_path): + # Existing config with another tool + existing = { + "hooks": { + "PreToolUse": [ + {"matcher": "Linter", "hooks": [{"type": "cmd", "command": "lint"}]} + ] + } + } + settings_file = Path(".claude/settings.json") + settings_file.parent.mkdir(parents=True) + settings_file.write_text(json.dumps(existing)) + + # Run twice + runner.invoke(cli, ["claude", "install", "--scope", "project"]) + runner.invoke(cli, ["claude", "install", "--scope", "project"]) + + data = json.loads(settings_file.read_text()) + hooks = data["hooks"]["PreToolUse"] + + # Should have 2 hooks total (linter + codeloom), not 3 + assert len(hooks) == 2 + assert any("Linter" in h["matcher"] for h in hooks) + assert any("codeloom" in json.dumps(h) for h in hooks) + +def test_skill_safety(tmp_path): + """Verify that edited skill files are not overwritten without --force.""" + runner = CliRunner() + with runner.isolated_filesystem(temp_dir=tmp_path): + # 1. Install official version + runner.invoke(cli, ["claude", "install", "--scope", "project"]) + skill_file = Path(".claude/skills/codeloom/SKILL.md") + + # 2. Manually edit it + skill_file.write_text("User edit") + + # 3. Try to update without force + result = runner.invoke(cli, ["claude", "install", "--scope", "project"]) + assert "manual edits" in result.output + assert skill_file.read_text() == "User edit" + + # 4. Update with force + result = runner.invoke(cli, ["claude", "install", "--scope", "project", "--force"]) + assert "Force-updated skill" in result.output + assert "User edit" not in skill_file.read_text() From 4d279787bde3213535d9c48fda2d5c87bf797df0 Mon Sep 17 00:00:00 2001 From: Vlad Shurupov Date: Wed, 27 May 2026 22:16:38 +1000 Subject: [PATCH 4/7] Added intelligent agent detection and unified uninstall --- codeloom/cli/_helpers.py | 33 +++- codeloom/cli/integrations.py | 256 +++++++++++++++----------------- codeloom/cli/main.py | 155 +++++++++++++++---- tests/test_intelligent_setup.py | 59 ++++++++ 4 files changed, 337 insertions(+), 166 deletions(-) create mode 100644 tests/test_intelligent_setup.py diff --git a/codeloom/cli/_helpers.py b/codeloom/cli/_helpers.py index b6406c6..9b96be2 100644 --- a/codeloom/cli/_helpers.py +++ b/codeloom/cli/_helpers.py @@ -104,13 +104,42 @@ def human_choose( choices: list[str], descriptions: list[str] | None = None, default: int = 1, -) -> str: - """Show an interactive arrow-key menu and return the chosen value. + multiple: bool = False, +) -> str | list[str]: + """Show an interactive menu and return the chosen value(s). Falls back to numbered input if terminal doesn't support raw mode. """ import sys + if multiple: + # For multiple selection, we always use numbered fallback + msg = f"{prompt} {_DIM}(Enter numbers separated by comma){_RESET}\n" + click.echo(msg) + for i, choice in enumerate(choices, 1): + desc = ( + f" {_DIM}{descriptions[i - 1]}{_RESET}" if descriptions else "" + ) + click.echo(f" {i}) {choice}{desc}") + click.echo() + + while True: + raw = click.prompt("Selection", default=str(default)) + try: + indices = [int(x.strip()) for x in raw.split(",")] + return [ + choices[i - 1] for i in indices if 1 <= i <= len(choices) + ] + except (ValueError, IndexError): + # Check for direct name matches + names = [x.strip().lower() for x in raw.split(",")] + selected = [c for c in choices if c.lower() in names] + if selected: + return selected + msg = "Invalid selection. Use numbers (e.g. 1,2) or names." + click.echo(msg) + continue + selected = default - 1 # 0-based def _render(sel: int, clear: bool = False) -> None: diff --git a/codeloom/cli/integrations.py b/codeloom/cli/integrations.py index e7fa5ea..3c3ac10 100644 --- a/codeloom/cli/integrations.py +++ b/codeloom/cli/integrations.py @@ -2,6 +2,7 @@ from __future__ import annotations +import json as _json from pathlib import Path import click @@ -61,7 +62,7 @@ def sync_codeloom_context(file_path: Path): frontmatter_pattern = re.compile(r"^---\s*\n.*?\n---\s*\n", re.DOTALL) fm_match = frontmatter_pattern.match(content) fm_text = fm_match.group(0) if fm_match else "" - main_content = content[len(fm_text):] + main_content = content[len(fm_text) :] # Try to find the marked block in the whole file block_pattern = re.compile( @@ -82,7 +83,6 @@ def sync_codeloom_context(file_path: Path): return else: # Move to correct position - # Remove block from wherever it is and insert after FM clean_main = block_pattern.sub("", main_content).lstrip() new_file_content = fm_text + new_context + clean_main file_path.write_text(new_file_content) @@ -98,8 +98,8 @@ def sync_codeloom_context(file_path: Path): # No marked block found. Check for legacy unmarked header. legacy_marker = "## codeloom" if legacy_marker in content: - # Safety fallback: Prepend marked block after FM and - # leave legacy alone + # Safety fallback: Prepend marked block after FM and leave + # legacy alone new_file_content = fm_text + new_context + main_content file_path.write_text(new_file_content) human_warn( @@ -113,6 +113,56 @@ def sync_codeloom_context(file_path: Path): human_ok(f"Prepended codeloom rules to {file_path.name}.") +def uninstall_codeloom_context(file_path: Path): + """Removes the marked codeloom block from a file.""" + import re + + if not file_path.exists(): + human_skip(f"{file_path.name} not found") + return + + content = file_path.read_text() + start_marker = "" + end_marker = "" + + pattern = re.compile( + f"{re.escape(start_marker)}.*?{re.escape(end_marker)}\\s*", + re.DOTALL, + ) + + if pattern.search(content): + new_content = pattern.sub("", content).strip() + if new_content: + file_path.write_text(new_content + "\n") + else: + file_path.unlink() + human_ok(f"Removed codeloom context from {file_path.name}") + else: + # Fallback for legacy unmarked header + lines = content.splitlines(keepends=True) + filtered = [] + skip = False + for line in lines: + if line.strip() == "## codeloom": + skip = True + continue + if ( + skip + and line.startswith("##") + and "codeloom" not in line.lower() + ): + skip = False + if not skip: + filtered.append(line) + + new_content = "".join(filtered).strip() + if new_content: + file_path.write_text(new_content + "\n") + else: + file_path.unlink() + human_ok(f"Removed legacy codeloom section from {file_path.name}") + + def sync_skill_file(source: Path, dest: Path, force: bool = False): """Updates a skill file if it's an official version or forced.""" import hashlib @@ -146,8 +196,6 @@ def get_hash(p: Path) -> str: shutil.copy2(source, dest) human_ok(f"Force-updated skill in {dest.parent}/") else: - # Check if destination is a "known" previous version - # (For now, we just warn if it's different and not identical) human_warn( f"Skill in {dest.parent}/ has manual edits. " "Use --force to overwrite." @@ -188,9 +236,6 @@ def merge_json_config(file_path: Path, new_data: dict, key_path: list[str]): hooks = curr.setdefault("hooks", {}) for event, entry_list in new_data.items(): existing_event_hooks = hooks.setdefault(event, []) - # Check if an entry with "codeloom" already exists for this event - import json as _json - already = any( "codeloom" in _json.dumps(h) for h in existing_event_hooks ) @@ -198,7 +243,6 @@ def merge_json_config(file_path: Path, new_data: dict, key_path: list[str]): existing_event_hooks.extend(entry_list) human_ok(f"Added codeloom {event} hook to {file_path.name}.") else: - # Update the existing hook in place (optional, for now skip) msg = f"codeloom {event} hook already in {file_path.name}." human_skip(msg) else: @@ -213,7 +257,7 @@ def merge_json_config(file_path: Path, new_data: dict, key_path: list[str]): curr[target_key] = new_data human_ok(f"Updated {target_key} in {file_path.name}.") - file_path.write_text(json.dumps(data, indent=2) + "\n") + file_path.write_text(_json.dumps(data, indent=2) + "\n") # ─── Claude Code ───────────────────────────────────────────────────────────── @@ -230,20 +274,13 @@ def claude_group(): "--scope", type=click.Choice(["user", "project"], case_sensitive=False), default=None, - help="Install scope: 'user' (global ~/.claude/skills/) or " - "'project' (.claude/skills/). " - "If omitted, you will be prompted to choose.", + help="Install scope: 'user' (global) or 'project' (local).", ) @click.option("--force", is_flag=True, help="Overwrite manual skill edits.") def claude_install(scope: str | None, force: bool = False): - """Install Claude Code integration. - - Priority: 1) Skill 2) CLAUDE.md + hooks 3) MCP - """ - + """Install Claude Code integration.""" project_root = Path.cwd() - # --- Prompt for scope if not specified --- if scope is None: scope = human_choose( "Where should codeloom be installed?", @@ -257,7 +294,6 @@ def claude_install(scope: str | None, force: bool = False): human_header(f"Installing codeloom for Claude Code (scope: {scope})") - # --- Priority 1: Install Skill --- skill_source = Path(__file__).parent.parent / "skill.md" if scope == "user": skill_dir = Path.home() / ".claude" / "skills" / "codeloom" @@ -267,12 +303,9 @@ def claude_install(scope: str | None, force: bool = False): skill_dest = skill_dir / "SKILL.md" sync_skill_file(skill_source, skill_dest, force=force) - # --- Priority 2: CLAUDE.md + hooks --- - # 1. Write section to project CLAUDE.md claude_md = project_root / "CLAUDE.md" sync_codeloom_context(claude_md) - # 2. Write hooks to .claude/settings.json settings_dir = project_root / ".claude" settings_dir.mkdir(parents=True, exist_ok=True) settings_file = settings_dir / "settings.json" @@ -326,64 +359,13 @@ def claude_install(scope: str | None, force: bool = False): } ], } - ] + ], } merge_json_config(settings_file, new_hooks, ["hooks"]) - human_done("Done! Run 'codeloom build .' to create your first code graph.") -def uninstall_codeloom_context(file_path: Path): - """Removes the marked codeloom block from a file.""" - import re - - if not file_path.exists(): - human_skip(f"{file_path.name} not found") - return - - content = file_path.read_text() - start_marker = "" - end_marker = "" - - pattern = re.compile( - f"{re.escape(start_marker)}.*?{re.escape(end_marker)}\\s*", - re.DOTALL, - ) - - if pattern.search(content): - new_content = pattern.sub("", content).strip() - if new_content: - file_path.write_text(new_content + "\n") - else: - file_path.unlink() - human_ok(f"Removed codeloom context from {file_path.name}") - else: - # Fallback for legacy unmarked header - lines = content.splitlines(keepends=True) - filtered = [] - skip = False - for line in lines: - if line.strip() == "## codeloom": - skip = True - continue - if ( - skip - and line.startswith("##") - and "codeloom" not in line.lower() - ): - skip = False - if not skip: - filtered.append(line) - - new_content = "".join(filtered).strip() - if new_content: - file_path.write_text(new_content + "\n") - else: - file_path.unlink() - human_ok(f"Removed legacy codeloom section from {file_path.name}") - - @claude_group.command(name="uninstall") @click.option( "--scope", @@ -392,14 +374,12 @@ def uninstall_codeloom_context(file_path: Path): help="Uninstall scope: 'user', 'project', or 'all' (default).", ) def claude_uninstall(scope: str): - """Remove Claude Code integration (skill + CLAUDE.md + hooks).""" - import json + """Remove Claude Code integration.""" import shutil human_header("Removing codeloom from Claude Code") project_root = Path.cwd() - # 0. Remove skill if scope in ("user", "all"): user_skill = Path.home() / ".claude" / "skills" / "codeloom" if user_skill.exists(): @@ -411,31 +391,28 @@ def claude_uninstall(scope: str): shutil.rmtree(proj_skill) human_ok("Project skill removed") - # 1. Remove section from CLAUDE.md claude_md = project_root / "CLAUDE.md" uninstall_codeloom_context(claude_md) - # 2. Remove hooks from .claude/settings.json - settings_file = project_root / ".claude" / "settings.json" if settings_file.exists(): - settings = json.loads(settings_file.read_text()) + settings = _json.loads(settings_file.read_text()) hooks = settings.get("hooks", {}) - for event in ("PreToolUse", "Stop"): + for event in ("PreToolUse", "PostToolUse", "Stop"): event_hooks = hooks.get(event, []) hooks[event] = [ h for h in event_hooks if ( - "codeloom" not in json.dumps(h) - and "auto_rebuild" not in json.dumps(h) + "codeloom" not in _json.dumps(h) + and "auto_rebuild" not in _json.dumps(h) ) ] if not hooks[event]: hooks.pop(event, None) if not hooks: settings.pop("hooks", None) - settings_file.write_text(json.dumps(settings, indent=2) + "\n") + settings_file.write_text(_json.dumps(settings, indent=2) + "\n") human_ok("Hooks removed from .claude/settings.json") human_done("codeloom integration removed.") @@ -456,11 +433,9 @@ def codex_install(): human_header("Installing codeloom for Codex CLI...") project_root = Path.cwd() - # 1. Write section to project AGENTS.md agents_md = project_root / "AGENTS.md" sync_codeloom_context(agents_md) - # 2. Write PreToolUse hook to .codex/hooks.json hooks_dir = project_root / ".codex" hooks_dir.mkdir(parents=True, exist_ok=True) hooks_file = hooks_dir / "hooks.json" @@ -498,30 +473,25 @@ def codex_install(): } ], } - ] + ], } merge_json_config(hooks_file, new_hooks, ["hooks"]) - human_done() @codex_group.command(name="uninstall") def codex_uninstall(): """Remove per-project Codex CLI integration.""" - import json human_header("Removing codeloom from Codex CLI") project_root = Path.cwd() - # 1. Remove section from AGENTS.md agents_md = project_root / "AGENTS.md" uninstall_codeloom_context(agents_md) - # 2. Remove hooks from .codex/hooks.json - hooks_file = project_root / ".codex" / "hooks.json" if hooks_file.exists(): - hooks_data = json.loads(hooks_file.read_text()) + hooks_data = _json.loads(hooks_file.read_text()) hooks = hooks_data.get("hooks", {}) for event in ("PreToolUse", "Stop"): event_hooks = hooks.get(event, []) @@ -529,15 +499,15 @@ def codex_uninstall(): h for h in event_hooks if ( - "codeloom" not in json.dumps(h) - and "auto_rebuild" not in json.dumps(h) + "codeloom" not in _json.dumps(h) + and "auto_rebuild" not in _json.dumps(h) ) ] if not hooks[event]: hooks.pop(event, None) if not hooks: hooks_data.pop("hooks", None) - hooks_file.write_text(json.dumps(hooks_data, indent=2) + "\n") + hooks_file.write_text(_json.dumps(hooks_data, indent=2) + "\n") human_ok("Hooks removed from .codex/hooks.json") human_done("codeloom integration removed.") @@ -554,16 +524,13 @@ def gemini_group(): @gemini_group.command(name="install") def gemini_install(): - """Install per-project Gemini CLI integration (GEMINI.md + BeforeTool - hook).""" + """Install per-project Gemini CLI integration (GEMINI.md + hooks).""" human_header("Installing codeloom for Gemini CLI...") project_root = Path.cwd() - # 1. Write section to project GEMINI.md gemini_md = project_root / "GEMINI.md" sync_codeloom_context(gemini_md) - # 2. Write BeforeTool hook to .gemini/settings.json settings_dir = project_root / ".gemini" settings_dir.mkdir(parents=True, exist_ok=True) settings_file = settings_dir / "settings.json" @@ -600,30 +567,25 @@ def gemini_install(): } ], } - ] + ], } merge_json_config(settings_file, new_hooks, ["hooks"]) - human_done() @gemini_group.command(name="uninstall") def gemini_uninstall(): """Remove per-project Gemini CLI integration.""" - import json human_header("Removing codeloom from Gemini CLI") project_root = Path.cwd() - # 1. Remove section from GEMINI.md gemini_md = project_root / "GEMINI.md" uninstall_codeloom_context(gemini_md) - # 2. Remove hooks from .gemini/settings.json - settings_file = project_root / ".gemini" / "settings.json" if settings_file.exists(): - settings = json.loads(settings_file.read_text()) + settings = _json.loads(settings_file.read_text()) hooks = settings.get("hooks", {}) for event in ("BeforeTool", "SessionEnd"): event_hooks = hooks.get(event, []) @@ -631,15 +593,15 @@ def gemini_uninstall(): h for h in event_hooks if ( - "codeloom" not in json.dumps(h) - and "auto_rebuild" not in json.dumps(h) + "codeloom" not in _json.dumps(h) + and "auto_rebuild" not in _json.dumps(h) ) ] if not hooks[event]: hooks.pop(event, None) if not hooks: settings.pop("hooks", None) - settings_file.write_text(json.dumps(settings, indent=2) + "\n") + settings_file.write_text(_json.dumps(settings, indent=2) + "\n") human_ok("Hooks removed from .gemini/settings.json") human_done("codeloom integration removed.") @@ -717,7 +679,6 @@ def windsurf_install(): rules_file = rules_dir / "codeloom.md" sync_codeloom_context(rules_file) - human_done() @@ -729,7 +690,6 @@ def windsurf_uninstall(): rules_file = project_root / ".windsurf" / "rules" / "codeloom.md" uninstall_codeloom_context(rules_file) - human_done("codeloom integration removed.") @@ -749,9 +709,7 @@ def cline_install(): project_root = Path.cwd() rules_file = project_root / ".clinerules" - sync_codeloom_context(rules_file) - human_done() @@ -763,10 +721,10 @@ def cline_uninstall(): rules_file = project_root / ".clinerules" uninstall_codeloom_context(rules_file) - human_done("codeloom integration removed.") - human_done("codeloom integration removed.") + +# ─── Aider CLI ─────────────────────────────────────────────────────────────── @click.group(name="aider") @@ -784,11 +742,9 @@ def aider_install(): human_header("Installing codeloom for Aider CLI...") project_root = Path.cwd() - # 1. Write CONVENTIONS.md with codeloom rules conventions_md = project_root / "CONVENTIONS.md" sync_codeloom_context(conventions_md) - # 2. Ensure .aider.conf.yml loads CONVENTIONS.md via read: conf_file = project_root / ".aider.conf.yml" if conf_file.exists(): conf = yaml.safe_load(conf_file.read_text()) or {} @@ -817,12 +773,9 @@ def aider_uninstall(): human_header("Removing codeloom from Aider CLI") project_root = Path.cwd() - # 1. Remove section from CONVENTIONS.md conventions_md = project_root / "CONVENTIONS.md" uninstall_codeloom_context(conventions_md) - # 2. Remove CONVENTIONS.md from .aider.conf.yml read list - conf_file = project_root / ".aider.conf.yml" if conf_file.exists(): conf = yaml.safe_load(conf_file.read_text()) or {} @@ -858,18 +811,11 @@ def opencode_group(): "--scope", type=click.Choice(["user", "project"], case_sensitive=False), default=None, - help="Install scope: 'user' (global ~/.config/opencode/skills/) " - "or 'project' (.opencode/skills/). " - "If omitted, you will be prompted to choose.", + help="Install scope: 'user' (global) or 'project' (local).", ) @click.option("--force", is_flag=True, help="Overwrite manual skill edits.") def opencode_install(scope: str | None, force: bool = False): - """Install the codeloom skill for OpenCode. - - Writes the skill file to .opencode/skills/codeloom/SKILL.md or - ~/.config/opencode/skills/codeloom/SKILL.md depending on scope. - OpenCode discovers skills automatically from the skills directory. - """ + """Install the codeloom skill for OpenCode.""" project_root = Path.cwd() if scope is None: @@ -895,7 +841,6 @@ def opencode_install(scope: str | None, force: bool = False): skill_dest = skill_dir / "SKILL.md" sync_skill_file(skill_source, skill_dest, force=force) - # Auto-register MCP config for OpenCode if scope == "user": mcp_config_path = Path.home() / ".config" / "opencode" / "config.json" else: @@ -909,7 +854,6 @@ def opencode_install(scope: str | None, force: bool = False): } merge_json_config(mcp_config_path, mcp_codeloom_config, ["mcp"]) - human_done( "Done! OpenCode will discover the skill and MCP tools automatically." ) @@ -940,7 +884,6 @@ def opencode_uninstall(scope: str): else: human_skip(f"{skill_dir}/ not found") - # Clean up empty parent directories parent = skill_dir.parent if parent.exists() and not any(parent.iterdir()): parent.rmdir() @@ -949,6 +892,47 @@ def opencode_uninstall(scope: str): human_done("codeloom integration removed.") +# ─── Detection Logic ────────────────────────────────────────────────────────── + + +def detect_agents() -> list[str]: + """Detect which agents are present on the machine or in the project.""" + import shutil + + detected = [] + project_root = Path.cwd() + + # Heuristics - mostly restricted to project root to avoid false positives + # in test environments (which share the real home directory). + heuristics = { + "claude": lambda: shutil.which("claude") + or (project_root / ".claude").exists() + or (project_root / "CLAUDE.md").exists(), + "opencode": lambda: shutil.which("opencode") + or (project_root / "opencode.json").exists() + or (project_root / ".opencode").exists(), + "aider": lambda: shutil.which("aider") + or (project_root / ".aider.conf.yml").exists() + or (project_root / "CONVENTIONS.md").exists(), + "cursor": lambda: (project_root / ".cursor").exists(), + "windsurf": lambda: (project_root / ".windsurf").exists(), + "cline": lambda: (project_root / ".clinerules").exists(), + "codex": lambda: (project_root / ".codex").exists() + or (project_root / "AGENTS.md").exists(), + "gemini": lambda: (project_root / ".gemini").exists() + or (project_root / "GEMINI.md").exists(), + } + + for agent, check in heuristics.items(): + try: + if check(): + detected.append(agent) + except Exception: + pass + + return detected + + # ─── Register all groups ───────────────────────────────────────────────────── diff --git a/codeloom/cli/main.py b/codeloom/cli/main.py index c8b1168..c28bea4 100644 --- a/codeloom/cli/main.py +++ b/codeloom/cli/main.py @@ -37,6 +37,7 @@ def cli(ctx): @cli.command() +@click.argument("platform", required=False) @click.option( "--scope", type=click.Choice(["user", "project"], case_sensitive=False), @@ -45,53 +46,151 @@ def cli(ctx): ) @click.option("--force", is_flag=True, help="Overwrite manual skill edits.") @click.pass_context -def setup(ctx, scope: str | None, force: bool): - """One-step setup for all AI agent integrations. +def setup(ctx, platform: str | None, scope: str | None, force: bool): + """Unified setup for AI agent integrations. - Detects installed editors (Claude Code, Cursor, Windsurf, etc.) - and configures MCP, skills, and context files automatically. + With no arguments, it intelligently detects present agents and + configures them. Use 'codeloom setup ' to pinpoint a specific + agent (e.g., claude, cursor, aider). """ - from ._helpers import human_choose, human_done, human_header + from ._helpers import ( + human_choose, + human_done, + human_fail, + human_header, + human_ok, + ) from .integrations import ( aider_install, claude_install, cline_install, codex_install, cursor_install, + detect_agents, gemini_install, opencode_install, windsurf_install, ) - if scope is None: - scope = human_choose( - "Default installation scope?", - ["user", "project"], - descriptions=[ - "Global (~/.config/) — works for all projects", - "Local (./.claude/ etc.) — this project only", - ], - default=1, - ) + agent_registry = { + "claude": claude_install, + "opencode": opencode_install, + "aider": aider_install, + "cursor": cursor_install, + "windsurf": windsurf_install, + "cline": cline_install, + "codex": codex_install, + "gemini": gemini_install, + } + + if platform: + if platform.lower() not in agent_registry: + valid = ", ".join(agent_registry.keys()) + human_fail(f"Unknown agent: {platform}. Valid: {valid}") + return + to_install = [platform.lower()] + else: + # Intelligent detection + human_header("Scanning for AI agents...") + detected = detect_agents() + if detected: + msg = f"Detected agents: {', '.join(detected)}" + human_ok(msg) + confirm_msg = "Proceed with setup for these agents?" + if click.confirm(confirm_msg, default=True): + to_install = detected + else: + return + else: + human_warn("No agents detected automatically.") + to_install = human_choose( + "Pick agents to configure manually:", + list(agent_registry.keys()), + multiple=True, + ) + + if not to_install: + human_done("Nothing to install.") + return human_header("codeloom: Automated Agent Setup") - # We run all installers. They are designed to be safe - # (skip if not applicable) though some currently prompt or assume paths. - # In a real setup we might check if the editor is actually installed first. + for agent in to_install: + installer = agent_registry[agent] + # Use ctx.invoke to prevent sys.argv parsing issues + if agent in ("claude", "opencode"): + ctx.invoke(installer, scope=scope, force=force) + else: + ctx.invoke(installer) + + human_done("Setup complete! Agents are now codeloom-aware.") - ctx.invoke(claude_install, scope=scope, force=force) - ctx.invoke(opencode_install, scope=scope, force=force) - # Project-level only integrations - ctx.invoke(aider_install) - ctx.invoke(cline_install) - ctx.invoke(codex_install) - ctx.invoke(cursor_install) - ctx.invoke(gemini_install) - ctx.invoke(windsurf_install) +@cli.command() +@click.argument("platform", required=False) +@click.pass_context +def uninstall(ctx, platform: str | None): + """Unified removal of AI agent integrations. + + With no arguments, it scans for codeloom footprints and removes + them. Use 'codeloom uninstall ' to remove a specific agent. + """ + from ._helpers import human_done, human_fail, human_header, human_ok + from .integrations import ( + aider_uninstall, + claude_uninstall, + cline_uninstall, + codex_uninstall, + cursor_uninstall, + detect_agents, + gemini_uninstall, + opencode_uninstall, + windsurf_uninstall, + ) + + agent_registry = { + "claude": claude_uninstall, + "opencode": opencode_uninstall, + "aider": aider_uninstall, + "cursor": cursor_uninstall, + "windsurf": windsurf_uninstall, + "cline": cline_uninstall, + "codex": codex_uninstall, + "gemini": gemini_uninstall, + } + + if platform: + if platform.lower() not in agent_registry: + human_fail(f"Unknown agent: {platform}") + return + to_remove = [platform.lower()] + else: + # Footprint scan (reuse detection heuristics) + human_header("Scanning for codeloom footprints...") + detected = detect_agents() + if detected: + msg = f"Found codeloom in: {', '.join(detected)}" + human_ok(msg) + if click.confirm("Remove these integrations?", default=True): + to_remove = detected + else: + return + else: + human_done("No codeloom integrations found.") + return + + human_header("codeloom: Automated Agent Removal") + + for agent in to_remove: + uninstaller = agent_registry[agent] + if agent in ("claude", "opencode"): + # Uninstall scope default is usually 'all' or 'project' + scope = "all" if agent == "claude" else "project" + ctx.invoke(uninstaller, scope=scope) + else: + ctx.invoke(uninstaller) - human_done("Setup complete! All detected agents are now codeloom-aware.") + human_done("Removal complete! codeloom integrations cleaned up.") diff --git a/tests/test_intelligent_setup.py b/tests/test_intelligent_setup.py new file mode 100644 index 0000000..0f5c3d3 --- /dev/null +++ b/tests/test_intelligent_setup.py @@ -0,0 +1,59 @@ +import json +from pathlib import Path +from unittest.mock import patch +from click.testing import CliRunner +from codeloom.cli.main import cli + +def test_intelligent_detection_cursor(tmp_path): + """Verify that setup only detects agents whose footprint is present.""" + runner = CliRunner() + with runner.isolated_filesystem(temp_dir=tmp_path): + # 1. Mock only Cursor presence + Path(".cursor").mkdir() + + # We need to mock shutil.which and detect_agents to ensure isolation + with patch("shutil.which", return_value=None): + result = runner.invoke(cli, ["setup"], input="n\n") + + # Use lowercase for case-insensitive check + output = result.output.lower() + assert "detected agents:" in output + assert "cursor" in output + # Claude/Aider should not be detected if mocked correctly + assert "claude" not in output + assert "aider" not in output + +def test_unified_uninstall_footprint_scan(tmp_path): + """Verify that 'uninstall' (no args) detects and removes footprints.""" + runner = CliRunner() + with runner.isolated_filesystem(temp_dir=tmp_path): + # 1. Setup multiple + runner.invoke(cli, ["setup", "claude"], input="project\n") + runner.invoke(cli, ["setup", "cursor"]) + + # 2. Run uninstall, confirm all + result = runner.invoke(cli, ["uninstall"], input="y\n") + + output = result.output.lower() + assert "found codeloom in:" in output + assert "claude" in output + assert "cursor" in output + assert "removal complete!" in output + + # Footprints should be gone (or files cleaned) + assert not Path("CLAUDE.md").exists() + assert not Path(".cursor/rules/codeloom.mdc").exists() + +def test_setup_unexpected_argument_fix(tmp_path): + """Verify that setup doesn't fail with 'unexpected extra argument' anymore.""" + runner = CliRunner() + with runner.isolated_filesystem(temp_dir=tmp_path): + # Mock detect_agents to return empty list to trigger manual selection + with patch("codeloom.cli.integrations.detect_agents", return_value=[]): + # Pick 'aider' from the list + result = runner.invoke(cli, ["setup"], input="project\naider\n") + + assert result.exit_code == 0 + assert "Error: Got unexpected extra argument" not in result.output + assert "Setup complete!" in result.output + From b907598c1c9170af91f53c72a6752f09a6065b49 Mon Sep 17 00:00:00 2001 From: Vlad Shurupov Date: Wed, 27 May 2026 22:36:08 +1000 Subject: [PATCH 5/7] Fixed OpenCode AGENTS.md sync and removed redundant Cline logic --- codeloom/cli/integrations.py | 8 ++++ tests/test_intelligent_setup.py | 65 +++++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+) diff --git a/codeloom/cli/integrations.py b/codeloom/cli/integrations.py index 3c3ac10..cfd3e57 100644 --- a/codeloom/cli/integrations.py +++ b/codeloom/cli/integrations.py @@ -727,6 +727,7 @@ def cline_uninstall(): # ─── Aider CLI ─────────────────────────────────────────────────────────────── + @click.group(name="aider") def aider_group(): """Manage per-project Aider CLI integration.""" @@ -841,6 +842,10 @@ def opencode_install(scope: str | None, force: bool = False): skill_dest = skill_dir / "SKILL.md" sync_skill_file(skill_source, skill_dest, force=force) + # Manage project context file + agents_md = project_root / "AGENTS.md" + sync_codeloom_context(agents_md) + if scope == "user": mcp_config_path = Path.home() / ".config" / "opencode" / "config.json" else: @@ -884,6 +889,9 @@ def opencode_uninstall(scope: str): else: human_skip(f"{skill_dir}/ not found") + agents_md = project_root / "AGENTS.md" + uninstall_codeloom_context(agents_md) + parent = skill_dir.parent if parent.exists() and not any(parent.iterdir()): parent.rmdir() diff --git a/tests/test_intelligent_setup.py b/tests/test_intelligent_setup.py index 0f5c3d3..c1340d4 100644 --- a/tests/test_intelligent_setup.py +++ b/tests/test_intelligent_setup.py @@ -57,3 +57,68 @@ def test_setup_unexpected_argument_fix(tmp_path): assert "Error: Got unexpected extra argument" not in result.output assert "Setup complete!" in result.output +def test_opencode_agents_md_sync(tmp_path): + """Verify that opencode install creates and cleans AGENTS.md.""" + runner = CliRunner() + with runner.isolated_filesystem(temp_dir=tmp_path): + # 1. Install + runner.invoke(cli, ["opencode", "install", "--scope", "project"]) + assert Path("AGENTS.md").exists() + assert "codeloom" in Path("AGENTS.md").read_text() + + # 2. Uninstall + runner.invoke(cli, ["opencode", "uninstall", "--scope", "project"]) + # Should be deleted if it only had codeloom content + assert not Path("AGENTS.md").exists() + +def test_surgical_uninstall_integrity(tmp_path): + """Verify that uninstall only removes codeloom block, leaving notes.""" + runner = CliRunner() + with runner.isolated_filesystem(temp_dir=tmp_path): + notes = "# Project Notes\nCustom content." + Path("CLAUDE.md").write_text(notes) + + # 1. Setup + runner.invoke(cli, ["claude", "install", "--scope", "project"]) + + # 2. Uninstall + runner.invoke(cli, ["claude", "uninstall", "--scope", "project"]) + + # 3. Verify notes are back to original + assert Path("CLAUDE.md").read_text().strip() == notes.strip() + +def test_command_group_uniqueness(): + """Programmatically verify that command groups are not duplicated.""" + # This checks for the NameError/duplication issue + from codeloom.cli.main import cli + commands = list(cli.commands.keys()) + # The registration happens at module load time, + # so we check the actual click object + assert "cline" in commands + assert "aider" in commands + assert "opencode" in commands + + # Check if groups have correct subcommands + cline_group = cli.commands["cline"] + assert "install" in cline_group.commands + assert "uninstall" in cline_group.commands + +def test_json_merge_safety(tmp_path): + """Verify JSON configuration remains valid and uncorrupted.""" + runner = CliRunner() + with runner.isolated_filesystem(temp_dir=tmp_path): + settings_file = Path(".claude/settings.json") + settings_file.parent.mkdir() + + # 1. Create invalid JSON + settings_file.write_text("{ invalid json }") + result = runner.invoke(cli, ["claude", "install", "--scope", "project"]) + assert "Could not parse" in result.output + + # 2. Valid JSON with existing content + settings_file.write_text('{"existing": true}') + runner.invoke(cli, ["claude", "install", "--scope", "project"]) + + data = json.loads(settings_file.read_text()) + assert data["existing"] is True + assert "hooks" in data From c48874f79d6babc952372f9a1a5a32f5913a458d Mon Sep 17 00:00:00 2001 From: Vlad Shurupov Date: Thu, 28 May 2026 11:04:36 +1000 Subject: [PATCH 6/7] Renamed project to codeloom in lockfile and gitignore --- .gitignore | 7 +- uv.lock | 433 +---------------------------------------------------- 2 files changed, 5 insertions(+), 435 deletions(-) diff --git a/.gitignore b/.gitignore index b0e6894..64f5350 100644 --- a/.gitignore +++ b/.gitignore @@ -23,9 +23,8 @@ env/ .DS_Store Thumbs.db -# hedwig-cg databases (generated) -.hedwig-cg/ -.hedwig-cb/ +# codeloom databases (generated) +.codeloom/ # Claude Code .claude/ @@ -41,7 +40,7 @@ htmlcov/ # Internal review feedback (not for public repo) feedbacks/ -# Generated integration files (from hedwig-cg install commands) +# Generated integration files (from codeloom install commands) .cursor/ .windsurf/ .codex/ diff --git a/uv.lock b/uv.lock index ab7a3ac..826723e 100644 --- a/uv.lock +++ b/uv.lock @@ -477,438 +477,9 @@ wheels = [ ] [[package]] -name = "hedwig-cg" -version = "0.14.0" -source = { editable = "." } -dependencies = [ - { name = "click" }, - { name = "faiss-cpu" }, - { name = "igraph" }, - { name = "leidenalg" }, - { name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "networkx", version = "3.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "pathspec" }, - { name = "python-hcl2" }, - { name = "pyyaml" }, - { name = "rich" }, - { name = "sentence-transformers" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, - { name = "tree-sitter" }, - { name = "tree-sitter-c" }, - { name = "tree-sitter-c-sharp" }, - { name = "tree-sitter-cpp" }, - { name = "tree-sitter-elixir" }, - { name = "tree-sitter-go" }, - { name = "tree-sitter-java" }, - { name = "tree-sitter-javascript" }, - { name = "tree-sitter-kotlin" }, - { name = "tree-sitter-lua" }, - { name = "tree-sitter-objc" }, - { name = "tree-sitter-php" }, - { name = "tree-sitter-python" }, - { name = "tree-sitter-ruby" }, - { name = "tree-sitter-rust" }, - { name = "tree-sitter-scala" }, - { name = "tree-sitter-swift" }, - { name = "tree-sitter-typescript" }, -] - -[package.optional-dependencies] -dev = [ - { name = "mcp" }, - { name = "pytest" }, - { name = "pytest-cov" }, - { name = "ruff" }, -] -docs = [ - { name = "pymupdf" }, -] -mcp = [ - { name = "mcp" }, -] - -[package.metadata] -requires-dist = [ - { name = "click", specifier = ">=8.1" }, - { name = "faiss-cpu", specifier = ">=1.7.4" }, - { name = "igraph", specifier = ">=0.11" }, - { name = "leidenalg", specifier = ">=0.10" }, - { name = "mcp", marker = "extra == 'dev'", specifier = ">=1.0" }, - { name = "mcp", marker = "extra == 'mcp'", specifier = ">=1.0" }, - { name = "networkx", specifier = ">=3.2" }, - { name = "numpy", specifier = ">=1.26" }, - { name = "pathspec", specifier = ">=0.12" }, - { name = "pymupdf", marker = "extra == 'docs'", specifier = ">=1.24" }, - { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.0" }, - { name = "pytest-cov", marker = "extra == 'dev'" }, - { name = "python-hcl2", specifier = ">=4.3" }, - { name = "pyyaml", specifier = ">=6.0" }, - { name = "rich", specifier = ">=13.0" }, - { name = "ruff", marker = "extra == 'dev'" }, - { name = "sentence-transformers", specifier = ">=3.0" }, - { name = "tomli", marker = "python_full_version < '3.11'", specifier = ">=2.0" }, - { name = "tree-sitter", specifier = ">=0.24" }, - { name = "tree-sitter-c", specifier = ">=0.23" }, - { name = "tree-sitter-c-sharp", specifier = ">=0.23" }, - { name = "tree-sitter-cpp", specifier = ">=0.23" }, - { name = "tree-sitter-elixir", specifier = ">=0.3" }, - { name = "tree-sitter-go", specifier = ">=0.23" }, - { name = "tree-sitter-java", specifier = ">=0.23" }, - { name = "tree-sitter-javascript", specifier = ">=0.23" }, - { name = "tree-sitter-kotlin", specifier = ">=1.0" }, - { name = "tree-sitter-lua", specifier = ">=0.5" }, - { name = "tree-sitter-objc", specifier = ">=3.0" }, - { name = "tree-sitter-php", specifier = ">=0.23" }, - { name = "tree-sitter-python", specifier = ">=0.23" }, - { name = "tree-sitter-ruby", specifier = ">=0.23" }, - { name = "tree-sitter-rust", specifier = ">=0.23" }, - { name = "tree-sitter-scala", specifier = ">=0.23" }, - { name = "tree-sitter-swift", specifier = ">=0.0.1" }, - { name = "tree-sitter-typescript", specifier = ">=0.23" }, -] -provides-extras = ["dev", "docs", "mcp"] - -[[package]] -name = "hf-xet" -version = "1.4.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/53/92/ec9ad04d0b5728dca387a45af7bc98fbb0d73b2118759f5f6038b61a57e8/hf_xet-1.4.3.tar.gz", hash = "sha256:8ddedb73c8c08928c793df2f3401ec26f95be7f7e516a7bee2fbb546f6676113", size = 670477, upload_time = "2026-03-31T22:40:07.874Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/72/43/724d307b34e353da0abd476e02f72f735cdd2bc86082dee1b32ea0bfee1d/hf_xet-1.4.3-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:7551659ba4f1e1074e9623996f28c3873682530aee0a846b7f2f066239228144", size = 3800935, upload_time = "2026-03-31T22:39:49.618Z" }, - { url = "https://files.pythonhosted.org/packages/2b/d2/8bee5996b699262edb87dbb54118d287c0e1b2fc78af7cdc41857ba5e3c4/hf_xet-1.4.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:bee693ada985e7045997f05f081d0e12c4c08bd7626dc397f8a7c487e6c04f7f", size = 3558942, upload_time = "2026-03-31T22:39:47.938Z" }, - { url = "https://files.pythonhosted.org/packages/c3/a1/e993d09cbe251196fb60812b09a58901c468127b7259d2bf0f68bf6088eb/hf_xet-1.4.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:21644b404bb0100fe3857892f752c4d09642586fd988e61501c95bbf44b393a3", size = 4207657, upload_time = "2026-03-31T22:39:39.69Z" }, - { url = "https://files.pythonhosted.org/packages/64/44/9eb6d21e5c34c63e5e399803a6932fa983cabdf47c0ecbcfe7ea97684b8c/hf_xet-1.4.3-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:987f09cfe418237812896a6736b81b1af02a3a6dcb4b4944425c4c4fca7a7cf8", size = 3986765, upload_time = "2026-03-31T22:39:37.936Z" }, - { url = "https://files.pythonhosted.org/packages/ea/7b/8ad6f16fdb82f5f7284a34b5ec48645bd575bdcd2f6f0d1644775909c486/hf_xet-1.4.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:60cf7fc43a99da0a853345cf86d23738c03983ee5249613a6305d3e57a5dca74", size = 4188162, upload_time = "2026-03-31T22:39:58.382Z" }, - { url = "https://files.pythonhosted.org/packages/1b/c4/39d6e136cbeea9ca5a23aad4b33024319222adbdc059ebcda5fc7d9d5ff4/hf_xet-1.4.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2815a49a7a59f3e2edf0cf113ae88e8cb2ca2a221bf353fb60c609584f4884d4", size = 4424525, upload_time = "2026-03-31T22:40:00.225Z" }, - { url = "https://files.pythonhosted.org/packages/46/f2/adc32dae6bdbc367853118b9878139ac869419a4ae7ba07185dc31251b76/hf_xet-1.4.3-cp313-cp313t-win_amd64.whl", hash = "sha256:42ee323265f1e6a81b0e11094564fb7f7e0ec75b5105ffd91ae63f403a11931b", size = 3671610, upload_time = "2026-03-31T22:40:10.42Z" }, - { url = "https://files.pythonhosted.org/packages/e2/19/25d897dcc3f81953e0c2cde9ec186c7a0fee413eb0c9a7a9130d87d94d3a/hf_xet-1.4.3-cp313-cp313t-win_arm64.whl", hash = "sha256:27c976ba60079fb8217f485b9c5c7fcd21c90b0367753805f87cb9f3cdc4418a", size = 3528529, upload_time = "2026-03-31T22:40:09.106Z" }, - { url = "https://files.pythonhosted.org/packages/ec/36/3e8f85ca9fe09b8de2b2e10c63b3b3353d7dda88a0b3d426dffbe7b8313b/hf_xet-1.4.3-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:5251d5ece3a81815bae9abab41cf7ddb7bcb8f56411bce0827f4a3071c92fdc6", size = 3801019, upload_time = "2026-03-31T22:39:56.651Z" }, - { url = "https://files.pythonhosted.org/packages/b5/9c/defb6cb1de28bccb7bd8d95f6e60f72a3d3fa4cb3d0329c26fb9a488bfe7/hf_xet-1.4.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1feb0f3abeacee143367c326a128a2e2b60868ec12a36c225afb1d6c5a05e6d2", size = 3558746, upload_time = "2026-03-31T22:39:54.766Z" }, - { url = "https://files.pythonhosted.org/packages/c1/bd/8d001191893178ff8e826e46ad5299446e62b93cd164e17b0ffea08832ec/hf_xet-1.4.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8b301fc150290ca90b4fccd079829b84bb4786747584ae08b94b4577d82fb791", size = 4207692, upload_time = "2026-03-31T22:39:46.246Z" }, - { url = "https://files.pythonhosted.org/packages/ce/48/6790b402803250e9936435613d3a78b9aaeee7973439f0918848dde58309/hf_xet-1.4.3-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:d972fbe95ddc0d3c0fc49b31a8a69f47db35c1e3699bf316421705741aab6653", size = 3986281, upload_time = "2026-03-31T22:39:44.648Z" }, - { url = "https://files.pythonhosted.org/packages/51/56/ea62552fe53db652a9099eda600b032d75554d0e86c12a73824bfedef88b/hf_xet-1.4.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c5b48db1ee344a805a1b9bd2cda9b6b65fe77ed3787bd6e87ad5521141d317cd", size = 4187414, upload_time = "2026-03-31T22:40:04.951Z" }, - { url = "https://files.pythonhosted.org/packages/7d/f5/bc1456d4638061bea997e6d2db60a1a613d7b200e0755965ec312dc1ef79/hf_xet-1.4.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:22bdc1f5fb8b15bf2831440b91d1c9bbceeb7e10c81a12e8d75889996a5c9da8", size = 4424368, upload_time = "2026-03-31T22:40:06.347Z" }, - { url = "https://files.pythonhosted.org/packages/e4/76/ab597bae87e1f06d18d3ecb8ed7f0d3c9a37037fc32ce76233d369273c64/hf_xet-1.4.3-cp314-cp314t-win_amd64.whl", hash = "sha256:0392c79b7cf48418cd61478c1a925246cf10639f4cd9d94368d8ca1e8df9ea07", size = 3672280, upload_time = "2026-03-31T22:40:16.401Z" }, - { url = "https://files.pythonhosted.org/packages/62/05/2e462d34e23a09a74d73785dbed71cc5dbad82a72eee2ad60a72a554155d/hf_xet-1.4.3-cp314-cp314t-win_arm64.whl", hash = "sha256:681c92a07796325778a79d76c67011764ecc9042a8c3579332b61b63ae512075", size = 3528945, upload_time = "2026-03-31T22:40:14.995Z" }, - { url = "https://files.pythonhosted.org/packages/ac/9f/9c23e4a447b8f83120798f9279d0297a4d1360bdbf59ef49ebec78fe2545/hf_xet-1.4.3-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:d0da85329eaf196e03e90b84c2d0aca53bd4573d097a75f99609e80775f98025", size = 3805048, upload_time = "2026-03-31T22:39:53.105Z" }, - { url = "https://files.pythonhosted.org/packages/0b/f8/7aacb8e5f4a7899d39c787b5984e912e6c18b11be136ef13947d7a66d265/hf_xet-1.4.3-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:e23717ce4186b265f69afa66e6f0069fe7efbf331546f5c313d00e123dc84583", size = 3562178, upload_time = "2026-03-31T22:39:51.295Z" }, - { url = "https://files.pythonhosted.org/packages/df/9a/a24b26dc8a65f0ecc0fe5be981a19e61e7ca963b85e062c083f3a9100529/hf_xet-1.4.3-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc360b70c815bf340ed56c7b8c63aacf11762a4b099b2fe2c9bd6d6068668c08", size = 4212320, upload_time = "2026-03-31T22:39:42.922Z" }, - { url = "https://files.pythonhosted.org/packages/53/60/46d493db155d2ee2801b71fb1b0fd67696359047fdd8caee2c914cc50c79/hf_xet-1.4.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:39f2d2e9654cd9b4319885733993807aab6de9dfbd34c42f0b78338d6617421f", size = 3991546, upload_time = "2026-03-31T22:39:41.335Z" }, - { url = "https://files.pythonhosted.org/packages/bc/f5/067363e1c96c6b17256910830d1b54099d06287e10f4ec6ec4e7e08371fc/hf_xet-1.4.3-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:49ad8a8cead2b56051aa84d7fce3e1335efe68df3cf6c058f22a65513885baac", size = 4193200, upload_time = "2026-03-31T22:40:01.936Z" }, - { url = "https://files.pythonhosted.org/packages/42/4b/53951592882d9c23080c7644542fda34a3813104e9e11fa1a7d82d419cb8/hf_xet-1.4.3-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7716d62015477a70ea272d2d68cd7cad140f61c52ee452e133e139abfe2c17ba", size = 4429392, upload_time = "2026-03-31T22:40:03.492Z" }, - { url = "https://files.pythonhosted.org/packages/8a/21/75a6c175b4e79662ad8e62f46a40ce341d8d6b206b06b4320d07d55b188c/hf_xet-1.4.3-cp37-abi3-win_amd64.whl", hash = "sha256:6b591fcad34e272a5b02607485e4f2a1334aebf1bc6d16ce8eb1eb8978ac2021", size = 3677359, upload_time = "2026-03-31T22:40:13.619Z" }, - { url = "https://files.pythonhosted.org/packages/8a/7c/44314ecd0e89f8b2b51c9d9e5e7a60a9c1c82024ac471d415860557d3cd8/hf_xet-1.4.3-cp37-abi3-win_arm64.whl", hash = "sha256:7c2c7e20bcfcc946dc67187c203463f5e932e395845d098cc2a93f5b67ca0b47", size = 3533664, upload_time = "2026-03-31T22:40:12.152Z" }, -] - -[[package]] -name = "httpcore" -version = "1.0.9" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "certifi" }, - { name = "h11" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload_time = "2025-04-24T22:06:22.219Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload_time = "2025-04-24T22:06:20.566Z" }, -] - -[[package]] -name = "httpx" -version = "0.28.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, - { name = "certifi" }, - { name = "httpcore" }, - { name = "idna" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload_time = "2024-12-06T15:37:23.222Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload_time = "2024-12-06T15:37:21.509Z" }, -] - -[[package]] -name = "httpx-sse" -version = "0.4.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0f/4c/751061ffa58615a32c31b2d82e8482be8dd4a89154f003147acee90f2be9/httpx_sse-0.4.3.tar.gz", hash = "sha256:9b1ed0127459a66014aec3c56bebd93da3c1bc8bb6618c8082039a44889a755d", size = 15943, upload_time = "2025-10-10T21:48:22.271Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/fd/6668e5aec43ab844de6fc74927e155a3b37bf40d7c3790e49fc0406b6578/httpx_sse-0.4.3-py3-none-any.whl", hash = "sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc", size = 8960, upload_time = "2025-10-10T21:48:21.158Z" }, -] - -[[package]] -name = "huggingface-hub" -version = "1.10.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "filelock" }, - { name = "fsspec" }, - { name = "hf-xet", marker = "platform_machine == 'AMD64' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'" }, - { name = "httpx" }, - { name = "packaging" }, - { name = "pyyaml" }, - { name = "tqdm" }, - { name = "typer" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/0c/4d/00734890c7fcfe2c7ff04f1c1a167186c42b19e370a2dd8cfd8c34fc92c4/huggingface_hub-1.10.2.tar.gz", hash = "sha256:4b276f820483b709dc86a53bcb8183ea496b8d8447c9f7f88a115a12b498a95f", size = 758428, upload_time = "2026-04-14T10:42:28.498Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5e/c9/4c1e1216b24bcab140c83acdf8bc89a846ea17cd8a06cd18e3fd308a297f/huggingface_hub-1.10.2-py3-none-any.whl", hash = "sha256:c26c908767cc711493978dc0b4f5747ba7841602997cc98bfd628450a28cf9bc", size = 642581, upload_time = "2026-04-14T10:42:26.563Z" }, -] - -[[package]] -name = "idna" -version = "3.11" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload_time = "2025-10-12T14:55:20.501Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload_time = "2025-10-12T14:55:18.883Z" }, -] - -[[package]] -name = "igraph" -version = "1.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "texttable" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/23/be/56bef1919005b4caf1f71522b300d359f7faeb7ae93a3b0baa9b4f146a87/igraph-1.0.0.tar.gz", hash = "sha256:2414d0be2e4d77ee5357807d100974b40f6082bb1bb71988ec46cfb6728651ee", size = 5077105, upload_time = "2025-10-23T12:22:50.127Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a5/03/3278ad0ceb3ea0e84d8ae3a85bdded4d0e57853aeb802a200feb43847b93/igraph-1.0.0-cp39-abi3-macosx_10_15_x86_64.whl", hash = "sha256:c2cbc415e02523e5a241eecee82319080bf928a70b1ba299f3b3e25bf029b6d4", size = 2257415, upload_time = "2025-10-23T12:22:27.246Z" }, - { url = "https://files.pythonhosted.org/packages/0d/bc/6281ec7f9baaf71ee57c3b1748da2d3148d15d253e1a03006f204aa68ca5/igraph-1.0.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:1a27753cd80680a8f676c2d5a467aaa4a95e510b30748398ec4e4aeb982130e8", size = 2048555, upload_time = "2025-10-23T12:22:29.49Z" }, - { url = "https://files.pythonhosted.org/packages/2a/38/3cd6428a4ed4c09a56df05998438e7774fd1d799ee4fb8fc481674f5f7fc/igraph-1.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:a55dc3a2a4e3fc3eba42479910c1511bfc3ecb33cdf5f0406891fd85f14b5aee", size = 5314141, upload_time = "2025-10-23T12:22:31.023Z" }, - { url = "https://files.pythonhosted.org/packages/7d/da/dd2867c25adbb41563720f14b5fc895c98bf88be682a3faff4f7b3118d2a/igraph-1.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2d04c2c76f686fb1f554ee35dfd3085f5e73b7965ba6b4cf06d53e66b1955522", size = 5683134, upload_time = "2025-10-23T12:22:32.423Z" }, - { url = "https://files.pythonhosted.org/packages/e5/40/243c118d34ab80382d7009c4dcb99b887384c3d2ce84d29eeac19e2a007a/igraph-1.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:f2b52dc1757fff0fed29a9f7a276d971a11db4211569ed78b9eab36288dfcc9d", size = 6211583, upload_time = "2025-10-23T12:22:34.238Z" }, - { url = "https://files.pythonhosted.org/packages/1d/b7/88f433819c54b496cb0315fce28e658970cb20ff5dbd52a5a605ce2888de/igraph-1.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:05c79a2a8fca695b2f217a6fa7f2549f896f757d4db41be32a055400cb19cc30", size = 6594509, upload_time = "2025-10-23T12:22:35.831Z" }, - { url = "https://files.pythonhosted.org/packages/7b/5d/8f7f6f619d374e959aa3664ebc4b24c10abc90c2e8efbed97f2623fadaf5/igraph-1.0.0-cp39-abi3-win32.whl", hash = "sha256:c2bce3cd472fec3dd9c4d8a3ea5b6b9be65fb30edf760beb4850760dd4f2d479", size = 2725406, upload_time = "2025-10-23T12:22:37.588Z" }, - { url = "https://files.pythonhosted.org/packages/af/77/a85b3745cf40a0572bae2de8cd9c2a2a8af78e5cf3e880fc0a249114e609/igraph-1.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:faeff8ede0cf15eb4ded44b0fcea6e1886740146e60504c24ad2da14e0939563", size = 3221663, upload_time = "2025-10-23T12:22:39.404Z" }, - { url = "https://files.pythonhosted.org/packages/ef/7e/5df541c37bdf6493035e89c22bd53f30d99b291bcda6c78e9a8afeecec2b/igraph-1.0.0-cp39-abi3-win_arm64.whl", hash = "sha256:b607cafc24b10a615e713ee96e58208ef27e0764af80140c7cc45d4724a3f2df", size = 2785701, upload_time = "2025-10-23T12:22:41.03Z" }, - { url = "https://files.pythonhosted.org/packages/b9/73/bf1d4dbbc9123435b3ca14bb608b243a50a4f158ecea564bf196715248d9/igraph-1.0.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:3189c1a8e8a8f58009f3f729040eb3701254d074ed37245691d529869ec940c5", size = 2246636, upload_time = "2025-10-23T12:22:42.314Z" }, - { url = "https://files.pythonhosted.org/packages/59/ac/28482f2af45cc0a0ca88a69d17a6ea694f58bdbd22cc876e7273a0379282/igraph-1.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ebe9502689b946301584b3cfacdbc70c58c4d664d804e39b6daa31be5c20bf46", size = 2036101, upload_time = "2025-10-23T12:22:43.957Z" }, - { url = "https://files.pythonhosted.org/packages/56/80/806a093df1d1ddc3b30d0418b1ee56388ae7018f8ae288677ee2b3a1abaf/igraph-1.0.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:f117683108c54330d6dc67a708e3724c13c9989885122a29781296872989a222", size = 3053403, upload_time = "2025-10-23T12:22:45.573Z" }, - { url = "https://files.pythonhosted.org/packages/56/bf/cf7aeff230a4368c0b8bc6b02f3ea27db41db33714b51e1e8a7c1458f31b/igraph-1.0.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:077dbff0edb8b4ce0f9fefdf325200346d9d5db02de31872b41743de08e67a16", size = 3262472, upload_time = "2025-10-23T12:22:47.248Z" }, - { url = "https://files.pythonhosted.org/packages/d8/ca/dbc06072d5eea402a6dc81f387afb1b7e0c415f1d8a75232943fc4d1bfdb/igraph-1.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:fe7c693b2a84a4e03ca31e65aa05a2ecd8728137fa9909ccbf6453b4200b856d", size = 3218861, upload_time = "2025-10-23T12:22:48.46Z" }, -] - -[[package]] -name = "iniconfig" -version = "2.3.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload_time = "2025-10-18T21:55:43.219Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload_time = "2025-10-18T21:55:41.639Z" }, -] - -[[package]] -name = "jinja2" -version = "3.1.6" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "markupsafe" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload_time = "2025-03-05T20:05:02.478Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload_time = "2025-03-05T20:05:00.369Z" }, -] +name = "codeloom" +version = "0.1.3" -[[package]] -name = "joblib" -version = "1.5.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/41/f2/d34e8b3a08a9cc79a50b2208a93dce981fe615b64d5a4d4abee421d898df/joblib-1.5.3.tar.gz", hash = "sha256:8561a3269e6801106863fd0d6d84bb737be9e7631e33aaed3fb9ce5953688da3", size = 331603, upload_time = "2025-12-15T08:41:46.427Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7b/91/984aca2ec129e2757d1e4e3c81c3fcda9d0f85b74670a094cc443d9ee949/joblib-1.5.3-py3-none-any.whl", hash = "sha256:5fc3c5039fc5ca8c0276333a188bbd59d6b7ab37fe6632daa76bc7f9ec18e713", size = 309071, upload_time = "2025-12-15T08:41:44.973Z" }, -] - -[[package]] -name = "jsonschema" -version = "4.26.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "attrs" }, - { name = "jsonschema-specifications" }, - { name = "referencing" }, - { name = "rpds-py" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload_time = "2026-01-07T13:41:07.246Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630, upload_time = "2026-01-07T13:41:05.306Z" }, -] - -[[package]] -name = "jsonschema-specifications" -version = "2025.9.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "referencing" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload_time = "2025-09-08T01:34:59.186Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload_time = "2025-09-08T01:34:57.871Z" }, -] - -[[package]] -name = "lark" -version = "1.3.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/da/34/28fff3ab31ccff1fd4f6c7c7b0ceb2b6968d8ea4950663eadcb5720591a0/lark-1.3.1.tar.gz", hash = "sha256:b426a7a6d6d53189d318f2b6236ab5d6429eaf09259f1ca33eb716eed10d2905", size = 382732, upload_time = "2025-10-27T18:25:56.653Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/82/3d/14ce75ef66813643812f3093ab17e46d3a206942ce7376d31ec2d36229e7/lark-1.3.1-py3-none-any.whl", hash = "sha256:c629b661023a014c37da873b4ff58a817398d12635d3bbb2c5a03be7fe5d1e12", size = 113151, upload_time = "2025-10-27T18:25:54.882Z" }, -] - -[[package]] -name = "leidenalg" -version = "0.11.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "igraph" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/5d/a5/853e93441aed7f82b0389f86f37e19413e817ba0c54cc790895935256968/leidenalg-0.11.0.tar.gz", hash = "sha256:f454be96bbc8089ea2a90ca853d8d389ab646de964a03bd58417f8b29ff8ef5d", size = 452850, upload_time = "2025-10-31T17:14:48.684Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/28/a9/4ab4e244215db0c8b626e4bed0d3e0fbd191c52d2d5f5cb9d160139ecc7e/leidenalg-0.11.0-cp38-abi3-macosx_10_9_x86_64.whl", hash = "sha256:5607589050bfc1926e657b4d8a3b5341fe1eb81018c22cf4a3d3a39e368d1fcb", size = 2256514, upload_time = "2025-10-31T17:14:27.574Z" }, - { url = "https://files.pythonhosted.org/packages/98/f4/98db342d603671ae0a233f0a624939a47161044a2716cbd62a50440a1132/leidenalg-0.11.0-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:9b5781876b1f1faed72a4f9926ff52de286843556b9d6791fe25a2acb33b7a5c", size = 1926003, upload_time = "2025-10-31T17:14:29.521Z" }, - { url = "https://files.pythonhosted.org/packages/9b/38/fd6ac21af10b12828b472eada4fce0edf2a212581238ad0c8d1afebc6f98/leidenalg-0.11.0-cp38-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a80f49477f8e793f27d8e08949177f19e3834cd878af50a662b4f87335d06549", size = 2545535, upload_time = "2025-10-31T17:14:31.102Z" }, - { url = "https://files.pythonhosted.org/packages/77/87/b087584750a788535b4a8d56ddeb82a175d32b472aa5338a4e2cc593a42c/leidenalg-0.11.0-cp38-abi3-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:2143be3e80485584ccbdf927323fce65345da17facd0f8b438f11015f5dc6c27", size = 2845029, upload_time = "2025-10-31T17:14:32.815Z" }, - { url = "https://files.pythonhosted.org/packages/b0/a4/a89e2ce16a580f7bea066ed49364f0b3e04a6412f0c3692975bee8515141/leidenalg-0.11.0-cp38-abi3-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:571a0934f831a69442d82889d319bdba93de924bd9e09b720cd8cbe6fdc08c17", size = 2738084, upload_time = "2025-10-31T17:14:35.246Z" }, - { url = "https://files.pythonhosted.org/packages/e8/fe/8923cac6cd7c9e0ac5f38aaa69a4744c93d025575763d05f7a3baae8020d/leidenalg-0.11.0-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:aec03e7178b19102dd271b453a39b9865cf283b4113151ba60514e5681046294", size = 4070307, upload_time = "2025-10-31T17:14:36.796Z" }, - { url = "https://files.pythonhosted.org/packages/fe/94/beaab5ee9968f9389f705532c31ffb868bad8a5ce68fb699ddde5ddc5409/leidenalg-0.11.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:310b9269a11fd1e960590c1a2b6ff685a2cc42aa3234ce67bc2a623ab61f26a9", size = 3797863, upload_time = "2025-10-31T17:14:38.124Z" }, - { url = "https://files.pythonhosted.org/packages/6e/8e/8caf4ba38fd7d8e6197348b348a4ab666b1b3117225ea2f0934a98a93176/leidenalg-0.11.0-cp38-abi3-win32.whl", hash = "sha256:5ea4cd7ee054540112b28f7e2d64658dcccd59f61a5d6a08a41df808645f96e9", size = 1643351, upload_time = "2025-10-31T17:14:39.385Z" }, - { url = "https://files.pythonhosted.org/packages/47/15/7d459a8e2a43f17c1db129b997b7bb7aa7f000a0967bab87c28b8c5cf448/leidenalg-0.11.0-cp38-abi3-win_amd64.whl", hash = "sha256:5e789c0960008d185413344a402d0587580c441644d4d20bf57c96f25d4d1710", size = 1990321, upload_time = "2025-10-31T17:14:40.892Z" }, - { url = "https://files.pythonhosted.org/packages/b2/01/f6bdfb489ce86cb11f7b5428bb8c3cff751436fff4e12c99f3b5f59d622e/leidenalg-0.11.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:aa9b3bb5e75c5120b57cb8a3ba176610853359a9f5e787ecd6ba7e85c61f4fdc", size = 2254690, upload_time = "2025-10-31T17:14:42.153Z" }, - { url = "https://files.pythonhosted.org/packages/9b/50/ffad5f00e995ea064d1345867f6aa3534ebfb58e361309e088a12ef278d1/leidenalg-0.11.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:bed72262f181774de8425d6951c0e9c1d9c3d9bc64e030993e4215d6ab67933b", size = 1924396, upload_time = "2025-10-31T17:14:43.544Z" }, - { url = "https://files.pythonhosted.org/packages/9d/7c/7683c87e9d0bf70519a0d6cdbb27363b3520a4d5edd9c5f59181e393e2ff/leidenalg-0.11.0-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c1672eb4b24c2c484c0f4d2225267a456751f4624ac99619fed4fb9e4a31049", size = 2379529, upload_time = "2025-10-31T17:14:44.67Z" }, - { url = "https://files.pythonhosted.org/packages/b8/0d/0560646fe75f6d161006cb6f164ef7d25096b9d59d68e836bce931ec8724/leidenalg-0.11.0-pp311-pypy311_pp73-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:3d9c74f9c3d97d1daec5cc9f43b56a62bb3f40d5fef9f2b5defd8d715bfb5cdd", size = 2683317, upload_time = "2025-10-31T17:14:46.03Z" }, - { url = "https://files.pythonhosted.org/packages/79/b9/d84d6225d79fed4fee19faef49e8b8fa58b70fcf55de5061bb1adf602eaa/leidenalg-0.11.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:023e97e4c96573e89fdea876da663e38694f6fa91e2c85e98455101409381d88", size = 2570109, upload_time = "2025-10-31T17:14:47.585Z" }, -] - -[[package]] -name = "markdown-it-py" -version = "4.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "mdurl" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload_time = "2025-08-11T12:57:52.854Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload_time = "2025-08-11T12:57:51.923Z" }, -] - -[[package]] -name = "markupsafe" -version = "3.0.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload_time = "2025-09-27T18:37:40.426Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e8/4b/3541d44f3937ba468b75da9eebcae497dcf67adb65caa16760b0a6807ebb/markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559", size = 11631, upload_time = "2025-09-27T18:36:05.558Z" }, - { url = "https://files.pythonhosted.org/packages/98/1b/fbd8eed11021cabd9226c37342fa6ca4e8a98d8188a8d9b66740494960e4/markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419", size = 12057, upload_time = "2025-09-27T18:36:07.165Z" }, - { url = "https://files.pythonhosted.org/packages/40/01/e560d658dc0bb8ab762670ece35281dec7b6c1b33f5fbc09ebb57a185519/markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695", size = 22050, upload_time = "2025-09-27T18:36:08.005Z" }, - { url = "https://files.pythonhosted.org/packages/af/cd/ce6e848bbf2c32314c9b237839119c5a564a59725b53157c856e90937b7a/markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591", size = 20681, upload_time = "2025-09-27T18:36:08.881Z" }, - { url = "https://files.pythonhosted.org/packages/c9/2a/b5c12c809f1c3045c4d580b035a743d12fcde53cf685dbc44660826308da/markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c", size = 20705, upload_time = "2025-09-27T18:36:10.131Z" }, - { url = "https://files.pythonhosted.org/packages/cf/e3/9427a68c82728d0a88c50f890d0fc072a1484de2f3ac1ad0bfc1a7214fd5/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f", size = 21524, upload_time = "2025-09-27T18:36:11.324Z" }, - { url = "https://files.pythonhosted.org/packages/bc/36/23578f29e9e582a4d0278e009b38081dbe363c5e7165113fad546918a232/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6", size = 20282, upload_time = "2025-09-27T18:36:12.573Z" }, - { url = "https://files.pythonhosted.org/packages/56/21/dca11354e756ebd03e036bd8ad58d6d7168c80ce1fe5e75218e4945cbab7/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1", size = 20745, upload_time = "2025-09-27T18:36:13.504Z" }, - { url = "https://files.pythonhosted.org/packages/87/99/faba9369a7ad6e4d10b6a5fbf71fa2a188fe4a593b15f0963b73859a1bbd/markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa", size = 14571, upload_time = "2025-09-27T18:36:14.779Z" }, - { url = "https://files.pythonhosted.org/packages/d6/25/55dc3ab959917602c96985cb1253efaa4ff42f71194bddeb61eb7278b8be/markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8", size = 15056, upload_time = "2025-09-27T18:36:16.125Z" }, - { url = "https://files.pythonhosted.org/packages/d0/9e/0a02226640c255d1da0b8d12e24ac2aa6734da68bff14c05dd53b94a0fc3/markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1", size = 13932, upload_time = "2025-09-27T18:36:17.311Z" }, - { url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload_time = "2025-09-27T18:36:18.185Z" }, - { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload_time = "2025-09-27T18:36:19.444Z" }, - { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload_time = "2025-09-27T18:36:20.768Z" }, - { url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload_time = "2025-09-27T18:36:22.249Z" }, - { url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload_time = "2025-09-27T18:36:23.535Z" }, - { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload_time = "2025-09-27T18:36:24.823Z" }, - { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload_time = "2025-09-27T18:36:25.95Z" }, - { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload_time = "2025-09-27T18:36:27.109Z" }, - { url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload_time = "2025-09-27T18:36:28.045Z" }, - { url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload_time = "2025-09-27T18:36:29.025Z" }, - { url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload_time = "2025-09-27T18:36:29.954Z" }, - { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload_time = "2025-09-27T18:36:30.854Z" }, - { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload_time = "2025-09-27T18:36:31.971Z" }, - { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload_time = "2025-09-27T18:36:32.813Z" }, - { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload_time = "2025-09-27T18:36:33.86Z" }, - { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload_time = "2025-09-27T18:36:35.099Z" }, - { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload_time = "2025-09-27T18:36:36.001Z" }, - { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload_time = "2025-09-27T18:36:36.906Z" }, - { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload_time = "2025-09-27T18:36:37.868Z" }, - { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload_time = "2025-09-27T18:36:38.761Z" }, - { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload_time = "2025-09-27T18:36:39.701Z" }, - { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload_time = "2025-09-27T18:36:40.689Z" }, - { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload_time = "2025-09-27T18:36:41.777Z" }, - { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload_time = "2025-09-27T18:36:43.257Z" }, - { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload_time = "2025-09-27T18:36:44.508Z" }, - { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload_time = "2025-09-27T18:36:45.385Z" }, - { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload_time = "2025-09-27T18:36:46.916Z" }, - { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload_time = "2025-09-27T18:36:47.884Z" }, - { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload_time = "2025-09-27T18:36:48.82Z" }, - { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload_time = "2025-09-27T18:36:49.797Z" }, - { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload_time = "2025-09-27T18:36:51.584Z" }, - { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload_time = "2025-09-27T18:36:52.537Z" }, - { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload_time = "2025-09-27T18:36:53.513Z" }, - { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload_time = "2025-09-27T18:36:54.819Z" }, - { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload_time = "2025-09-27T18:36:55.714Z" }, - { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload_time = "2025-09-27T18:36:56.908Z" }, - { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload_time = "2025-09-27T18:36:57.913Z" }, - { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload_time = "2025-09-27T18:36:58.833Z" }, - { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload_time = "2025-09-27T18:36:59.739Z" }, - { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload_time = "2025-09-27T18:37:00.719Z" }, - { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload_time = "2025-09-27T18:37:01.673Z" }, - { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload_time = "2025-09-27T18:37:02.639Z" }, - { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload_time = "2025-09-27T18:37:03.582Z" }, - { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload_time = "2025-09-27T18:37:04.929Z" }, - { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload_time = "2025-09-27T18:37:06.342Z" }, - { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload_time = "2025-09-27T18:37:07.213Z" }, - { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload_time = "2025-09-27T18:37:09.572Z" }, - { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload_time = "2025-09-27T18:37:10.58Z" }, - { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload_time = "2025-09-27T18:37:11.547Z" }, - { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload_time = "2025-09-27T18:37:12.48Z" }, - { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload_time = "2025-09-27T18:37:13.485Z" }, - { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload_time = "2025-09-27T18:37:14.408Z" }, - { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload_time = "2025-09-27T18:37:15.36Z" }, - { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload_time = "2025-09-27T18:37:16.496Z" }, - { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload_time = "2025-09-27T18:37:17.476Z" }, - { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload_time = "2025-09-27T18:37:18.453Z" }, - { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload_time = "2025-09-27T18:37:19.332Z" }, - { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload_time = "2025-09-27T18:37:20.245Z" }, - { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload_time = "2025-09-27T18:37:21.177Z" }, - { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload_time = "2025-09-27T18:37:22.167Z" }, - { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload_time = "2025-09-27T18:37:23.296Z" }, - { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload_time = "2025-09-27T18:37:24.237Z" }, - { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload_time = "2025-09-27T18:37:25.271Z" }, - { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload_time = "2025-09-27T18:37:26.285Z" }, - { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload_time = "2025-09-27T18:37:27.316Z" }, - { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload_time = "2025-09-27T18:37:28.327Z" }, -] - -[[package]] -name = "mcp" -version = "1.27.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, - { name = "httpx" }, - { name = "httpx-sse" }, - { name = "jsonschema" }, - { name = "pydantic" }, - { name = "pydantic-settings" }, - { name = "pyjwt", extra = ["crypto"] }, - { name = "python-multipart" }, - { name = "pywin32", marker = "sys_platform == 'win32'" }, - { name = "sse-starlette" }, - { name = "starlette" }, - { name = "typing-extensions" }, - { name = "typing-inspection" }, - { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/8b/eb/c0cfc62075dc6e1ec1c64d352ae09ac051d9334311ed226f1f425312848a/mcp-1.27.0.tar.gz", hash = "sha256:d3dc35a7eec0d458c1da4976a48f982097ddaab87e278c5511d5a4a56e852b83", size = 607509, upload_time = "2026-04-02T14:48:08.88Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9c/46/f6b4ad632c67ef35209a66127e4bddc95759649dd595f71f13fba11bdf9a/mcp-1.27.0-py3-none-any.whl", hash = "sha256:5ce1fa81614958e267b21fb2aa34e0aea8e2c6ede60d52aba45fd47246b4d741", size = 215967, upload_time = "2026-04-02T14:48:07.24Z" }, -] - -[[package]] -name = "mdurl" -version = "0.1.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload_time = "2022-08-14T12:40:10.846Z" } wheels = [ From a0e9a90c8845c6ba63fbaa58a6bb1fe06a645014 Mon Sep 17 00:00:00 2001 From: Vlad Shurupov Date: Thu, 28 May 2026 11:50:32 +1000 Subject: [PATCH 7/7] Updated .gitignore --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 64f5350..e26c544 100644 --- a/.gitignore +++ b/.gitignore @@ -41,6 +41,7 @@ htmlcov/ feedbacks/ # Generated integration files (from codeloom install commands) +.opencode/ .cursor/ .windsurf/ .codex/ @@ -48,6 +49,7 @@ AGENTS.md GEMINI.md CONVENTIONS.md .aider.conf.yml +opencode.json # OMC state .omc/