From 32a877afb0479d6d88d46e17f805e969dd717263 Mon Sep 17 00:00:00 2001 From: "Md. Fatin Shadab Turja" <71595077+FatinShadab@users.noreply.github.com> Date: Mon, 1 Jun 2026 10:02:58 +0600 Subject: [PATCH 1/5] fix(deps): support Python 3.12+ Tree-sitter installs without breaking 3.11 Pinned tree-sitter grammar packages at 0.21.x are not published for Python 3.12+, which blocks pip/pipx installs on macOS Apple Silicon and other 3.12 environments. Keep the existing 0.21.x pins for Python <3.12 and add bounded >=0.23,<0.26 constraints for Python >=3.12 across core and grammar packages in pyproject.toml and requirements.txt. Add _build_language() in the parser to tolerate legacy and modern tree_sitter.Language constructor signatures so upgraded grammars do not fail at runtime. Add parser API regression tests and a compatibility CI matrix (ubuntu + macos-14, Python 3.11/3.12/3.13) with install, parser, and CLI smoke checks. Changelog: document Tree-sitter install fix for 3.12+. --- .github/workflows/compatibility.yml | 41 +++++++++++++++++++++++++++++ CHANGELOG.md | 1 + pyproject.toml | 18 ++++++++----- requirements.txt | 18 ++++++++----- src/codegenome/parser.py | 12 ++++++++- tests/test_parser.py | 32 ++++++++++++++++++++++ 6 files changed, 109 insertions(+), 13 deletions(-) create mode 100644 .github/workflows/compatibility.yml diff --git a/.github/workflows/compatibility.yml b/.github/workflows/compatibility.yml new file mode 100644 index 0000000..f2ca8b0 --- /dev/null +++ b/.github/workflows/compatibility.yml @@ -0,0 +1,41 @@ +name: Compatibility + +on: + push: + branches: ["main"] + pull_request: + +jobs: + install-and-parser-smoke: + name: "${{ matrix.os }} / py${{ matrix.python-version }}" + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-14] + python-version: ["3.11", "3.12", "3.13"] + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Upgrade build tooling + run: python -m pip install --upgrade pip setuptools wheel + + - name: Install package and test dependencies + run: | + python -m pip install . + python -m pip install pytest + + - name: Parser test suite + run: python -m pytest tests/test_parser.py -q + + - name: CLI smoke test + run: | + codegenome --help + python -c "import codegenome; print(codegenome.__version__)" diff --git a/CHANGELOG.md b/CHANGELOG.md index 98e770a..6739f05 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Agent rules no longer reference misleading HTTP endpoints that caused agents to `curl` the server instead of using MCP transport. - MCP server keeps localhost-only behavior by default and now requires an explicit remote HTTP opt-in (`--allow-remote-http`) for non-loopback hosts. - Release lint blockers (unused imports and test lint violations) were resolved so full lint/test/build gates pass before upload. +- Tree-sitter dependency constraints now support Python 3.12+ installations (including macOS Apple Silicon) while preserving legacy pins for Python 3.11 compatibility. ### Documentation diff --git a/pyproject.toml b/pyproject.toml index b281dd9..dc9c188 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,12 +24,18 @@ classifiers = [ "Topic :: Software Development :: Quality Assurance", ] dependencies = [ - "tree-sitter==0.21.3", - "tree-sitter-python==0.21.0", - "tree-sitter-javascript==0.21.0", - "tree-sitter-typescript==0.21.0", - "tree-sitter-go==0.21.0", - "tree-sitter-rust==0.21.2", + "tree-sitter==0.21.3; python_version < '3.12'", + "tree-sitter>=0.23,<0.26; python_version >= '3.12'", + "tree-sitter-python==0.21.0; python_version < '3.12'", + "tree-sitter-python>=0.23,<0.26; python_version >= '3.12'", + "tree-sitter-javascript==0.21.0; python_version < '3.12'", + "tree-sitter-javascript>=0.23,<0.26; python_version >= '3.12'", + "tree-sitter-typescript==0.21.0; python_version < '3.12'", + "tree-sitter-typescript>=0.23,<0.26; python_version >= '3.12'", + "tree-sitter-go==0.21.0; python_version < '3.12'", + "tree-sitter-go>=0.23,<0.26; python_version >= '3.12'", + "tree-sitter-rust==0.21.2; python_version < '3.12'", + "tree-sitter-rust>=0.23,<0.26; python_version >= '3.12'", "watchdog", "fastmcp", "leidenalg", diff --git a/requirements.txt b/requirements.txt index 5d7ae1f..cdb7500 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,9 +1,15 @@ -tree-sitter==0.21.3 -tree-sitter-python==0.21.0 -tree-sitter-javascript==0.21.0 -tree-sitter-typescript==0.21.0 -tree-sitter-go==0.21.0 -tree-sitter-rust==0.21.2 +tree-sitter==0.21.3; python_version < "3.12" +tree-sitter>=0.23,<0.26; python_version >= "3.12" +tree-sitter-python==0.21.0; python_version < "3.12" +tree-sitter-python>=0.23,<0.26; python_version >= "3.12" +tree-sitter-javascript==0.21.0; python_version < "3.12" +tree-sitter-javascript>=0.23,<0.26; python_version >= "3.12" +tree-sitter-typescript==0.21.0; python_version < "3.12" +tree-sitter-typescript>=0.23,<0.26; python_version >= "3.12" +tree-sitter-go==0.21.0; python_version < "3.12" +tree-sitter-go>=0.23,<0.26; python_version >= "3.12" +tree-sitter-rust==0.21.2; python_version < "3.12" +tree-sitter-rust>=0.23,<0.26; python_version >= "3.12" networkx>=3.2,<4 watchdog>=4.0,<5 fastmcp>=2.0,<3 diff --git a/src/codegenome/parser.py b/src/codegenome/parser.py index 071d2e5..21f4f7f 100644 --- a/src/codegenome/parser.py +++ b/src/codegenome/parser.py @@ -214,12 +214,22 @@ def _load_languages() -> dict[str, Language]: for key, module_name, attr_name, lang_name in specs: try: module = __import__(module_name) - languages[key] = Language(getattr(module, attr_name)(), lang_name) + languages[key] = _build_language(module, attr_name, lang_name) except Exception as exc: # pragma: no cover - optional grammars logger.warning("Failed to load tree-sitter grammar %s: %s", key, exc) return languages +def _build_language(module: object, attr_name: str, lang_name: str) -> Language: + """Create a Language object across tree-sitter API variants.""" + language_capsule = getattr(module, attr_name)() + try: + return Language(language_capsule, lang_name) + except TypeError: + # Newer tree-sitter releases accept only the grammar capsule. + return Language(language_capsule) + + class SourceParser: """Parse source files and extract symbols, imports, calls, and inheritance. diff --git a/tests/test_parser.py b/tests/test_parser.py index ce32782..9ee060f 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -3,9 +3,11 @@ from __future__ import annotations from pathlib import Path +from types import SimpleNamespace import pytest +from codegenome import parser as parser_module from codegenome.parser import SourceParser @@ -129,3 +131,33 @@ def test_parser_read_failure_is_graceful(parser: SourceParser, tmp_path: Path) - result = parser.parse_file(path) assert result is not None assert result.errors + + +def test_build_language_supports_legacy_signature(monkeypatch: pytest.MonkeyPatch) -> None: + capsule = object() + + class LegacyLanguage: + def __init__(self, received_capsule: object, name: str) -> None: + self.received_capsule = received_capsule + self.name = name + + module = SimpleNamespace(language=lambda: capsule) + monkeypatch.setattr(parser_module, "Language", LegacyLanguage) + + language = parser_module._build_language(module, "language", "python") + assert language.received_capsule is capsule + assert language.name == "python" + + +def test_build_language_supports_modern_signature(monkeypatch: pytest.MonkeyPatch) -> None: + capsule = object() + + class ModernLanguage: + def __init__(self, received_capsule: object) -> None: + self.received_capsule = received_capsule + + module = SimpleNamespace(language=lambda: capsule) + monkeypatch.setattr(parser_module, "Language", ModernLanguage) + + language = parser_module._build_language(module, "language", "python") + assert language.received_capsule is capsule From 6ca1b875b7564a5ceea5996ecab0e36d6999e66b Mon Sep 17 00:00:00 2001 From: "Md. Fatin Shadab Turja" <71595077+FatinShadab@users.noreply.github.com> Date: Mon, 1 Jun 2026 10:27:54 +0600 Subject: [PATCH 2/5] feature enhance made the tui console outputs copyable --- src/codegenome/tui.py | 77 ++++++++++++++++++++++++++++++++----------- tests/test_tui.py | 36 +++++++++++++++++++- 2 files changed, 93 insertions(+), 20 deletions(-) diff --git a/src/codegenome/tui.py b/src/codegenome/tui.py index 45eefd8..948b720 100644 --- a/src/codegenome/tui.py +++ b/src/codegenome/tui.py @@ -9,8 +9,11 @@ from pathlib import Path from typing import Literal +from textual import events +from textual.actions import SkipAction from textual.app import App, ComposeResult from textual.containers import Container, Horizontal, Vertical +from textual.selection import Selection from textual.widgets import Button, ContentSwitcher, Footer, Header, Input, Label, RichLog, Static, TabbedContent, TabPane from textual.worker import Worker, WorkerState, get_current_worker @@ -38,12 +41,38 @@ class ActiveProcess: channel: LogChannel +class ReadOnlyRichLog(RichLog): + """Console log output: selectable/copyable, not keyboard-editable.""" + + ALLOW_SELECT = True + + def get_selection(self, selection: Selection) -> tuple[str, str] | None: + """Return plain text for the selected region.""" + if not self.lines: + return None + text = "\n".join(line.text for line in self.lines) + extracted = selection.extract(text) + if not extracted: + return None + return extracted, "\n" + + def selection_updated(self, selection: Selection | None) -> None: + self._line_cache.clear() + self.refresh() + + def on_key(self, event: events.Key) -> None: + """Ignore printable keys so log panes cannot be edited.""" + if event.character and event.character.isprintable(): + event.prevent_default() + event.stop() + + class CodeGenomeTUI(App): """A Textual app for managing CodeGenome.""" BINDINGS = [ ("ctrl+q", "quit_app", "Quit"), - ("ctrl+c", "quit_app", "Quit"), + ("ctrl+c", "copy_log_text", "Copy"), ] CSS = """ @@ -116,7 +145,7 @@ class CodeGenomeTUI(App): margin-bottom: 1; } - .info-panel RichLog { + .info-panel ReadOnlyRichLog { height: 1fr; min-height: 6; } @@ -170,25 +199,25 @@ class CodeGenomeTUI(App): layout: vertical; } - .log-pane RichLog { + .log-pane ReadOnlyRichLog { height: 1fr; width: 1fr; border: solid $surface-lighten-1; } - #tab-analyze RichLog { + #tab-analyze ReadOnlyRichLog { border: solid cyan; } - #tab-mcp RichLog { + #tab-mcp ReadOnlyRichLog { border: solid green; } - #tab-evolve RichLog { + #tab-evolve ReadOnlyRichLog { border: solid magenta; } - #tab-general RichLog { + #tab-general ReadOnlyRichLog { border: solid white; } """ @@ -239,13 +268,13 @@ def compose(self) -> ComposeResult: with Horizontal(id="workspace-info-panels"): with Vertical(classes="info-panel"): yield Label("[bold cyan]Tracked Folders[/bold cyan]") - yield RichLog(id="info-folders", markup=True, highlight=False, wrap=True) + yield ReadOnlyRichLog(id="info-folders", markup=True, highlight=False, wrap=True) with Vertical(classes="info-panel"): yield Label("[bold cyan]File Extensions[/bold cyan]") - yield RichLog(id="info-extensions", markup=True, highlight=False, wrap=True) + yield ReadOnlyRichLog(id="info-extensions", markup=True, highlight=False, wrap=True) with Vertical(classes="info-panel"): yield Label("[bold cyan].gitignore Files[/bold cyan]") - yield RichLog(id="info-gitignore", markup=True, highlight=False, wrap=True) + yield ReadOnlyRichLog(id="info-gitignore", markup=True, highlight=False, wrap=True) with Horizontal(classes="page-actions"): yield Button("Back", id="btn-back-to-set", variant="default") yield Button("Continue", id="btn-continue", variant="primary", disabled=True) @@ -270,16 +299,16 @@ def compose(self) -> ComposeResult: with TabbedContent(initial="tab-analyze"): with TabPane("Analyze", id="tab-analyze"): with Vertical(classes="log-pane"): - yield RichLog(id="log-analyze", markup=True, highlight=True) + yield ReadOnlyRichLog(id="log-analyze", markup=True, highlight=True) with TabPane("MCP Server", id="tab-mcp"): with Vertical(classes="log-pane"): - yield RichLog(id="log-mcp", markup=True, highlight=True) + yield ReadOnlyRichLog(id="log-mcp", markup=True, highlight=True) with TabPane("Live Evolve", id="tab-evolve"): with Vertical(classes="log-pane"): - yield RichLog(id="log-evolve", markup=True, highlight=True) + yield ReadOnlyRichLog(id="log-evolve", markup=True, highlight=True) with TabPane("General", id="tab-general"): with Vertical(classes="log-pane"): - yield RichLog(id="log-general", markup=True, highlight=True) + yield ReadOnlyRichLog(id="log-general", markup=True, highlight=True) with Horizontal(classes="page-actions"): yield Button("Stop MCP Server", id="btn-stop-mcp", variant="error") @@ -294,15 +323,15 @@ def on_mount(self) -> None: self.pages = self.query_one(ContentSwitcher) self.workspace_input = self.query_one("#workspace-input", Input) self.workspace_scan_status = self.query_one("#workspace-scan-status", Static) - self.info_folders_log = self.query_one("#info-folders", RichLog) - self.info_extensions_log = self.query_one("#info-extensions", RichLog) - self.info_gitignore_log = self.query_one("#info-gitignore", RichLog) + self.info_folders_log = self.query_one("#info-folders", ReadOnlyRichLog) + self.info_extensions_log = self.query_one("#info-extensions", ReadOnlyRichLog) + self.info_gitignore_log = self.query_one("#info-gitignore", ReadOnlyRichLog) self.workspace_summary = self.query_one("#workspace-summary", Static) self.continue_button = self.query_one("#btn-continue", Button) self.set_workspace_button = self.query_one("#btn-set-workspace", Button) self.log_tabs = self.query_one(TabbedContent) - self.log_widgets: dict[LogChannel, RichLog] = { - channel: self.query_one(f"#{log_id}", RichLog) + self.log_widgets: dict[LogChannel, ReadOnlyRichLog] = { + channel: self.query_one(f"#{log_id}", ReadOnlyRichLog) for channel, log_id in self.LOG_IDS.items() } self.command_buttons: dict[str, Button] = { @@ -758,6 +787,16 @@ async def _cleanup_subprocesses(self) -> None: self._subprocesses.clear() self.active_processes.clear() + def action_copy_log_text(self) -> None: + """Copy selected log text to the clipboard.""" + try: + self.screen.action_copy_text() + except SkipAction: + self.notify( + "Select text in a log panel first, then press Ctrl+C.", + severity="warning", + ) + def action_quit_app(self) -> None: """Handle quit action from bindings.""" self.quit_app() diff --git a/tests/test_tui.py b/tests/test_tui.py index 9371a6a..5ac818f 100644 --- a/tests/test_tui.py +++ b/tests/test_tui.py @@ -4,7 +4,41 @@ import asyncio -from codegenome.tui import ActiveProcess, CodeGenomeTUI +from rich.segment import Segment + +from textual import events +from textual.geometry import Offset +from textual.selection import Selection +from textual.strip import Strip + +from codegenome.tui import ActiveProcess, CodeGenomeTUI, ReadOnlyRichLog + + +def test_bindings_use_ctrl_c_for_copy_not_quit() -> None: + binding_map = {key: action for key, action, _description in CodeGenomeTUI.BINDINGS} + assert binding_map["ctrl+c"] == "copy_log_text" + assert binding_map["ctrl+q"] == "quit_app" + + +def test_read_only_rich_log_get_selection_returns_plain_text() -> None: + log = ReadOnlyRichLog() + log.lines = [ + Strip([Segment("alpha")], 5), + Strip([Segment("beta")], 4), + ] + selection = Selection(Offset(0, 0), Offset(4, 1)) + result = log.get_selection(selection) + assert result is not None + text, ending = result + assert text == "alpha\nbeta" + assert ending == "\n" + + +def test_read_only_rich_log_blocks_printable_keys() -> None: + log = ReadOnlyRichLog() + event = events.Key(key="x", character="x") + log.on_key(event) + assert event._stop_propagation is True def test_command_button_ids_include_channel_specific_stop_buttons() -> None: From a6d687016ae2419c2ddd0b8957a9001a3ba5b355 Mon Sep 17 00:00:00 2001 From: Sajid Date: Mon, 1 Jun 2026 11:09:11 +0600 Subject: [PATCH 3/5] copy button added --- .cursor/rules/watcher-knowledge-graph.mdc | 29 ++++ .github/copilot-instructions.md | 24 +++ .idea/.gitignore | 10 ++ .idea/codegenome.iml | 9 + .idea/compiler.xml | 8 + .idea/misc.xml | 6 + .idea/modules.xml | 8 + .idea/vcs.xml | 6 + .windsurfrules | 24 +++ AGENTS.md | 24 +++ package-lock.json | 6 + src/codegenome/tui.py | 201 +++++++++++++++++++--- tests/test_tui.py | 49 ++++++ 13 files changed, 380 insertions(+), 24 deletions(-) create mode 100644 .cursor/rules/watcher-knowledge-graph.mdc create mode 100644 .github/copilot-instructions.md create mode 100644 .idea/.gitignore create mode 100644 .idea/codegenome.iml create mode 100644 .idea/compiler.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/modules.xml create mode 100644 .idea/vcs.xml create mode 100644 .windsurfrules create mode 100644 AGENTS.md create mode 100644 package-lock.json diff --git a/.cursor/rules/watcher-knowledge-graph.mdc b/.cursor/rules/watcher-knowledge-graph.mdc new file mode 100644 index 0000000..42b7600 --- /dev/null +++ b/.cursor/rules/watcher-knowledge-graph.mdc @@ -0,0 +1,29 @@ +--- +description: CodeGenome knowledge graph and MCP Context +alwaysApply: true +--- + +# CodeGenome MCP Integration + +You are operating within a repository analyzed by CodeGenome, an architectural knowledge graph tool. This project contains a `.genome/` directory. + +## Core Directives + +1. **Mandatory MCP Usage**: When `.genome/watcher.db` exists, you MUST use CodeGenome MCP access for all codebase, architecture, dependency, or symbol queries whenever it is available. +2. **Access Order**: First use native CodeGenome MCP tools exposed in your context. If those tools are missing, you MAY try a local MCP HTTP endpoint such as `http://127.0.0.1:7331/mcp` when the user has started it or configured it. Treat this as MCP transport access, not as an arbitrary application HTTP API. +3. **Prefer Graph over Grep**: Use graph-backed MCP tools instead of raw file searching (`grep`) or reading entire files blindly. The graph provides semantic understanding. +4. **Fallback Gracefully**: If native MCP tools are missing and HTTP MCP access is unavailable, incompatible, or returns empty data, tell the user exactly what failed and what to configure. Then, if needed, read `.genome/graph.json` or `.genome/exports/*.md` before resorting to standard text searches. +5. **Keep Context Fresh**: After modifying code files, remind the user to run `codegenome analyze` or start the live evolution server `codegenome evolve --live` to keep the graph updated. + +## Available MCP Capabilities + +- **Discovery**: `search_nodes` (find symbols) +- **Relationships**: `get_neighbors` (imports, callers, callees) +- **Architecture**: `get_entry_points`, `get_dead_code`, `get_circular_deps`, `get_god_nodes` +- **Metrics**: `get_complexity`, `get_churn`, `get_graph` (summary statistics) +- **Evolution**: `get_timeline`, `get_changes` (architectural diffs) + +## Constraints & Behaviors + +- Only read `.genome/graph.json` or `.genome/exports/*.md` directly if native MCP tools and local HTTP MCP transport are unavailable or fail to surface enough context. +- Verify your MCP usage by monitoring tool call success. If native tools are missing, try the configured local HTTP MCP endpoint when possible. If both native and HTTP MCP access fail, politely ask the user to configure their editor's MCP settings to run `codegenome mcp-start` (stdio) or start the server with `codegenome mcp-start --transport http`. diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..b512202 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,24 @@ +# CodeGenome Knowledge Graph (MCP) + +You are operating within a repository analyzed by CodeGenome, an architectural knowledge graph tool. This project contains a `.genome/` directory. + +## Core Directives + +1. **Mandatory MCP Usage**: When `.genome/watcher.db` exists, you MUST use CodeGenome MCP access for all codebase, architecture, dependency, or symbol queries whenever it is available. +2. **Access Order**: First use native CodeGenome MCP tools exposed in your context. If those tools are missing, you MAY try a local MCP HTTP endpoint such as `http://127.0.0.1:7331/mcp` when the user has started it or configured it. Treat this as MCP transport access, not as an arbitrary application HTTP API. +3. **Prefer Graph over Grep**: Use graph-backed MCP tools instead of raw file searching (`grep`) or reading entire files blindly. The graph provides semantic understanding. +4. **Fallback Gracefully**: If native MCP tools are missing and HTTP MCP access is unavailable, incompatible, or returns empty data, tell the user exactly what failed and what to configure. Then, if needed, read `.genome/graph.json` or `.genome/exports/*.md` before resorting to standard text searches. +5. **Keep Context Fresh**: After modifying code files, remind the user to run `codegenome analyze` or start the live evolution server `codegenome evolve --live` to keep the graph updated. + +## Available MCP Capabilities + +- **Discovery**: `search_nodes` (find symbols) +- **Relationships**: `get_neighbors` (imports, callers, callees) +- **Architecture**: `get_entry_points`, `get_dead_code`, `get_circular_deps`, `get_god_nodes` +- **Metrics**: `get_complexity`, `get_churn`, `get_graph` (summary statistics) +- **Evolution**: `get_timeline`, `get_changes` (architectural diffs) + +## Constraints & Behaviors + +- Only read `.genome/graph.json` or `.genome/exports/*.md` directly if native MCP tools and local HTTP MCP transport are unavailable or fail to surface enough context. +- Verify your MCP usage by monitoring tool call success. If native tools are missing, try the configured local HTTP MCP endpoint when possible. If both native and HTTP MCP access fail, politely ask the user to configure their editor's MCP settings to run `codegenome mcp-start` (stdio) or start the server with `codegenome mcp-start --transport http`. diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..7bc07ec --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,10 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Environment-dependent path to Maven home directory +/mavenHomeManager.xml +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/codegenome.iml b/.idea/codegenome.iml new file mode 100644 index 0000000..d6ebd48 --- /dev/null +++ b/.idea/codegenome.iml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..a1757ae --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..628afae --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..f338140 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.windsurfrules b/.windsurfrules new file mode 100644 index 0000000..b512202 --- /dev/null +++ b/.windsurfrules @@ -0,0 +1,24 @@ +# CodeGenome Knowledge Graph (MCP) + +You are operating within a repository analyzed by CodeGenome, an architectural knowledge graph tool. This project contains a `.genome/` directory. + +## Core Directives + +1. **Mandatory MCP Usage**: When `.genome/watcher.db` exists, you MUST use CodeGenome MCP access for all codebase, architecture, dependency, or symbol queries whenever it is available. +2. **Access Order**: First use native CodeGenome MCP tools exposed in your context. If those tools are missing, you MAY try a local MCP HTTP endpoint such as `http://127.0.0.1:7331/mcp` when the user has started it or configured it. Treat this as MCP transport access, not as an arbitrary application HTTP API. +3. **Prefer Graph over Grep**: Use graph-backed MCP tools instead of raw file searching (`grep`) or reading entire files blindly. The graph provides semantic understanding. +4. **Fallback Gracefully**: If native MCP tools are missing and HTTP MCP access is unavailable, incompatible, or returns empty data, tell the user exactly what failed and what to configure. Then, if needed, read `.genome/graph.json` or `.genome/exports/*.md` before resorting to standard text searches. +5. **Keep Context Fresh**: After modifying code files, remind the user to run `codegenome analyze` or start the live evolution server `codegenome evolve --live` to keep the graph updated. + +## Available MCP Capabilities + +- **Discovery**: `search_nodes` (find symbols) +- **Relationships**: `get_neighbors` (imports, callers, callees) +- **Architecture**: `get_entry_points`, `get_dead_code`, `get_circular_deps`, `get_god_nodes` +- **Metrics**: `get_complexity`, `get_churn`, `get_graph` (summary statistics) +- **Evolution**: `get_timeline`, `get_changes` (architectural diffs) + +## Constraints & Behaviors + +- Only read `.genome/graph.json` or `.genome/exports/*.md` directly if native MCP tools and local HTTP MCP transport are unavailable or fail to surface enough context. +- Verify your MCP usage by monitoring tool call success. If native tools are missing, try the configured local HTTP MCP endpoint when possible. If both native and HTTP MCP access fail, politely ask the user to configure their editor's MCP settings to run `codegenome mcp-start` (stdio) or start the server with `codegenome mcp-start --transport http`. diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..b512202 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,24 @@ +# CodeGenome Knowledge Graph (MCP) + +You are operating within a repository analyzed by CodeGenome, an architectural knowledge graph tool. This project contains a `.genome/` directory. + +## Core Directives + +1. **Mandatory MCP Usage**: When `.genome/watcher.db` exists, you MUST use CodeGenome MCP access for all codebase, architecture, dependency, or symbol queries whenever it is available. +2. **Access Order**: First use native CodeGenome MCP tools exposed in your context. If those tools are missing, you MAY try a local MCP HTTP endpoint such as `http://127.0.0.1:7331/mcp` when the user has started it or configured it. Treat this as MCP transport access, not as an arbitrary application HTTP API. +3. **Prefer Graph over Grep**: Use graph-backed MCP tools instead of raw file searching (`grep`) or reading entire files blindly. The graph provides semantic understanding. +4. **Fallback Gracefully**: If native MCP tools are missing and HTTP MCP access is unavailable, incompatible, or returns empty data, tell the user exactly what failed and what to configure. Then, if needed, read `.genome/graph.json` or `.genome/exports/*.md` before resorting to standard text searches. +5. **Keep Context Fresh**: After modifying code files, remind the user to run `codegenome analyze` or start the live evolution server `codegenome evolve --live` to keep the graph updated. + +## Available MCP Capabilities + +- **Discovery**: `search_nodes` (find symbols) +- **Relationships**: `get_neighbors` (imports, callers, callees) +- **Architecture**: `get_entry_points`, `get_dead_code`, `get_circular_deps`, `get_god_nodes` +- **Metrics**: `get_complexity`, `get_churn`, `get_graph` (summary statistics) +- **Evolution**: `get_timeline`, `get_changes` (architectural diffs) + +## Constraints & Behaviors + +- Only read `.genome/graph.json` or `.genome/exports/*.md` directly if native MCP tools and local HTTP MCP transport are unavailable or fail to surface enough context. +- Verify your MCP usage by monitoring tool call success. If native tools are missing, try the configured local HTTP MCP endpoint when possible. If both native and HTTP MCP access fail, politely ask the user to configure their editor's MCP settings to run `codegenome mcp-start` (stdio) or start the server with `codegenome mcp-start --transport http`. diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..5f6e7b9 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "codegenome", + "lockfileVersion": 3, + "requires": true, + "packages": {} +} diff --git a/src/codegenome/tui.py b/src/codegenome/tui.py index 948b720..48d4b6d 100644 --- a/src/codegenome/tui.py +++ b/src/codegenome/tui.py @@ -14,7 +14,18 @@ from textual.app import App, ComposeResult from textual.containers import Container, Horizontal, Vertical from textual.selection import Selection -from textual.widgets import Button, ContentSwitcher, Footer, Header, Input, Label, RichLog, Static, TabbedContent, TabPane +from textual.widgets import ( + Button, + ContentSwitcher, + Footer, + Header, + Input, + Label, + RichLog, + Static, + TabbedContent, + TabPane, +) from textual.worker import Worker, WorkerState, get_current_worker from codegenome.workspace_info import ( @@ -70,6 +81,11 @@ def on_key(self, event: events.Key) -> None: class CodeGenomeTUI(App): """A Textual app for managing CodeGenome.""" + def __init__(self, *args, **kwargs) -> None: + super().__init__(*args, **kwargs) + self._subprocesses: set[asyncio.subprocess.Process] = set() + self.active_processes: list[ActiveProcess] = [] + BINDINGS = [ ("ctrl+q", "quit_app", "Quit"), ("ctrl+c", "copy_log_text", "Copy"), @@ -220,6 +236,36 @@ class CodeGenomeTUI(App): #tab-general ReadOnlyRichLog { border: solid white; } + + .panel-header { + height: 1; + layout: horizontal; + align: left middle; + margin-bottom: 1; + } + + .panel-header Label { + width: 1fr; + margin: 0; + padding: 0; + height: auto; + } + + .copy-btn { + height: 1; + min-width: 8; + border: none; + padding: 0 1; + margin: 0; + background: $surface-lighten-1; + color: $text; + text-style: bold; + } + + .copy-btn:hover { + background: $primary; + color: $text; + } """ LOG_IDS: dict[LogChannel, str] = { @@ -257,7 +303,9 @@ def compose(self) -> ComposeResult: with Vertical(classes="set-workspace-panel"): yield Label("[bold]Set Workspace[/bold]") yield Label("Enter the path to your project root:") - yield Input(value=".", id="workspace-input", placeholder="Enter path to workspace...") + yield Input( + value=".", id="workspace-input", placeholder="Enter path to workspace..." + ) with Horizontal(classes="page-actions"): yield Button("Set", id="btn-set-workspace", variant="primary") yield Button("Quit", id="btn-quit", variant="default") @@ -267,14 +315,38 @@ def compose(self) -> ComposeResult: yield Static(id="workspace-scan-status", markup=True) with Horizontal(id="workspace-info-panels"): with Vertical(classes="info-panel"): - yield Label("[bold cyan]Tracked Folders[/bold cyan]") - yield ReadOnlyRichLog(id="info-folders", markup=True, highlight=False, wrap=True) + with Horizontal(classes="panel-header"): + yield Label("[bold cyan]Tracked Folders[/bold cyan]") + yield Button( + "Copy", id="btn-copy-folders", variant="default", classes="copy-btn" + ) + yield ReadOnlyRichLog( + id="info-folders", markup=True, highlight=False, wrap=True + ) with Vertical(classes="info-panel"): - yield Label("[bold cyan]File Extensions[/bold cyan]") - yield ReadOnlyRichLog(id="info-extensions", markup=True, highlight=False, wrap=True) + with Horizontal(classes="panel-header"): + yield Label("[bold cyan]File Extensions[/bold cyan]") + yield Button( + "Copy", + id="btn-copy-extensions", + variant="default", + classes="copy-btn", + ) + yield ReadOnlyRichLog( + id="info-extensions", markup=True, highlight=False, wrap=True + ) with Vertical(classes="info-panel"): - yield Label("[bold cyan].gitignore Files[/bold cyan]") - yield ReadOnlyRichLog(id="info-gitignore", markup=True, highlight=False, wrap=True) + with Horizontal(classes="panel-header"): + yield Label("[bold cyan].gitignore Files[/bold cyan]") + yield Button( + "Copy", + id="btn-copy-gitignore", + variant="default", + classes="copy-btn", + ) + yield ReadOnlyRichLog( + id="info-gitignore", markup=True, highlight=False, wrap=True + ) with Horizontal(classes="page-actions"): yield Button("Back", id="btn-back-to-set", variant="default") yield Button("Continue", id="btn-continue", variant="primary", disabled=True) @@ -289,25 +361,61 @@ def compose(self) -> ComposeResult: yield Button("Analyze", id="btn-analyze", variant="primary") yield Button("Export", id="btn-export", variant="primary") yield Button("Generate AI Rules", id="btn-rules", variant="primary") - yield Button("Start MCP HTTP (Local)", id="btn-mcp-local", variant="success") + yield Button( + "Start MCP HTTP (Local)", id="btn-mcp-local", variant="success" + ) yield Button("Start MCP HTTP (LAN)", id="btn-mcp-lan", variant="warning") with Horizontal(classes="command-row"): - yield Button("Live Evolve (Local)", id="btn-evolve-local", variant="success") + yield Button( + "Live Evolve (Local)", id="btn-evolve-local", variant="success" + ) yield Button("Live Evolve (LAN)", id="btn-evolve-lan", variant="success") with Container(id="log-container"): with TabbedContent(initial="tab-analyze"): with TabPane("Analyze", id="tab-analyze"): with Vertical(classes="log-pane"): + with Horizontal(classes="panel-header"): + yield Label("[bold cyan]Analyze Log[/bold cyan]") + yield Button( + "Copy", + id="btn-copy-analyze", + variant="default", + classes="copy-btn", + ) yield ReadOnlyRichLog(id="log-analyze", markup=True, highlight=True) with TabPane("MCP Server", id="tab-mcp"): with Vertical(classes="log-pane"): + with Horizontal(classes="panel-header"): + yield Label("[bold cyan]MCP Server Log[/bold cyan]") + yield Button( + "Copy", + id="btn-copy-mcp", + variant="default", + classes="copy-btn", + ) yield ReadOnlyRichLog(id="log-mcp", markup=True, highlight=True) with TabPane("Live Evolve", id="tab-evolve"): with Vertical(classes="log-pane"): + with Horizontal(classes="panel-header"): + yield Label("[bold cyan]Live Evolve Log[/bold cyan]") + yield Button( + "Copy", + id="btn-copy-evolve", + variant="default", + classes="copy-btn", + ) yield ReadOnlyRichLog(id="log-evolve", markup=True, highlight=True) with TabPane("General", id="tab-general"): with Vertical(classes="log-pane"): + with Horizontal(classes="panel-header"): + yield Label("[bold cyan]General Log[/bold cyan]") + yield Button( + "Copy", + id="btn-copy-general", + variant="default", + classes="copy-btn", + ) yield ReadOnlyRichLog(id="log-general", markup=True, highlight=True) with Horizontal(classes="page-actions"): @@ -338,8 +446,6 @@ def on_mount(self) -> None: button_id: self.query_one(f"#{button_id}", Button) for button_id in self.COMMAND_BUTTON_IDS } - self.active_processes: list[ActiveProcess] = [] - self._subprocesses: set[asyncio.subprocess.Process] = set() self._workspace_path: str | None = None self._pending_workspace_info: WorkspaceInfo | None = None self._main_initialized = False @@ -377,8 +483,7 @@ def update_workspace_scan_panels(self, info: WorkspaceInfo) -> None: """Populate the three scan result panels from workspace info.""" if info.error: self.workspace_scan_status.update( - f"[bold]Root:[/bold] {info.root} " - f"[bold red]Error:[/bold red] {info.error}" + f"[bold]Root:[/bold] {info.root} [bold red]Error:[/bold red] {info.error}" ) else: dir_count = len(info.tracked_directories) @@ -428,7 +533,11 @@ async def _load_workspace_info(self, path: str) -> None: def background_refresh_workspace_info(self) -> None: """Periodically refresh workspace counts in the background.""" - if getattr(self, "_workspace_path", None) and getattr(self, "pages", None) and self.pages.current == PAGE_MAIN: + if ( + getattr(self, "_workspace_path", None) + and getattr(self, "pages", None) + and self.pages.current == PAGE_MAIN + ): self.run_worker( self._do_background_refresh(self._workspace_path), exclusive=True, @@ -441,7 +550,7 @@ async def _do_background_refresh(self, path: str) -> None: info = await asyncio.to_thread(collect_workspace_info, Path(path)) if worker.is_cancelled: return - + # Only update the summary bar to avoid flashing the UI self.workspace_summary.update(format_workspace_summary(info)) @@ -475,7 +584,9 @@ def enter_main_dashboard(self) -> None: self.show_page(PAGE_MAIN) if getattr(self, "_workspace_poll_timer", None) is None: - self._workspace_poll_timer = self.set_interval(5.0, self.background_refresh_workspace_info) + self._workspace_poll_timer = self.set_interval( + 5.0, self.background_refresh_workspace_info + ) if not self._main_initialized: self._main_initialized = True @@ -495,12 +606,43 @@ def focus_log_tab(self, channel: LogChannel) -> None: """Switch the visible tab to the given log channel.""" self.log_tabs.active = self.TAB_IDS[channel] + def copy_panel_output(self, log_widget: ReadOnlyRichLog, panel_name: str) -> None: + """Get the text from a ReadOnlyRichLog and copy it to clipboard.""" + text = "\n".join(line.text for line in log_widget.lines) + if not text: + self.notify(f"No content to copy in {panel_name}.", severity="warning") + return + self.copy_to_clipboard(text) + self.notify(f"Copied {panel_name} output to clipboard!") + def on_button_pressed(self, event: Button.Pressed) -> None: """Handle command button presses.""" button_id = event.button.id if button_id in self.COMMAND_BUTTON_IDS and event.button.disabled: return + if button_id == "btn-copy-folders": + self.copy_panel_output(self.info_folders_log, "Tracked Folders") + return + elif button_id == "btn-copy-extensions": + self.copy_panel_output(self.info_extensions_log, "File Extensions") + return + elif button_id == "btn-copy-gitignore": + self.copy_panel_output(self.info_gitignore_log, ".gitignore Files") + return + elif button_id == "btn-copy-analyze": + self.copy_panel_output(self.log_widgets["analyze"], "Analyze Log") + return + elif button_id == "btn-copy-mcp": + self.copy_panel_output(self.log_widgets["mcp"], "MCP Server Log") + return + elif button_id == "btn-copy-evolve": + self.copy_panel_output(self.log_widgets["evolve"], "Live Evolve Log") + return + elif button_id == "btn-copy-general": + self.copy_panel_output(self.log_widgets["general"], "General Log") + return + if button_id == "btn-set-workspace": self.refresh_workspace_info() return @@ -536,7 +678,16 @@ def on_button_pressed(self, event: Button.Pressed) -> None: ) elif button_id == "btn-mcp-local": self.run_command( - ["codegenome", "mcp-start", "--path", workspace, "--transport", "http", "--port", "7331"], + [ + "codegenome", + "mcp-start", + "--path", + workspace, + "--transport", + "http", + "--port", + "7331", + ], channel="mcp", is_background=True, ) @@ -781,11 +932,13 @@ async def _stop_processes( async def _cleanup_subprocesses(self) -> None: """Close every tracked subprocess and its pipes.""" - for process in list(self._subprocesses): - with suppress(Exception): - await asyncio.shield(self._close_subprocess(process)) - self._subprocesses.clear() - self.active_processes.clear() + if hasattr(self, "_subprocesses"): + for process in list(self._subprocesses): + with suppress(Exception): + await asyncio.shield(self._close_subprocess(process)) + self._subprocesses.clear() + if hasattr(self, "active_processes"): + self.active_processes.clear() def action_copy_log_text(self) -> None: """Copy selected log text to the clipboard.""" @@ -812,7 +965,7 @@ def quit_app(self) -> None: async def _shutdown_and_exit(self) -> None: """Terminate subprocesses, close pipes, then exit cleanly.""" - if self._subprocesses: + if getattr(self, "_subprocesses", None): self.write_log("general", "[yellow]Stopping running commands...[/yellow]") await self._cleanup_subprocesses() diff --git a/tests/test_tui.py b/tests/test_tui.py index 5ac818f..30c7e65 100644 --- a/tests/test_tui.py +++ b/tests/test_tui.py @@ -97,3 +97,52 @@ def fake_focus(channel: str) -> None: assert logs == [("mcp", "[yellow]No active MCP server process to stop.[/yellow]")] assert focused == ["mcp"] + + +def test_copy_panel_output_copies_text_to_clipboard() -> None: + app = CodeGenomeTUI() + log = ReadOnlyRichLog() + log.lines = [ + Strip([Segment("line 1")], 6), + Strip([Segment("line 2")], 6), + ] + + copied: list[str] = [] + notifications: list[str] = [] + + def fake_copy_to_clipboard(text: str) -> None: + copied.append(text) + + def fake_notify(message: str, *, severity: str = "information") -> None: + notifications.append(message) + + app.copy_to_clipboard = fake_copy_to_clipboard # type: ignore[method-assign] + app.notify = fake_notify # type: ignore[method-assign] + + app.copy_panel_output(log, "Test Panel") + + assert copied == ["line 1\nline 2"] + assert notifications == ["Copied Test Panel output to clipboard!"] + + +def test_copy_panel_output_notifies_if_empty() -> None: + app = CodeGenomeTUI() + log = ReadOnlyRichLog() + log.lines = [] + + copied: list[str] = [] + notifications: list[str] = [] + + def fake_copy_to_clipboard(text: str) -> None: + copied.append(text) + + def fake_notify(message: str, *, severity: str = "information") -> None: + notifications.append(message) + + app.copy_to_clipboard = fake_copy_to_clipboard # type: ignore[method-assign] + app.notify = fake_notify # type: ignore[method-assign] + + app.copy_panel_output(log, "Test Panel") + + assert not copied + assert notifications == ["No content to copy in Test Panel."] From e0811bbc965e2db5b526c47511c29860d050322e Mon Sep 17 00:00:00 2001 From: "Md. Fatin Shadab Turja" <71595077+FatinShadab@users.noreply.github.com> Date: Mon, 1 Jun 2026 21:39:43 +0600 Subject: [PATCH 4/5] Update parser.py --- src/codegenome/parser.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/codegenome/parser.py b/src/codegenome/parser.py index 21f4f7f..4223995 100644 --- a/src/codegenome/parser.py +++ b/src/codegenome/parser.py @@ -242,8 +242,11 @@ def __init__(self) -> None: self._languages = _load_languages() self._parsers: dict[str, Parser] = {} for key, language in self._languages.items(): - parser = Parser() - parser.set_language(language) + try: + parser = Parser(language) + except TypeError: + parser = Parser() + parser.set_language(language) self._parsers[key] = parser def detect_language(self, path: Path | str) -> str | None: From 99b79a53e85e562b3a99d0f78ca89442d9c8c1a7 Mon Sep 17 00:00:00 2001 From: "Md. Fatin Shadab Turja" <71595077+FatinShadab@users.noreply.github.com> Date: Mon, 1 Jun 2026 23:54:11 +0600 Subject: [PATCH 5/5] Update CHANGELOG.md --- CHANGELOG.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6739f05..df3988d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- **Copyable TUI console outputs** — users can now select text in the log panes and press `Ctrl+C` to copy it to the clipboard. - **LAN live graph sharing** — `codegenome evolve --live --lan` binds HTTP and WebSocket to `0.0.0.0` so other devices on the same network can open the live graph. The CLI prints a shareable LAN URL (for example `http://192.168.1.42:8000/graph.html?live=1`). - **TUI MCP HTTP mode controls** — the dashboard now includes separate **Start MCP HTTP (Local)** and **Start MCP HTTP (LAN)** buttons so users can intentionally choose localhost-only or LAN exposure. - **Git-aware file filtering** — the scanner respects workspace `.gitignore` and `.genomeignore` files, including nested ignore files in subdirectories, negation rules (`!pattern`), and anchored patterns. @@ -33,7 +34,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Agent rules no longer reference misleading HTTP endpoints that caused agents to `curl` the server instead of using MCP transport. - MCP server keeps localhost-only behavior by default and now requires an explicit remote HTTP opt-in (`--allow-remote-http`) for non-loopback hosts. - Release lint blockers (unused imports and test lint violations) were resolved so full lint/test/build gates pass before upload. -- Tree-sitter dependency constraints now support Python 3.12+ installations (including macOS Apple Silicon) while preserving legacy pins for Python 3.11 compatibility. +- Tree-sitter dependency constraints now support Python 3.12+ installations (including macOS Apple Silicon) while preserving legacy pins for Python 3.11 compatibility (fixes [#1](https://github.com/Ogro-Projukti/codegenome/issues/1)). +- Updated tree-sitter `Parser` initialization to support both legacy and modern (`>=0.23`) API signatures without breaking runtime. ### Documentation