Skip to content
43 changes: 37 additions & 6 deletions api/composers.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,12 @@ def list_composers():
try:
local = WorkspaceLocalComposer.from_dict(c)
except SchemaError as e:
print(f"Schema drift in {db_path}: {e}")
_logger.warning(
"Schema drift in %s: %s (%s)",
db_path,
e,
type(e).__name__,
)
continue
# Use the typed view downstream so the dataclass is
# load-bearing, not just a filter (Brad's review): the
Expand All @@ -91,9 +96,20 @@ def list_composers():
c["workspaceFolder"] = workspace_folder
composers.append((local, c))
except SchemaError as e:
print(f"Schema drift in {db_path}: {e}")
_logger.warning(
"Schema drift in %s: %s (%s)",
db_path,
e,
type(e).__name__,
)
except Exception as e:
print(f"Failed reading composers from {db_path}: {e}")
_logger.error(
"Failed reading composers from %s: %s (%s)",
db_path,
e,
type(e).__name__,
exc_info=True,
)

composers.sort(key=lambda pair: to_epoch_ms(pair[0].last_updated_at), reverse=True)
return jsonify([c for _, c in composers])
Expand Down Expand Up @@ -152,7 +168,12 @@ def get_composer(composer_id):
# Same drift list_composers() logs and skips at line ~78,
# so a single-composer fetch can't silently return malformed
# JSON the list endpoint hid.
print(f"Schema drift in workspace-local composer {composer_id}: {e}")
_logger.warning(
"Schema drift in workspace-local composer %s: %s (%s)",
composer_id,
e,
type(e).__name__,
)
continue
# Match list_composers() at line 89 and the global
# fallback below: `conversation` is normalised to []
Expand All @@ -163,7 +184,12 @@ def get_composer(composer_id):
payload["conversation"] = payload.get("conversation") or []
return jsonify(payload)
except SchemaError as e:
print(f"Schema drift in {db_path}: {e}")
_logger.warning(
"Schema drift in %s: %s (%s)",
db_path,
e,
type(e).__name__,
)
except (OSError, sqlite3.Error, json.JSONDecodeError, ValueError):
pass

Expand All @@ -186,7 +212,12 @@ def get_composer(composer_id):
# Don't return malformed JSON to the client — surface the drift
# as a 404 + log, matching the silent-skip behaviour of the
# list endpoints for the same row.
print(f"Schema drift in composer {composer_id}: {e}")
_logger.warning(
"Schema drift in composer %s: %s (%s)",
composer_id,
e,
type(e).__name__,
)
return jsonify({"error": "Composer schema drift"}), 404
payload = dict(composer.raw)
payload["conversation"] = payload.get("conversation") or []
Expand Down
23 changes: 20 additions & 3 deletions api/config_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
src/app/api/get-username/route.ts GET /api/get-username
"""

import logging
import os
import subprocess
import sys
Expand All @@ -16,6 +17,7 @@
from utils.workspace_path import set_workspace_path_override

bp = Blueprint("config_api", __name__)
_logger = logging.getLogger(__name__)


@bp.route("/api/detect-environment")
Expand Down Expand Up @@ -44,7 +46,12 @@ def detect_environment():
})

except Exception as e:
print(f"Failed to detect environment: {e}")
_logger.warning(
"Failed to detect environment: %s (%s)",
e,
type(e).__name__,
exc_info=True,
)
return jsonify({"os": "unknown", "isWSL": False, "isRemote": False})


Expand Down Expand Up @@ -80,7 +87,12 @@ def validate_path():
)

except Exception as e:
print(f"Validation error: {e}")
_logger.error(
"Validation error: %s (%s)",
e,
type(e).__name__,
exc_info=True,
)
return jsonify({"valid": False, "error": "Failed to validate path"}), 500


Expand Down Expand Up @@ -135,5 +147,10 @@ def get_username():
return jsonify({"username": username})

except Exception as e:
print(f"Failed to get username: {e}")
_logger.warning(
"Failed to get username: %s (%s)",
e,
type(e).__name__,
exc_info=True,
)
return jsonify({"username": "YOUR_USERNAME"})
19 changes: 15 additions & 4 deletions api/export_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import io
import json
import logging
import os
import sqlite3
import zipfile
Expand All @@ -32,6 +33,7 @@
)

bp = Blueprint("export_api", __name__)
_logger = logging.getLogger(__name__)


def _get_state_dir() -> str:
Expand Down Expand Up @@ -181,7 +183,13 @@ def export_chats():
exported.append({"path": rel_path, "content": md, "updatedAt": updated_at_ms})

except Exception as e:
print(f"Error processing composer {composer_id} for export: {e}")
_logger.error(
"Error processing composer %s for export: %s (%s)",
composer_id,
e,
type(e).__name__,
exc_info=True,
)

count = len(exported)
if count == 0:
Expand All @@ -208,7 +216,10 @@ def export_chats():
)

except Exception as e:
print(f"Export error: {e}")
import traceback
traceback.print_exc()
_logger.error(
"Export failed: %s (%s)",
e,
type(e).__name__,
exc_info=True,
)
return jsonify({"error": f"Export failed: {str(e)}"}), 500
11 changes: 8 additions & 3 deletions api/pdf.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@
"""

import io
import logging
import re

from flask import Blueprint, Response, jsonify, request

bp = Blueprint("pdf", __name__)
_logger = logging.getLogger(__name__)


def _safe_text(text: str) -> str:
Expand Down Expand Up @@ -168,9 +170,12 @@ def footer(self):
)

except Exception as e:
print(f"Failed to generate PDF: {e}")
import traceback
traceback.print_exc()
_logger.error(
"Failed to generate PDF: %s (%s)",
e,
type(e).__name__,
exc_info=True,
)
return jsonify({"error": f"Failed to generate PDF: {str(e)}"}), 500


Expand Down
14 changes: 12 additions & 2 deletions api/search.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,12 @@ def search():
# Drift logged so the operator can see why a chat dropped
# out of search results; bad row still skipped so search
# keeps returning results from the well-formed ones.
print(f"Schema drift in bubble {bid}: {e}")
_logger.warning(
"Schema drift in bubble %s: %s (%s)",
bid,
e,
type(e).__name__,
)
except (json.JSONDecodeError, ValueError):
pass

Expand All @@ -178,7 +183,12 @@ def search():
try:
composer = Composer.from_dict(json.loads(row["value"]), composer_id=composer_id)
except SchemaError as e:
print(f"Schema drift in composer {composer_id}: {e}")
_logger.warning(
"Schema drift in composer %s: %s (%s)",
composer_id,
e,
type(e).__name__,
)
continue
except (json.JSONDecodeError, TypeError, ValueError):
continue
Expand Down
6 changes: 6 additions & 0 deletions app.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from the Cursor editor's AI chat feature.
"""

import logging
import os
import sys
from datetime import datetime
Expand Down Expand Up @@ -35,6 +36,11 @@ def _get_base_path():


def create_app(exclusion_rules_path=None):
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(levelname)s %(name)s %(funcName)s: %(message)s",
)

base = _get_base_path()
app = Flask(
__name__,
Expand Down
35 changes: 29 additions & 6 deletions scripts/export.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,18 @@
_logger = logging.getLogger(__name__)


def _configure_cli_logging() -> None:
"""Route log records to stderr so stdout stays for export progress lines."""
root = logging.getLogger()
if root.handlers:
return
logging.basicConfig(
level=logging.INFO,
format="%(levelname)s: %(message)s",
stream=sys.stderr,
)


def _json_dump_safe(value) -> str:
"""Best-effort JSON serialization for exclusion matching."""
try:
Expand Down Expand Up @@ -165,6 +177,7 @@ def parse_args():


def main():
_configure_cli_logging()
opts = parse_args()
since = opts["since"]
out_dir = os.path.abspath(opts["out_dir"])
Expand Down Expand Up @@ -215,10 +228,9 @@ def main():

with _open_global_db(workspace_path) as (global_db, global_db_path):
if global_db is None:
print(
f"Note: Cursor IDE global storage not found at {global_db_path}"
" — skipping IDE chats.",
file=sys.stderr,
_logger.info(
"Cursor IDE global storage not found at %s — skipping IDE chats.",
global_db_path,
)
else:
project_layouts_map = load_project_layouts_map(global_db)
Expand Down Expand Up @@ -347,7 +359,12 @@ def main():
try:
cli_projects = list_cli_projects(get_cli_chats_path())
except Exception as e:
print(f"Warning: Could not enumerate CLI chats ({e}) — skipping.", file=sys.stderr)
_logger.warning(
"Could not enumerate CLI chats: %s (%s) — skipping",
e,
type(e).__name__,
exc_info=True,
)
cli_projects = []

for cp in cli_projects:
Expand Down Expand Up @@ -378,7 +395,13 @@ def main():
messages = traverse_blobs(session["db_path"])
bubbles = messages_to_bubbles(messages, created_ms)
except Exception as e:
print(f"Warning: Could not read CLI session {session_id}: {e}", file=sys.stderr)
_logger.warning(
"Could not read CLI session %s: %s (%s)",
session_id,
e,
type(e).__name__,
exc_info=True,
)
continue

if not bubbles:
Expand Down
27 changes: 24 additions & 3 deletions services/cli_tabs.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
from __future__ import annotations

import logging
from datetime import datetime

from flask import current_app, jsonify

_logger = logging.getLogger(__name__)

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.workspace_path import get_cli_chats_path
Expand Down Expand Up @@ -44,13 +47,25 @@ def _get_cli_workspace_tabs(workspace_id: str):
try:
messages = traverse_blobs(session["db_path"])
except Exception as e:
print(f"CLI: could not read session {session_id}: {e}")
_logger.warning(
"Could not read CLI session %s: %s (%s)",
session_id,
e,
type(e).__name__,
exc_info=True,
)
continue

try:
bubbles = messages_to_bubbles(messages, created_ms)
except Exception as e:
print(f"CLI: could not convert session {session_id} to bubbles: {e}")
_logger.warning(
"Could not convert CLI session %s to bubbles: %s (%s)",
session_id,
e,
type(e).__name__,
exc_info=True,
)
continue
if not bubbles:
continue
Expand Down Expand Up @@ -113,5 +128,11 @@ def _get_cli_workspace_tabs(workspace_id: str):
return jsonify({"tabs": tabs})

except Exception as e:
print(f"Failed to get CLI workspace tabs: {e}")
_logger.error(
"Failed to get CLI workspace tabs for %s: %s (%s)",
workspace_id,
e,
type(e).__name__,
exc_info=True,
)
return jsonify({"error": "Failed to get CLI workspace tabs"}), 500
8 changes: 6 additions & 2 deletions services/workspace_listing.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,8 +134,12 @@ def _safe_fetchall(query: str, params: tuple = ()) -> list:
cid,
e,
)
except Exception:
_logger.exception("Failed to load composer rows from global storage")
except Exception as e:
_logger.error(
"Failed to load composer rows from global storage: %s",
e,
exc_info=True,
)

# Group workspace entries by normalized folder path
folder_to_entries: dict[str, list] = {}
Expand Down
Loading
Loading