Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions api/flask_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,16 @@

from __future__ import annotations

from typing import Any, overload
from typing import Any, cast, overload

from flask import Response, current_app, jsonify

from utils.exclusion_rules import RuleTokens

def exclusion_rules() -> list[list[Any]]:

def exclusion_rules() -> list[RuleTokens]:
"""Return loaded exclusion rules from app config (empty list when unset)."""
return current_app.config.get("EXCLUSION_RULES") or []
return cast(list[RuleTokens], current_app.config.get("EXCLUSION_RULES") or [])


@overload
Expand Down
6 changes: 3 additions & 3 deletions api/search.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@
import sqlite3
from typing import Any

from flask import Blueprint, Response, current_app, request
from flask import Blueprint, Response, request

from api.flask_config import json_response
from api.flask_config import exclusion_rules, json_response

from models import ParseWarningCollector, SearchResult
from services.search import (
Expand Down Expand Up @@ -138,7 +138,7 @@ def search() -> tuple[Response, int] | Response:
if workspace_filter and not _workspace_exists(workspace_filter, workspace_path):
return _search_error("Workspace not found", "workspace_not_found", 404)

rules = current_app.config.get("EXCLUSION_RULES") or []
rules = exclusion_rules()
all_history = request.args.get("all_history") in ("1", "true")
since_ms = resolve_search_since_ms(
all_history=all_history,
Expand Down
5 changes: 3 additions & 2 deletions app.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,9 @@ def create_app(exclusion_rules_path: str | None = None) -> Flask:
# Exclusion rules: optional path (CLI or default ~/.cursor-chat-browser/exclusion-rules.txt).
# Rules are loaded once at startup; an app restart is required to pick up changes to the file.
resolved = resolve_exclusion_rules_path(exclusion_rules_path)
loaded_rules = load_rules(resolved)
app.config["EXCLUSION_RULES_PATH"] = resolved
app.config["EXCLUSION_RULES"] = load_rules(resolved)
app.config["EXCLUSION_RULES"] = loaded_rules

@app.context_processor
def inject_year() -> dict[str, int]:
Expand All @@ -75,7 +76,7 @@ def inject_year() -> dict[str, int]:

start_search_index_background(
resolve_workspace_path(),
app.config["EXCLUSION_RULES"],
loaded_rules,
)
except Exception:
logging.getLogger(__name__).exception("Failed to start search index background worker")
Expand Down
2 changes: 1 addition & 1 deletion requirements-lock.txt
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,5 @@ fpdf2==2.8.7 # via -r requirements.txt
itsdangerous==2.2.0 # via flask
jinja2==3.1.6 # via flask
markupsafe==3.0.3 # via flask, jinja2, werkzeug
pillow==12.2.0 # via -r requirements.txt, fpdf2
pillow==12.3.0 # via -r requirements.txt, fpdf2
werkzeug==3.1.8 # via flask
4 changes: 2 additions & 2 deletions services/cli_tabs.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,14 @@
from api.flask_config import json_response

from utils.cli_chat_reader import list_cli_projects, messages_to_bubbles, traverse_blobs
from utils.exclusion_rules import build_searchable_text, is_excluded_by_rules
from utils.exclusion_rules import RuleTokens, build_searchable_text, is_excluded_by_rules
from utils.workspace_path import get_cli_chats_path

_logger = logging.getLogger(__name__)


def get_cli_workspace_tabs(
workspace_id: str, rules: list[Any],
workspace_id: str, rules: list[RuleTokens],
) -> Response | tuple[Response, int]:
"""Return Flask JSON response with tabs for a Cursor CLI project.

Expand Down
12 changes: 6 additions & 6 deletions services/export_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
cursor_cli_session_to_markdown,
cursor_ide_chat_to_markdown,
)
from utils.exclusion_rules import build_searchable_text, is_excluded_by_rules
from utils.exclusion_rules import RuleTokens, build_searchable_text, is_excluded_by_rules
from utils.path_helpers import to_epoch_ms
from utils.text_extract import extract_text_from_bubble, slug
from utils.workspace_path import get_cli_chats_path
Expand Down Expand Up @@ -130,7 +130,7 @@ def build_workspace_display_maps(

def prepare_workspace_orchestration(
workspace_path: str,
rules: list[Any],
rules: list[RuleTokens],
*,
nocache: bool = False,
workspace_entries: list[dict[str, Any]] | None = None,
Expand Down Expand Up @@ -169,7 +169,7 @@ def prepare_workspace_orchestration(

def load_global_db_export_data(
orch: WorkspaceOrchestration,
rules: list[Any],
rules: list[RuleTokens],
*,
nocache: bool = False,
) -> GlobalDbExportData | None:
Expand Down Expand Up @@ -222,7 +222,7 @@ def _collect_ide_export_entries(
*,
orch: WorkspaceOrchestration,
db_data: GlobalDbExportData,
exclusion_rules: list[Any],
exclusion_rules: list[RuleTokens],
since: SinceMode,
last_export_ms: int,
today: str,
Expand Down Expand Up @@ -363,7 +363,7 @@ def _collect_ide_export_entries(

def _collect_cli_export_entries(
*,
exclusion_rules: list[Any],
exclusion_rules: list[RuleTokens],
since: SinceMode,
last_export_ms: int,
today: str,
Expand Down Expand Up @@ -487,7 +487,7 @@ def _collect_cli_export_entries(
def collect_export_entries(
*,
workspace_path: str,
exclusion_rules: list[Any],
exclusion_rules: list[RuleTokens],
since: SinceMode,
last_export_ms: int,
out_dir: str,
Expand Down
14 changes: 7 additions & 7 deletions services/search.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
)
from services.workspace_resolver import infer_invalid_workspace_aliases
from utils.cli_chat_reader import list_cli_projects, messages_to_bubbles, traverse_blobs
from utils.exclusion_rules import build_searchable_text, is_excluded_by_rules
from utils.exclusion_rules import RuleTokens, build_searchable_text, is_excluded_by_rules
from utils.text_extract import extract_text_from_bubble
from utils.path_helpers import (
get_workspace_display_name,
Expand Down Expand Up @@ -368,7 +368,7 @@ def workspace_for_composer(self, composer: Composer) -> str:

def _load_search_workspace_assigner(
workspace_path: str,
rules: list[Any],
rules: list[RuleTokens],
workspace_entries: list[dict[str, Any]],
) -> _SearchWorkspaceAssigner | None:
ctx = resolve_workspace_context_cached(
Expand Down Expand Up @@ -428,7 +428,7 @@ def search_global_storage(
workspace_path: str,
query: str,
query_lower: str,
rules: list[Any],
rules: list[RuleTokens],
parse_warnings: ParseWarningCollector,
*,
since_ms: int | None = None,
Expand Down Expand Up @@ -478,7 +478,7 @@ def _search_global_storage_via_index(
workspace_path: str,
query: str,
query_lower: str,
rules: list[Any],
rules: list[RuleTokens],
parse_warnings: ParseWarningCollector,
*,
since_ms: int | None = None,
Expand Down Expand Up @@ -651,7 +651,7 @@ def _search_global_storage_live_scan(
workspace_path: str,
query: str,
query_lower: str,
rules: list[Any],
rules: list[RuleTokens],
parse_warnings: ParseWarningCollector,
*,
since_ms: int | None = None,
Expand Down Expand Up @@ -828,7 +828,7 @@ def search_legacy_workspaces(
query: str,
query_lower: str,
search_type: str,
rules: list[Any],
rules: list[RuleTokens],
*,
since_ms: int | None = None,
) -> list[SearchResult]:
Expand Down Expand Up @@ -961,7 +961,7 @@ def search_cli_sessions(
cli_chats_path: str,
query: str,
query_lower: str,
rules: list[Any],
rules: list[RuleTokens],
parse_warnings: ParseWarningCollector | None = None,
*,
since_ms: int | None = None,
Expand Down
11 changes: 6 additions & 5 deletions services/search_index.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
open_global_db,
)
from utils.path_helpers import to_epoch_ms
from utils.exclusion_rules import RuleTokens
from utils.workspace_path import get_cli_chats_path

__all__ = [
Expand Down Expand Up @@ -127,7 +128,7 @@ def index_search_enabled() -> bool:
)


def _storage_fingerprint(workspace_path: str, rules: list[Any]) -> dict[str, Any]:
def _storage_fingerprint(workspace_path: str, rules: list[RuleTokens]) -> dict[str, Any]:
entries = collect_workspace_entries(workspace_path)
gdb = global_storage_db_path(workspace_path)
cli_path = get_cli_chats_path()
Expand Down Expand Up @@ -228,7 +229,7 @@ def _create_schema(conn: sqlite3.Connection) -> None:

def build_search_index(
workspace_path: str,
rules: list[Any],
rules: list[RuleTokens],
*,
force: bool = False,
) -> bool:
Expand Down Expand Up @@ -358,7 +359,7 @@ def build_search_index(
return False


def ensure_search_index(workspace_path: str, rules: list[Any]) -> None:
def ensure_search_index(workspace_path: str, rules: list[RuleTokens]) -> None:
"""Build index synchronously if missing or stale."""
if not index_search_enabled():
return
Expand All @@ -377,7 +378,7 @@ def ensure_search_index(workspace_path: str, rules: list[Any]) -> None:

def start_search_index_background(
workspace_path: str,
rules: list[Any],
rules: list[RuleTokens],
*,
poll_seconds: int = 60,
) -> None:
Expand Down Expand Up @@ -549,7 +550,7 @@ def query_composer_rows_in_window(
return {row["composer_id"]: row for row in rows}


def index_is_usable(workspace_path: str, rules: list[Any]) -> bool:
def index_is_usable(workspace_path: str, rules: list[RuleTokens]) -> bool:
"""True when the on-disk index matches the current Cursor storage fingerprint."""
if not index_search_enabled() or _resolve_active_index_db_path() is None:
return False
Expand Down
6 changes: 4 additions & 2 deletions services/summary_cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
from pathlib import Path
from typing import Any

from utils.exclusion_rules import RuleTokens

_logger = logging.getLogger(__name__)

CACHE_VERSION = 1
Expand Down Expand Up @@ -46,7 +48,7 @@ def nocache_enabled(*, request_nocache: bool = False) -> bool:
)


def _rules_digest(rules: list[Any]) -> str:
def _rules_digest(rules: list[RuleTokens]) -> str:
try:
payload = json.dumps(rules, sort_keys=True, ensure_ascii=False)
except (TypeError, ValueError):
Expand All @@ -68,7 +70,7 @@ def fingerprint_workspace_storage(
workspace_entries: list[dict[str, Any]],
*,
global_db_path: str | None,
rules: list[Any],
rules: list[RuleTokens],
cli_chats_path: str | None = None,
) -> dict[str, Any]:
"""Build a fingerprint dict for cache invalidation."""
Expand Down
4 changes: 2 additions & 2 deletions services/workspace_composer_scan.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from typing import Any

from models import Bubble, Composer, ParseWarningCollector, SchemaError
from utils.exclusion_rules import build_searchable_text, is_excluded_by_rules
from utils.exclusion_rules import RuleTokens, build_searchable_text, is_excluded_by_rules
from services.workspace_resolver import determine_project_for_conversation

_logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -81,7 +81,7 @@ def composer_chat_title(composer: Composer) -> str:


def is_composer_excluded(
rules: list[Any],
rules: list[RuleTokens],
*,
project_name: str,
composer: Composer,
Expand Down
8 changes: 5 additions & 3 deletions services/workspace_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
from dataclasses import dataclass, replace
from typing import Any

from utils.exclusion_rules import RuleTokens

from models import Bubble
from services.workspace_db import (
COMPOSER_ROWS_WITH_HEADERS_SQL,
Expand Down Expand Up @@ -86,7 +88,7 @@ def resolve_workspace_context(

def resolve_workspace_context_cached(
workspace_path: str,
rules: list[Any],
rules: list[RuleTokens],
*,
workspace_entries: list[dict[str, Any]] | None = None,
nocache: bool = False,
Expand Down Expand Up @@ -147,7 +149,7 @@ def resolve_invalid_workspace_aliases_cached(
ctx: WorkspaceContext,
global_db: sqlite3.Connection,
workspace_path: str,
rules: list[Any],
rules: list[RuleTokens],
*,
nocache: bool = False,
project_layouts_map: dict[str, list[str]] | None = None,
Expand Down Expand Up @@ -223,7 +225,7 @@ def with_invalid_workspace_aliases(
ctx: WorkspaceContext,
global_db: sqlite3.Connection,
workspace_path: str,
rules: list[Any],
rules: list[RuleTokens],
*,
nocache: bool = False,
project_layouts_map: dict[str, list[str]] | None = None,
Expand Down
4 changes: 3 additions & 1 deletion services/workspace_db.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
from pathlib import Path
from typing import Any, cast

from utils.exclusion_rules import RuleTokens

_logger = logging.getLogger(__name__)

from models import Bubble, ParseWarningCollector, SchemaError
Expand Down Expand Up @@ -428,7 +430,7 @@ def build_composer_id_to_workspace_id(
def build_composer_id_to_workspace_id_cached(
workspace_path: str,
workspace_entries: list[dict[str, Any]],
rules: list[Any],
rules: list[RuleTokens],
*,
nocache: bool = False,
) -> dict[str, str]:
Expand Down
6 changes: 3 additions & 3 deletions services/workspace_listing.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
_logger = logging.getLogger(__name__)

from utils.cli_chat_reader import list_cli_projects
from utils.exclusion_rules import build_searchable_text, is_excluded_by_rules
from utils.exclusion_rules import RuleTokens, build_searchable_text, is_excluded_by_rules
from utils.path_helpers import (
get_workspace_folder_paths,
normalize_file_path,
Expand Down Expand Up @@ -49,7 +49,7 @@

def list_workspace_projects(
workspace_path: str,
rules: list[Any],
rules: list[RuleTokens],
*,
nocache: bool = False,
) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
Expand Down Expand Up @@ -102,7 +102,7 @@ def list_workspace_projects(

def _build_workspace_projects_uncached(
workspace_path: str,
rules: list[Any],
rules: list[RuleTokens],
orch: WorkspaceOrchestration,
*,
nocache: bool = False,
Expand Down
Loading
Loading