Skip to content

Commit 4b8548a

Browse files
committed
Merge origin/master into feat/typed-models-schema-validation-24
Resolves three conflicts in scripts/export.py, all the result of PR #35 (linter cleanup) landing on master while PR #30 was open: 1. Line 41 import block — master added `# noqa: E402` to the existing `cursor_md_exporter` import; this branch added `ExportEntry, SchemaError` from `models`. Kept both: the new import also gets `# noqa: E402` so the linter discipline is consistent. 2. Manifest writer (local) at the `for e in exported:` loop — master renamed the loop variable to `entry` (to dodge a mypy `Assignment to variable "e" outside except: block` clash); this branch added `title` + `workspace` fields to each manifest row. Kept master's `entry` rename + this branch's wider schema. 3. Manifest writer (global) — same shape as #2, resolved identically. Plus one secondary fix in tests/test_models.py: master removed `continue-on-error: true` from the mypy CI step, so the stricter gate now flags 5 `var-annotated` errors in this branch's test file (loop variables iterating heterogeneous tuples). Fixed by hoisting each tuple into a typed local — `bad_ids: tuple[object, ...] = (...)` — matching the pattern this same file already uses at line 180. Verified locally on the merged tree: - mypy --ignore-missing-imports --no-strict-optional . → Success - ruff check api/ utils/ scripts/export.py app.py → All checks passed - unittest discover tests → 223 / 223 OK - python app.py boot smoke → HTTP 200, no errors
2 parents b9f34a6 + 4210226 commit 4b8548a

11 files changed

Lines changed: 97 additions & 88 deletions

File tree

.github/workflows/tests.yml

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,9 @@ jobs:
5555
# Codebase already has type hints across most of the surface (~70+ typed
5656
# functions). Mypy runs in lenient mode (--ignore-missing-imports for
5757
# untyped third-party deps; no strict-optional) so the gate isn't a wall
58-
# of false positives on first run. continue-on-error keeps findings as
59-
# warnings during the surface-cleanup phase; flip to required by removing
60-
# continue-on-error once the surface is clean.
58+
# of false positives. The transitional `continue-on-error: true` was
59+
# removed in #29 once mypy reached zero errors on this repo — type
60+
# failures now block merges.
6161
typecheck:
6262
name: Typecheck (mypy)
6363
runs-on: ubuntu-latest
@@ -75,9 +75,8 @@ jobs:
7575
python -m pip install 'flask>=3.0' 'fpdf2>=2.7' 'mypy>=1.10'
7676
7777
- name: Run mypy
78-
# Transitional only (maintainer consensus): keeps CI green until `mypy` exits
79-
# zero on this repo — then delete this line so type errors fail the job.
80-
continue-on-error: true
78+
# No `continue-on-error` — mypy now exits zero on this repo (closes #29),
79+
# so type errors must fail the job from here on.
8180
run: mypy --ignore-missing-imports --no-strict-optional --pretty .
8281

8382
# ── Secret scan: gitleaks ─────────────────────────────────────────────────

api/composers.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"""
66

77
import json
8+
import logging
89
import os
910
import sqlite3
1011
from contextlib import closing
@@ -16,6 +17,7 @@
1617
from models import Composer, SchemaError, Workspace, WorkspaceLocalComposer
1718

1819
bp = Blueprint("composers", __name__)
20+
_logger = logging.getLogger(__name__)
1921

2022

2123
def _read_json_file(path: str):
@@ -96,8 +98,8 @@ def list_composers():
9698
composers.sort(key=lambda pair: to_epoch_ms(pair[0].last_updated_at), reverse=True)
9799
return jsonify([c for _, c in composers])
98100

99-
except Exception as e:
100-
print(f"Failed to get composers: {e}")
101+
except Exception:
102+
_logger.exception("Failed to get composers")
101103
return jsonify({"error": "Failed to get composers"}), 500
102104

103105

@@ -187,6 +189,6 @@ def get_composer(composer_id):
187189

188190
return jsonify({"error": "Composer not found"}), 404
189191

190-
except Exception as e:
191-
print(f"Failed to get composer: {e}")
192+
except Exception:
193+
_logger.exception("Failed to get composer")
192194
return jsonify({"error": "Failed to get composer"}), 500

api/export_api.py

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
import os
1010
import re
1111
import sqlite3
12-
import sys
1312
import zipfile
1413
from contextlib import closing
1514
from datetime import datetime
@@ -18,7 +17,7 @@
1817
from flask import Blueprint, Response, current_app, jsonify, request
1918

2019
from utils.workspace_path import resolve_workspace_path
21-
from utils.path_helpers import normalize_file_path, get_workspace_folder_paths, to_epoch_ms
20+
from utils.path_helpers import get_workspace_folder_paths, to_epoch_ms
2221
from utils.text_extract import extract_text_from_bubble
2322
from utils.tool_parser import parse_tool_call
2423
from utils.exclusion_rules import build_searchable_text, is_excluded_by_rules
@@ -390,15 +389,15 @@ def export_chats():
390389
status_str = f" ({tool_status})" if tool_status else ""
391390
md += f"> **Tool: {tool_summary}**{status_str}\n"
392391
if t.get("input"):
393-
md += f">\n> **INPUT:**\n> ```\n"
392+
md += ">\n> **INPUT:**\n> ```\n"
394393
for iline in str(t["input"]).split("\n"):
395394
md += f"> {iline}\n"
396-
md += f"> ```\n"
395+
md += "> ```\n"
397396
if t.get("output"):
398-
md += f">\n> **OUTPUT:**\n> ```\n"
397+
md += ">\n> **OUTPUT:**\n> ```\n"
399398
for oline in str(t["output"]).split("\n"):
400399
md += f"> {oline}\n"
401-
md += f"> ```\n"
400+
md += "> ```\n"
402401
md += "\n"
403402
md += "---\n\n"
404403

api/logs.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
"""
55

66
import json
7+
import logging
78
import os
89
import re
910
import sqlite3
@@ -16,6 +17,7 @@
1617
from utils.path_helpers import to_epoch_ms
1718

1819
bp = Blueprint("logs", __name__)
20+
_logger = logging.getLogger(__name__)
1921

2022

2123
def _extract_chat_id_from_bubble_key(key: str) -> str | None:
@@ -69,8 +71,8 @@ def get_logs():
6971
"type": "chat",
7072
"messageCount": len(bubbles),
7173
})
72-
except Exception as e:
73-
print(f"Error reading global storage: {e}")
74+
except Exception:
75+
_logger.exception("Error reading global storage")
7476

7577
# Per-workspace (legacy)
7678
try:
@@ -133,9 +135,9 @@ def get_logs():
133135
except Exception:
134136
pass
135137

136-
logs.sort(key=lambda l: l.get("timestamp") or 0, reverse=True)
138+
logs.sort(key=lambda log: log.get("timestamp") or 0, reverse=True)
137139
return jsonify({"logs": logs})
138140

139-
except Exception as e:
140-
print(f"Failed to get logs: {e}")
141+
except Exception:
142+
_logger.exception("Failed to get logs")
141143
return jsonify({"error": "Failed to get logs", "logs": []}), 500

api/search.py

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
"""
55

66
import json
7+
import logging
78
import os
89
import re
910
import sqlite3
@@ -15,12 +16,13 @@
1516

1617
from utils.exclusion_rules import build_searchable_text, is_excluded_by_rules
1718
from utils.workspace_path import resolve_workspace_path, get_cli_chats_path
18-
from utils.path_helpers import normalize_file_path, get_workspace_folder_paths, to_epoch_ms
19+
from utils.path_helpers import to_epoch_ms
1920
from utils.text_extract import extract_text_from_bubble
2021
from utils.cli_chat_reader import list_cli_projects, traverse_blobs, messages_to_bubbles
2122
from models import Bubble, Composer, SchemaError
2223

2324
bp = Blueprint("search", __name__)
25+
_logger = logging.getLogger(__name__)
2426

2527

2628
def _json_dump_safe(value) -> str:
@@ -52,7 +54,7 @@ def _build_exclusion_searchable(
5254
metadata_parts: list[str] | None = None,
5355
) -> str:
5456
"""Build broad searchable text so exclusion rules cover visible output."""
55-
combined = []
57+
combined: list[str] = []
5658
if content_parts:
5759
combined.extend(p for p in content_parts if p)
5860
if metadata_parts:
@@ -243,7 +245,7 @@ def search():
243245
# Derive title from first bubble
244246
for text in bubble_texts:
245247
if text:
246-
first_lines = [l for l in text.split("\n") if l.strip()]
248+
first_lines = [ln for ln in text.split("\n") if ln.strip()]
247249
if first_lines:
248250
title = first_lines[0][:100]
249251
break
@@ -262,8 +264,8 @@ def search():
262264
except Exception:
263265
pass
264266

265-
except Exception as e:
266-
print(f"Error searching global storage: {e}")
267+
except Exception:
268+
_logger.exception("Error searching global storage")
267269
finally:
268270
if conn is not None:
269271
conn.close()
@@ -447,8 +449,8 @@ def search():
447449
"type": "cli_agent",
448450
"source": "cli",
449451
})
450-
except Exception as e:
451-
print(f"Error searching CLI sessions: {e}")
452+
except Exception:
453+
_logger.exception("Error searching CLI sessions")
452454

453455
# Sort by timestamp descending
454456
def _ts(r):
@@ -463,6 +465,6 @@ def _ts(r):
463465

464466
return jsonify({"results": results})
465467

466-
except Exception as e:
467-
print(f"Search failed: {e}")
468+
except Exception:
469+
_logger.exception("Search failed")
468470
return jsonify({"error": "Search failed", "results": []}), 500

api/workspaces.py

Lines changed: 19 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from __future__ import annotations
99

1010
import json
11+
import logging
1112
import os
1213
import re
1314
import sqlite3
@@ -32,10 +33,12 @@
3233
to_epoch_ms,
3334
)
3435
from utils.text_extract import extract_text_from_bubble, format_tool_action
36+
from utils.tool_parser import parse_tool_call as _parse_tool_call
3537
from utils.exclusion_rules import build_searchable_text, is_excluded_by_rules
3638
from models import Bubble, Composer, SchemaError, Workspace
3739

3840
bp = Blueprint("workspaces", __name__)
41+
_logger = logging.getLogger(__name__)
3942

4043

4144
def _get_workspace_display_name(workspace_path: str, workspace_id: str) -> str:
@@ -674,7 +677,6 @@ def list_workspaces():
674677
primary = group[0]
675678
all_ws_ids = [e["name"] for e in group]
676679

677-
db_path = os.path.join(workspace_path, primary["name"], "state.vscdb")
678680
try:
679681
mtime = max(
680682
os.path.getmtime(os.path.join(workspace_path, e["name"], "state.vscdb"))
@@ -769,14 +771,14 @@ def list_workspaces():
769771
),
770772
"source": "cli",
771773
})
772-
except Exception as e:
773-
print(f"Failed to load CLI projects: {e}")
774+
except Exception:
775+
_logger.exception("Failed to load CLI projects")
774776

775777
projects.sort(key=lambda p: p["lastModified"], reverse=True)
776778
return jsonify(projects)
777779

778-
except Exception as e:
779-
print(f"Failed to get workspaces: {e}")
780+
except Exception:
781+
_logger.exception("Failed to get workspaces")
780782
return jsonify({"error": "Failed to get workspaces"}), 500
781783

782784

@@ -850,16 +852,15 @@ def get_workspace(workspace_id):
850852
"lastModified": datetime.fromtimestamp(mtime, tz=timezone.utc).isoformat(),
851853
})
852854

853-
except Exception as e:
854-
print(f"Failed to get workspace: {e}")
855+
except Exception:
856+
_logger.exception("Failed to get workspace")
855857
return jsonify({"error": "Failed to get workspace"}), 500
856858

857859

858860
# ---------------------------------------------------------------------------
859861
# GET /api/workspaces/<id>/tabs
860862
# ---------------------------------------------------------------------------
861863

862-
from utils.tool_parser import parse_tool_call as _parse_tool_call
863864

864865

865866
def _get_cli_workspace_tabs(workspace_id: str):
@@ -883,8 +884,8 @@ def _get_cli_workspace_tabs(workspace_id: str):
883884

884885
try:
885886
messages = traverse_blobs(session["db_path"])
886-
except Exception as e:
887-
print(f"CLI: could not read session {session_id}: {e}")
887+
except Exception: # noqa: BLE001 — best-effort per-session skip; one corrupted session must not 500 the endpoint, and the failure mode is logged with exc_info so the concrete type is preserved.
888+
_logger.warning("CLI: could not read session %s", session_id, exc_info=True)
888889
continue
889890

890891
bubbles = messages_to_bubbles(messages, created_ms)
@@ -896,7 +897,7 @@ def _get_cli_workspace_tabs(workspace_id: str):
896897
if not title or title.startswith("New Agent"):
897898
for b in bubbles:
898899
if b["type"] == "user" and b.get("text"):
899-
first_lines = [l for l in b["text"].split("\n") if l.strip()]
900+
first_lines = [ln for ln in b["text"].split("\n") if ln.strip()]
900901
if first_lines:
901902
title = first_lines[0][:100]
902903
if len(title) == 100:
@@ -948,8 +949,8 @@ def _get_cli_workspace_tabs(workspace_id: str):
948949
tabs.sort(key=lambda t: t.get("timestamp") or 0, reverse=True)
949950
return jsonify({"tabs": tabs})
950951

951-
except Exception as e:
952-
print(f"Failed to get CLI workspace tabs: {e}")
952+
except Exception:
953+
_logger.exception("Failed to get CLI workspace tabs")
953954
return jsonify({"error": "Failed to get CLI workspace tabs"}), 500
954955

955956

@@ -1275,7 +1276,7 @@ def get_workspace_tabs(workspace_id):
12751276
if not cd.get("name") and bubbles:
12761277
first_msg = bubbles[0].get("text", "")
12771278
if first_msg:
1278-
first_lines = [l for l in first_msg.split("\n") if l.strip()]
1279+
first_lines = [ln for ln in first_msg.split("\n") if ln.strip()]
12791280
if first_lines:
12801281
title = first_lines[0][:100]
12811282
if len(title) == 100:
@@ -1418,14 +1419,14 @@ def get_workspace_tabs(workspace_id):
14181419

14191420
response["tabs"].append(tab)
14201421

1421-
except Exception as e:
1422-
print(f"Error parsing composer data for {composer_id}: {e}")
1422+
except Exception: # noqa: BLE001 — best-effort per-composer skip in a read-many loop; one malformed row must not 500 the tabs endpoint, and exc_info captures the concrete type for debugging.
1423+
_logger.warning("Error parsing composer data for %s", composer_id, exc_info=True)
14231424

14241425
# Sort tabs by timestamp descending (newest first)
14251426
response["tabs"].sort(key=lambda t: t.get("timestamp") or 0, reverse=True)
14261427

14271428
return jsonify(response)
14281429

1429-
except Exception as e:
1430-
print(f"Failed to get workspace tabs: {e}")
1430+
except Exception:
1431+
_logger.exception("Failed to get workspace tabs")
14311432
return jsonify({"error": "Failed to get workspace tabs"}), 500

app.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,6 @@ def favicon():
9090

9191
if __name__ == "__main__":
9292
import argparse
93-
import sys
9493

9594
parser = argparse.ArgumentParser(description="Cursor Chat Browser (Python)")
9695
parser.add_argument("--port", type=int, default=3000)

0 commit comments

Comments
 (0)