blackboard-cli 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. blackboard_cli-0.1.0/.claude/CLAUDE.md +81 -0
  2. blackboard_cli-0.1.0/.claude/hooks/README.md +47 -0
  3. blackboard_cli-0.1.0/.claude/hooks/scripts/guard-uv.sh +45 -0
  4. blackboard_cli-0.1.0/.claude/hooks/scripts/suggest-targeted-tests.sh +58 -0
  5. blackboard_cli-0.1.0/.claude/settings.json +30 -0
  6. blackboard_cli-0.1.0/.claude/settings.local.json +92 -0
  7. blackboard_cli-0.1.0/.github/workflows/ci.yml +33 -0
  8. blackboard_cli-0.1.0/.gitignore +22 -0
  9. blackboard_cli-0.1.0/CLAUDE.md +212 -0
  10. blackboard_cli-0.1.0/LICENSE +21 -0
  11. blackboard_cli-0.1.0/PKG-INFO +31 -0
  12. blackboard_cli-0.1.0/README.md +175 -0
  13. blackboard_cli-0.1.0/bb/__init__.py +1 -0
  14. blackboard_cli-0.1.0/bb/adapters/__init__.py +0 -0
  15. blackboard_cli-0.1.0/bb/adapters/base.py +56 -0
  16. blackboard_cli-0.1.0/bb/adapters/blackboard_ultra.py +692 -0
  17. blackboard_cli-0.1.0/bb/adapters/registry.py +42 -0
  18. blackboard_cli-0.1.0/bb/ai/__init__.py +0 -0
  19. blackboard_cli-0.1.0/bb/ai/chat.py +411 -0
  20. blackboard_cli-0.1.0/bb/ai/prompts.py +26 -0
  21. blackboard_cli-0.1.0/bb/ai/providers/__init__.py +0 -0
  22. blackboard_cli-0.1.0/bb/ai/providers/ollama.py +62 -0
  23. blackboard_cli-0.1.0/bb/auto_setup.py +240 -0
  24. blackboard_cli-0.1.0/bb/cache.py +74 -0
  25. blackboard_cli-0.1.0/bb/cli.py +867 -0
  26. blackboard_cli-0.1.0/bb/config.py +52 -0
  27. blackboard_cli-0.1.0/bb/db.py +361 -0
  28. blackboard_cli-0.1.0/bb/downloader.py +80 -0
  29. blackboard_cli-0.1.0/bb/hash.py +14 -0
  30. blackboard_cli-0.1.0/bb/mcp/__init__.py +0 -0
  31. blackboard_cli-0.1.0/bb/mcp/server.py +23 -0
  32. blackboard_cli-0.1.0/bb/models/__init__.py +0 -0
  33. blackboard_cli-0.1.0/bb/models/content.py +75 -0
  34. blackboard_cli-0.1.0/bb/notify/__init__.py +20 -0
  35. blackboard_cli-0.1.0/bb/notify/discord.py +30 -0
  36. blackboard_cli-0.1.0/bb/notify/ntfy.py +32 -0
  37. blackboard_cli-0.1.0/bb/notify/telegram.py +34 -0
  38. blackboard_cli-0.1.0/bb/notify/terminal.py +23 -0
  39. blackboard_cli-0.1.0/bb/parsers/__init__.py +0 -0
  40. blackboard_cli-0.1.0/bb/parsers/html_llm.py +71 -0
  41. blackboard_cli-0.1.0/bb/parsers/ical.py +88 -0
  42. blackboard_cli-0.1.0/bb/security/__init__.py +0 -0
  43. blackboard_cli-0.1.0/bb/security/session.py +138 -0
  44. blackboard_cli-0.1.0/bb/sync.py +121 -0
  45. blackboard_cli-0.1.0/bb/tools/__init__.py +40 -0
  46. blackboard_cli-0.1.0/bb/tools/ai_tools.py +203 -0
  47. blackboard_cli-0.1.0/bb/tools/queries.py +329 -0
  48. blackboard_cli-0.1.0/inspect_courses_page.py +40 -0
  49. blackboard_cli-0.1.0/pyproject.toml +64 -0
  50. blackboard_cli-0.1.0/selectors/.gitkeep +0 -0
  51. blackboard_cli-0.1.0/selectors/blackboard_ultra.toml +80 -0
  52. blackboard_cli-0.1.0/tests/__init__.py +0 -0
  53. blackboard_cli-0.1.0/tests/fixtures/activity_stream.html +111 -0
  54. blackboard_cli-0.1.0/tests/fixtures/sample.ics +53 -0
  55. blackboard_cli-0.1.0/tests/test_adapter_base.py +151 -0
  56. blackboard_cli-0.1.0/tests/test_ai_tools.py +237 -0
  57. blackboard_cli-0.1.0/tests/test_auto_setup.py +265 -0
  58. blackboard_cli-0.1.0/tests/test_blackboard_adapter.py +230 -0
  59. blackboard_cli-0.1.0/tests/test_cache.py +324 -0
  60. blackboard_cli-0.1.0/tests/test_chat.py +717 -0
  61. blackboard_cli-0.1.0/tests/test_cli.py +198 -0
  62. blackboard_cli-0.1.0/tests/test_cli_ann.py +201 -0
  63. blackboard_cli-0.1.0/tests/test_cli_auth_status.py +269 -0
  64. blackboard_cli-0.1.0/tests/test_cli_auto_setup.py +213 -0
  65. blackboard_cli-0.1.0/tests/test_cli_course.py +368 -0
  66. blackboard_cli-0.1.0/tests/test_cli_download.py +404 -0
  67. blackboard_cli-0.1.0/tests/test_cli_due.py +243 -0
  68. blackboard_cli-0.1.0/tests/test_cli_grades.py +220 -0
  69. blackboard_cli-0.1.0/tests/test_cli_import_ical.py +298 -0
  70. blackboard_cli-0.1.0/tests/test_config.py +129 -0
  71. blackboard_cli-0.1.0/tests/test_db.py +358 -0
  72. blackboard_cli-0.1.0/tests/test_db_downloads.py +94 -0
  73. blackboard_cli-0.1.0/tests/test_downloader.py +190 -0
  74. blackboard_cli-0.1.0/tests/test_hash.py +94 -0
  75. blackboard_cli-0.1.0/tests/test_html_llm.py +146 -0
  76. blackboard_cli-0.1.0/tests/test_ical_parser.py +248 -0
  77. blackboard_cli-0.1.0/tests/test_mcp_server.py +127 -0
  78. blackboard_cli-0.1.0/tests/test_models.py +384 -0
  79. blackboard_cli-0.1.0/tests/test_notifications.py +191 -0
  80. blackboard_cli-0.1.0/tests/test_notify.py +127 -0
  81. blackboard_cli-0.1.0/tests/test_notify_telegram_discord.py +100 -0
  82. blackboard_cli-0.1.0/tests/test_packaging.py +65 -0
  83. blackboard_cli-0.1.0/tests/test_registry.py +148 -0
  84. blackboard_cli-0.1.0/tests/test_session.py +287 -0
  85. blackboard_cli-0.1.0/tests/test_sync_flow.py +332 -0
  86. blackboard_cli-0.1.0/tests/test_tools.py +485 -0
  87. blackboard_cli-0.1.0/tests/test_tools_download.py +230 -0
  88. blackboard_cli-0.1.0/uv.lock +1585 -0
@@ -0,0 +1,81 @@
1
+ # CLAUDE.md
2
+
3
+ This file provides project-specific guidance for Claude Code when working in this repository.
4
+
5
+ ## Project Identity
6
+
7
+ `bb-cli` is a terminal-first Blackboard client for students. The current repository already implements the Day 1–9 foundation: CLI commands, SQLite persistence, iCal + Blackboard sync, course content caching, downloads, and an AI-facing query layer. The immediate delivery priority is Day 10: `bb chat`.
8
+
9
+ ## Current Delivery Phase
10
+
11
+ - Treat the repository state as implemented truth.
12
+ - Treat `PLAN.md` as delivery-priority truth.
13
+ - The current sprint is complete through Day 9.
14
+ - Prioritize Day 10–14 deliverables before v0.2 ideas.
15
+ - If a requested change is outside the current plan, note it as future work instead of silently expanding scope.
16
+
17
+ ## Non-Negotiable Rules
18
+
19
+ - Use `uv` for running, testing, and packaging workflows. Do not replace repo workflows with direct `pip` usage.
20
+ - Keep Blackboard data answers grounded in local data or tool results. Do not invent Blackboard facts.
21
+ - Keep selectors externalized in `selectors/blackboard_ultra.toml`. Do not hardcode fragile selectors into random call sites unless there is a strong reason.
22
+ - Prefer minimal-impact changes that fit the current architecture.
23
+ - When project brief, roadmap, and source diverge, trust the current repository state first.
24
+
25
+ ## Repo Navigation by Path
26
+
27
+ - `pyproject.toml` — package metadata, entrypoint, build config, dev tooling
28
+ - `bb/cli.py` — Typer command surface and user-facing orchestration
29
+ - `bb/db.py` — SQLite schema, migrations, persistence helpers, sync log, downloads
30
+ - `bb/sync.py` — iCal retry flow, activity-stream sync, grade sync, snapshot save
31
+ - `bb/adapters/blackboard_ultra.py` — Blackboard auth, session restore, scraping, selector-driven parsing, content-tree traversal
32
+ - `selectors/blackboard_ultra.toml` — runtime selector source for Blackboard scraping
33
+ - `bb/tools/queries.py` — AI-facing query surface returning JSON-friendly results
34
+ - `bb/models/content.py` — content tree dataclasses and serialization helpers
35
+
36
+ ## Change Rules by Lane
37
+
38
+ ### CLI and command behavior
39
+ Start in `bb/cli.py`. Check whether the change also affects persistence in `bb/db.py`, sync behavior in `bb/sync.py`, or cached content behavior.
40
+
41
+ ### Database, schema, or persistence
42
+ Start in `bb/db.py`. Review migrations, query helpers, and all callers in `bb/cli.py` and `bb/tools/queries.py`.
43
+
44
+ ### Blackboard scraping or selector breakage
45
+ Inspect `bb/adapters/blackboard_ultra.py` and `selectors/blackboard_ultra.toml` together. Prefer selector fixes before broad parser rewrites. Check `bb/sync.py` snapshot behavior when stream scraping returns zero items.
46
+
47
+ ### AI-facing data access and future chat behavior
48
+ Start in `bb/tools/queries.py`. Ensure outputs stay JSON-serializable, missing DB/cache states are handled gracefully, and response logic stays grounded in actual data.
49
+
50
+ ### Course content, cache, and downloads
51
+ Inspect `bb/models/content.py`, the course/download commands in `bb/cli.py`, and the Blackboard content scraping logic in `bb/adapters/blackboard_ultra.py`.
52
+
53
+ ## Verification Rules
54
+
55
+ - CLI changes: verify the affected command flow and any impacted tables or cache behavior.
56
+ - DB changes: verify schema compatibility, caller expectations, and regression risk.
57
+ - Scraping changes: verify the selector path, fallback behavior, and minimum viable fix.
58
+ - Tool-layer changes: verify JSON shape, empty-state behavior, and alignment with the underlying database or cached files.
59
+ - Sprint-facing AI/chat changes: prefer extending the tool surface before compensating with prompt-only behavior.
60
+
61
+ ## Skills and Subagents
62
+
63
+ Use `.claude/context/` for project memory, `.claude/skills/` for repeatable workflows, and `.claude/agents/` for specialized review or investigation once those are added. Keep this file constitutional and path-aware; move long checklists and playbooks into skills or context files.
64
+
65
+ ## Git Rules
66
+
67
+ - **NEVER commit `docs/` or `tasks/`** — internal planning files (specs, plans, brainstorming). They are in `.gitignore`. If `git add` warns "ignored file", stop immediately — never force-add.
68
+ - **`git filter-repo` removes remote** — after running it, always re-add: `git remote add origin git@github.com:Bruce1508/bb-cli.git`
69
+ - **Force push only after intentional history rewrite** — use `git push --force origin main` only after `git filter-repo`. Normal pushes use `--force-with-lease`.
70
+
71
+ ## Python / Testing Gotchas (learned Day 8–9)
72
+
73
+ - **`pdfplumber` pages outside `with` block** — `pdf.pages` raises after context exits. Always capture `page_count = len(pdf.pages)` INSIDE `with pdfplumber.open() as pdf:`.
74
+ - **`iterdir()` on missing dir** — `Path.iterdir()` raises `FileNotFoundError` if dir doesn't exist. Guard: `if not path.exists(): return ...` before iterating.
75
+ - **`httpx.stream()` needs method arg** — `client.stream("GET", url)`, not `client.stream(url)`. Also: `Content-Length` is a string — cast: `int(response.headers.get("content-length", 0))`.
76
+ - **`patch()` needs module-level import** — `patch("bb.tools.queries.pdfplumber.open")` requires `import pdfplumber` at module level. Lazy imports inside functions don't exist as attributes at patch time.
77
+ - **`or` vs `is not None`** — `x or default` treats empty list/string/0 as falsy. Use `x if x is not None else default` when "provided but empty" is a valid value.
78
+ - **Library classes must not have UI deps** — never put `typer.confirm` / `input()` inside library classes (`Downloader`, etc.). That's the CLI layer's responsibility; mixing them breaks non-TTY usage and testing.
79
+ - **Partial file cleanup on failure** — on download error, always `dest_path.unlink(missing_ok=True)` in except clauses so corrupt partial files don't accumulate silently.
80
+ - **`uv pip install -e .` .pth bug** — hatchling generates `_bb_cli.pth` without trailing newline; Python skips it. Fix: `printf '/path/to/bb-cli\n' > .venv/lib/python3.13/site-packages/_bb_cli.pth`.
81
+ - **`webbrowser.open(None)`** — silently opens `"None"` as a URL. Always guard: `if item.url is None: raise typer.Exit(1)` before calling.
@@ -0,0 +1,47 @@
1
+ # Hooks
2
+
3
+ This directory contains project-specific hook scripts for `bb-cli`.
4
+
5
+ ## Scope
6
+
7
+ These scripts are wired through the project settings file:
8
+
9
+ - `.claude/settings.json`
10
+
11
+ This follows Claude Code's project-level hook model, where hooks declared in `.claude/settings.json` apply to the current repository.
12
+
13
+ ## Hook scripts
14
+
15
+ ### `scripts/guard-uv.sh`
16
+
17
+ Runs before Bash tool calls and checks for commands that drift from repository conventions.
18
+
19
+ Current behavior:
20
+ - flags direct `pip` usage
21
+ - flags `python -m pip`
22
+ - flags bare `pytest` when it is not invoked through `uv run`
23
+
24
+ The hook does not silently block the command. Instead, it returns a `PreToolUse` decision of `ask`, which surfaces the repository convention and lets the user confirm intentionally.
25
+
26
+ ### `scripts/suggest-targeted-tests.sh`
27
+
28
+ Runs after successful `Edit` or `Write` tool calls and suggests small, relevant checks based on which paths were changed.
29
+
30
+ Current path groups:
31
+ - DB and tool layer: `bb/db.py`, `bb/tools/queries.py`, `bb/models/content.py`
32
+ - scraping lane: `bb/adapters/blackboard_ultra.py`, `selectors/blackboard_ultra.toml`, `bb/sync.py`
33
+ - CLI surface: `bb/cli.py`
34
+ - packaging: `pyproject.toml`
35
+
36
+ The hook adds repo-specific follow-up context rather than trying to run tests automatically.
37
+
38
+ ## Design intent
39
+
40
+ These hooks are deliberately lightweight guardrails.
41
+
42
+ They are meant to:
43
+ - reinforce repo conventions
44
+ - reduce avoidable workflow drift
45
+ - suggest the smallest relevant verification step
46
+
47
+ They are not meant to become a heavy automation layer that interrupts normal work unnecessarily.
@@ -0,0 +1,45 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ if [[ $# -gt 0 && -n "${1:-}" ]]; then
5
+ INPUT="$1"
6
+ elif [[ -n "${CLAUDE_HOOK_INPUT:-}" ]]; then
7
+ INPUT="$CLAUDE_HOOK_INPUT"
8
+ else
9
+ INPUT="$(cat)"
10
+ fi
11
+
12
+ if [[ -z "$INPUT" ]]; then
13
+ exit 0
14
+ fi
15
+
16
+ MESSAGE=""
17
+ ADDITIONAL=""
18
+
19
+ if echo "$INPUT" | grep -Eiq '"command"[[:space:]]*:[[:space:]]*"[^"]*python[[:space:]]+-m[[:space:]]+pip'; then
20
+ MESSAGE="Prefer uv-based workflows over python -m pip."
21
+ ADDITIONAL="This repository uses uv-based install and execution flows. Prefer commands like 'uv pip install -e .' or 'uv run ...' instead of python -m pip."
22
+ elif echo "$INPUT" | grep -Eiq '"command"[[:space:]]*:[[:space:]]*"[^"]*(^|[^[:alnum:]_])pip([[:space:]]|$)'; then
23
+ MESSAGE="Prefer uv-based workflows over direct pip usage."
24
+ ADDITIONAL="This repository uses uv-based install and execution flows. Prefer commands like 'uv pip install -e .' or 'uv run ...' instead of direct pip usage."
25
+ elif echo "$INPUT" | grep -Eiq '"command"[[:space:]]*:[[:space:]]*"[^"]*(^|[^[:alnum:]_])pytest([[:space:]]|$)' && ! echo "$INPUT" | grep -Eiq 'uv[[:space:]]+run[[:space:]]+pytest'; then
26
+ MESSAGE="Prefer running tests through uv."
27
+ ADDITIONAL="Repository convention is to run tests through uv, for example 'uv run pytest tests/ -v'."
28
+ fi
29
+
30
+ if [[ -z "$MESSAGE" ]]; then
31
+ exit 0
32
+ fi
33
+
34
+ cat <<EOF
35
+ {
36
+ "hookSpecificOutput": {
37
+ "hookEventName": "PreToolUse",
38
+ "permissionDecision": "ask",
39
+ "permissionDecisionReason": "$MESSAGE",
40
+ "additionalContext": "$ADDITIONAL"
41
+ }
42
+ }
43
+ EOF
44
+
45
+ exit 0
@@ -0,0 +1,58 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ if [[ $# -gt 0 && -n "${1:-}" ]]; then
5
+ INPUT="$1"
6
+ elif [[ -n "${CLAUDE_HOOK_INPUT:-}" ]]; then
7
+ INPUT="$CLAUDE_HOOK_INPUT"
8
+ else
9
+ INPUT="$(cat)"
10
+ fi
11
+
12
+ if [[ -z "$INPUT" ]]; then
13
+ exit 0
14
+ fi
15
+
16
+ suggestions=()
17
+
18
+ if echo "$INPUT" | grep -Eiq 'bb/db\.py|bb/tools/queries\.py|bb/models/content\.py'; then
19
+ suggestions+=("uv run pytest tests/ -v")
20
+ fi
21
+
22
+ if echo "$INPUT" | grep -Eiq 'bb/adapters/blackboard_ultra\.py|selectors/blackboard_ultra\.toml|bb/sync\.py'; then
23
+ suggestions+=("Check scraping-related tests or add a narrow regression test around the changed adapter or selector lane.")
24
+ fi
25
+
26
+ if echo "$INPUT" | grep -Eiq 'bb/cli\.py'; then
27
+ suggestions+=("Sanity-check the affected command flow with uv run bb <command>.")
28
+ fi
29
+
30
+ if echo "$INPUT" | grep -Eiq 'pyproject\.toml'; then
31
+ suggestions+=("Verify package and CLI sanity with uv build and a quick uv run bb <command> check.")
32
+ fi
33
+
34
+ if [[ ${#suggestions[@]} -eq 0 ]]; then
35
+ exit 0
36
+ fi
37
+
38
+ json_array=""
39
+ for s in "${suggestions[@]}"; do
40
+ escaped=$(printf '%s' "$s" | sed 's/\\/\\\\/g; s/"/\\"/g')
41
+ if [[ -n "$json_array" ]]; then
42
+ json_array+="\\n- ${escaped}"
43
+ else
44
+ json_array+="- ${escaped}"
45
+ fi
46
+ done
47
+
48
+ cat <<EOF
49
+ {
50
+ "hookSpecificOutput": {
51
+ "hookEventName": "PostToolUse",
52
+ "additionalContext": "Suggested targeted verification:\n${json_array}"
53
+ },
54
+ "suppressOutput": true
55
+ }
56
+ EOF
57
+
58
+ exit 0
@@ -0,0 +1,30 @@
1
+ {
2
+ "hooks": {
3
+ "PreToolUse": [
4
+ {
5
+ "matcher": "Bash",
6
+ "hooks": [
7
+ {
8
+ "type": "command",
9
+ "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/scripts/guard-uv.sh",
10
+ "timeout": 10,
11
+ "statusMessage": "Checking bb-cli shell workflow conventions"
12
+ }
13
+ ]
14
+ }
15
+ ],
16
+ "PostToolUse": [
17
+ {
18
+ "matcher": "Edit|Write",
19
+ "hooks": [
20
+ {
21
+ "type": "command",
22
+ "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/scripts/suggest-targeted-tests.sh",
23
+ "timeout": 10,
24
+ "statusMessage": "Suggesting targeted verification for bb-cli"
25
+ }
26
+ ]
27
+ }
28
+ ]
29
+ }
30
+ }
@@ -0,0 +1,92 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(/opt/homebrew/bin/uv run:*)",
5
+ "Bash(uv run:*)",
6
+ "Bash(echo \"EXIT: $?\")",
7
+ "Bash(git add:*)",
8
+ "Bash(git commit:*)",
9
+ "Bash(uv pip:*)",
10
+ "mcp__plugin_context7_context7__resolve-library-id",
11
+ "Bash(git remote:*)",
12
+ "Bash(pip show:*)",
13
+ "Bash(pip3 install:*)",
14
+ "Bash(git push:*)",
15
+ "Bash(grep:*)",
16
+ "Bash(sed -i '' 's/patch\\(\"\"bb\\\\.cli\\\\.notify\"\"\\)/patch\\(\"\"bb.cli.dispatch_notify\"\"\\)/g' /Users/brucevo/Desktop/bb-cli/tests/test_cli_import_ical.py)",
17
+ "Bash(sed -i '' 's/Mock bb\\\\.cli\\\\.notify/Mock bb.cli.dispatch_notify/' /Users/brucevo/Desktop/bb-cli/tests/test_cli_import_ical.py)",
18
+ "Bash(sw_vers -productVersion)",
19
+ "Read(//Users/brucevo/Library/LaunchAgents/**)",
20
+ "mcp__plugin_context7_context7__query-docs",
21
+ "Bash(/Users/brucevo/Desktop/bb-cli/.venv/bin/python3 -c \"import bb; print\\(bb.__file__\\)\" 2>&1)",
22
+ "Bash(/Users/brucevo/Desktop/bb-cli/.venv/bin/python3 /Users/brucevo/Desktop/bb-cli/.venv/bin/bb auto-setup --help 2>&1)",
23
+ "Bash(/Users/brucevo/Desktop/bb-cli/.venv/bin/python3 -c \"import sys; print\\(sys.path\\)\")",
24
+ "Bash(git:*)",
25
+ "Bash(grep -v \"^$\")",
26
+ "Bash(ls /Users/brucevo/Desktop/bb-cli/.venv/lib/python*/site-packages/)",
27
+ "Bash(python3 -m site --user-site)",
28
+ "Bash(/Users/brucevo/Desktop/bb-cli/.venv/bin/python3 -c \":*)",
29
+ "Bash(.venv/bin/python -c \"import site; print\\(site.getusersitepackages\\(\\)\\); print\\(site.getsitepackages\\(\\)\\)\")",
30
+ "Bash(xxd)",
31
+ "Bash(printf /Users/brucevo/Desktop/bb-clin cat /Users/brucevo/Desktop/bb-cli/.venv/lib/python3.13/site-packages/_bb_cli.pth)",
32
+ "Bash(uv sync:*)",
33
+ "Bash(printf '/Users/brucevo/Desktop/bb-cli\\\\n')",
34
+ "Bash(git-filter-repo --version)",
35
+ "Skill(code-review:code-review)",
36
+ "Bash(.venv/bin/bb status:*)",
37
+ "Bash(cat .venv/lib/python*/site-packages/bb*.pth)",
38
+ "Bash(ls .venv/lib/python*/site-packages/)",
39
+ "Bash(.venv/bin/bb download:*)",
40
+ "Bash(.venv/bin/bb open:*)",
41
+ "Bash(PYTHONPATH=/Users/brucevo/Desktop/bb-cli .venv/bin/bb status 2>&1)",
42
+ "Bash(PYTHONPATH=/Users/brucevo/Desktop/bb-cli .venv/bin/bb --help)",
43
+ "Bash(.venv/bin/python -c \"import site; site.addsitedir\\('.venv/lib/python3.13/site-packages'\\); import bb; print\\(bb.__file__\\)\" 2>&1)",
44
+ "Bash(.venv/bin/python -c \"import sys; print\\(sys.path\\)\")",
45
+ "Bash(.venv/bin/python -c \"import bb; print\\(bb.__file__\\)\" 2>&1)",
46
+ "Read(//opt/homebrew/Cellar/python@3.13/3.13.7/Frameworks/Python.framework/Versions/3.13/bin/**)",
47
+ "Bash(/Users/brucevo/Desktop/bb-cli/.venv/bin/python3 /Users/brucevo/Desktop/bb-cli/.venv/bin/bb status 2>&1)",
48
+ "Bash(/Users/brucevo/Desktop/bb-cli/.venv/bin/python3 -c \"\nimport site\nimport sys\nprint\\('site-packages:', site.getsitepackages\\(\\)\\)\nprint\\('/Users/brucevo/Desktop/bb-cli' in sys.path\\)\n\")",
49
+ "Bash(uv venv:*)",
50
+ "Bash(printf n:*)",
51
+ "Bash(printf '%s\\\\n' '/Users/brucevo/Desktop/bb-cli')",
52
+ "Bash(python3:*)",
53
+ "Bash(.venv/bin/python -c \"from bb.cli import app; print\\('ok'\\)\" 2>&1)",
54
+ "Bash(/Users/brucevo/Desktop/bb-cli/.venv/bin/python3 -c \"import sys; print\\([p for p in sys.path if 'bb' in p.lower\\(\\)]\\)\")",
55
+ "Bash(/Users/brucevo/Desktop/bb-cli/.venv/bin/python3 -c \"import site; print\\(site.getusersitepackages\\(\\)\\); print\\(site.getsitepackages\\(\\)\\)\")",
56
+ "Bash(sqlite3 ~/.bb/bb.db \"DELETE FROM deadlines; DELETE FROM sync_log;\")",
57
+ "Bash(sqlite3:*)",
58
+ "Bash(find /Users/brucevo/Desktop/bb-cli/bb/adapters/__pycache__ -name \"*.pyc\" -delete)",
59
+ "Bash(printf /Users/brucevo/Desktop/bb-clin)",
60
+ "Bash(/Users/brucevo/Desktop/bb-cli/.venv/bin/python3 -c \"import sys; print\\([p for p in sys.path if 'bb-cli' in p]\\)\")",
61
+ "Bash(/Users/brucevo/Desktop/bb-cli/.venv/bin/python3 -c \"import site; site.addsitedir\\('/Users/brucevo/Desktop/bb-cli/.venv/lib/python3.13/site-packages'\\); import sys; print\\([p for p in sys.path if 'bb-cli' in p or 'Desktop' in p]\\)\")",
62
+ "Bash(ls:*)",
63
+ "Bash(/Users/brucevo/Desktop/bb-cli/.venv/bin/python3 -S -c \"import sys; print\\(sys.path\\)\")",
64
+ "Bash(find /Users/brucevo/Desktop/bb-cli/bb -name \"*.pyc\" -delete)",
65
+ "Bash(find /Users/brucevo/Desktop/bb-cli/bb -name \"__pycache__\" -type d -exec rm -rf {} +)",
66
+ "Bash(/Users/brucevo/Desktop/bb-cli/.worktrees/day10-bb-chat/.venv/bin/python -c \"import bb; print\\(bb.__file__\\)\")",
67
+ "Bash(/Users/brucevo/Desktop/bb-cli/.worktrees/day10-bb-chat/.venv/bin/python -c \"from bb.cli import app; cmds = [c.name for c in app.registered_commands]; print\\(cmds\\)\")",
68
+ "Bash(/Users/brucevo/Desktop/bb-cli/.worktrees/day10-bb-chat/.venv/bin/bb chat:*)",
69
+ "Bash(xxd:*)",
70
+ "Bash(/Users/brucevo/Desktop/bb-cli/.worktrees/day10-bb-chat/.venv/bin/python3 -c \"import sys; print\\([p for p in sys.path if 'bb' in p or 'day10' in p]\\)\")",
71
+ "Bash(/Users/brucevo/Desktop/bb-cli/inspect_courses_page.py:*)",
72
+ "Bash(echo \"/clear:*)",
73
+ "Bash(uv add:*)",
74
+ "Bash(pip index:*)",
75
+ "Bash(pip install:*)",
76
+ "Bash(curl -s \"https://pypi.org/pypi/mcp/json\")",
77
+ "Bash(curl -s https://pypi.org/pypi/mcp/json)",
78
+ "Bash(gtimeout 2 uv run bb mcp-server)",
79
+ "Bash(echo \"exit: $?\")",
80
+ "Bash(wc -l tests/*.py)",
81
+ "Bash(uv build:*)",
82
+ "Bash(source /tmp/bb-test/bin/activate)",
83
+ "Bash(bb version:*)",
84
+ "Bash(bb --help)",
85
+ "Bash(deactivate)",
86
+ "Bash(rm -rf /tmp/bb-test)",
87
+ "Bash(curl -s -o /dev/null -w \"%{http_code}\" https://pypi.org/pypi/blackboard-cli/json)",
88
+ "Bash(curl -s -o /dev/null -w \"%{http_code}\" https://pypi.org/pypi/bbcli/json)",
89
+ "Bash(curl -s -o /dev/null -w \"%{http_code}\" https://pypi.org/pypi/bb-student/json)"
90
+ ]
91
+ }
92
+ }
@@ -0,0 +1,33 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ jobs:
10
+ ci:
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ - uses: actions/checkout@v4
14
+
15
+ - name: Install uv
16
+ uses: astral-sh/setup-uv@v4
17
+ with:
18
+ version: "latest"
19
+
20
+ - name: Set up Python
21
+ run: uv python install 3.11
22
+
23
+ - name: Install dependencies
24
+ run: uv sync --all-groups
25
+
26
+ - name: Lint
27
+ run: uv run ruff check bb/
28
+
29
+ - name: Test
30
+ run: uv run pytest tests/ -q
31
+
32
+ - name: Build
33
+ run: uv build
@@ -0,0 +1,22 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.pyc
4
+ *.pyo
5
+ .pytest_cache/
6
+ .ruff_cache/
7
+ dist/
8
+ *.egg-info/
9
+ .coverage
10
+ htmlcov/
11
+ # uv.lock intentionally tracked for reproducible CI installs
12
+ **/.DS_Store
13
+ # Note: ~/.bb/ is outside the repo — no gitignore entry needed
14
+
15
+ # Keep internal docs/plans private — only CLAUDE.md and README.md are public
16
+ *.md
17
+ !CLAUDE.md
18
+ !README.md
19
+
20
+ # NEVER commit internal planning docs — specs, plans, brainstorming notes
21
+ docs/
22
+ tasks/
@@ -0,0 +1,212 @@
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+ ## Project Overview
6
+
7
+ `bb-cli` is "Claude Code for Blackboard" — a terminal-first tool that lets college students manage their entire school life without opening a browser. Core features: sync deadlines/grades/announcements from Blackboard Ultra, browse/download course content, and **`bb chat` — an interactive AI assistant** that answers questions using real Blackboard data. Think of it as giving students a personal AI that actually knows their courses, deadlines, and grades.
8
+
9
+ Target distribution: PyPI package `bb-cli`. Blackboard Ultra first, plugin architecture for Canvas/Moodle/Brightspace later.
10
+
11
+ ## Package Manager & Commands
12
+
13
+ This project uses `uv` exclusively — do not use `pip` directly.
14
+
15
+ ```bash
16
+ # Run CLI commands
17
+ uv run bb <command>
18
+
19
+ # Run tests
20
+ uv run pytest tests/ -v
21
+ uv run pytest tests/test_db.py -v # single test file
22
+ uv run pytest --cov=bb --cov-report=term-missing # with coverage
23
+
24
+ # Watch mode during development
25
+ uv run ptw
26
+
27
+ # Linting and formatting
28
+ uv run ruff check bb/
29
+ uv run ruff format bb/
30
+
31
+ # Build package
32
+ uv build
33
+
34
+ # Install from local source
35
+ uv pip install -e .
36
+
37
+ # Start MCP server (for Claude Desktop integration)
38
+ uv run bb mcp-server
39
+ ```
40
+
41
+ ## Architecture
42
+
43
+ ### Core Data Flow
44
+
45
+ ```
46
+ bb sync
47
+ → Phase 1: iCal feed (no auth) via httpx
48
+ → Phase 2: Activity Stream scraping via Playwright (headed Chromium)
49
+ → Parse → upsert to SQLite (~/.bb/bb.db)
50
+ → Notify via configured provider(s)
51
+
52
+ bb chat
53
+ → User types natural language query
54
+ → LLM decides which tool functions to call
55
+ → Tool functions query local DB / read cached files / trigger sync
56
+ → LLM formats response with real data
57
+ → Never hallucinate — always ground answers in tool results
58
+
59
+ bb mcp-server
60
+ → Exposes tool functions as MCP protocol tools
61
+ → Claude Desktop / Cursor / any MCP client can connect
62
+ → Student asks Claude "what are my deadlines?" → Claude calls bb-cli tools → real data
63
+ ```
64
+
65
+ ### Project Structure
66
+
67
+ ```
68
+ bb/
69
+ cli.py # Typer app — all command definitions
70
+ config.py # TOML config at ~/.bb/config.toml (Pydantic validation)
71
+ db.py # SQLite (WAL mode) — CRUD + version-based migrations
72
+ sync.py # Orchestrates both sync phases
73
+ hash.py # Content hashing utilities for dedup
74
+ logger.py # Structured logging with Rich handler
75
+ adapters/
76
+ base.py # LMSAdapter ABC — defines interface all adapters must implement
77
+ registry.py # Decorator-based adapter registration + discovery
78
+ blackboard_ultra.py # Playwright scraping: auth, activity stream, grades, content
79
+ parsers/
80
+ ical.py # .ics → list[Deadline], UTC storage / Toronto display
81
+ html_llm.py # LLM-based HTML parser — fallback when CSS selectors fail
82
+ security/
83
+ session.py # Fernet-encrypted session files; keyring for key storage
84
+ notify/
85
+ base.py # Notifier ABC
86
+ terminal.py # OS-native notifications (osascript / notify-send)
87
+ ntfy.py # ntfy.sh push (zero account required)
88
+ telegram.py # Telegram Bot API
89
+ discord.py # Discord webhook
90
+ tools/
91
+ __init__.py # TOOL_REGISTRY — central dict of all callable tool functions
92
+ queries.py # DB query tools: deadlines, grades, announcements, courses
93
+ ai_tools.py # AI-powered tools: summarize, study plan, key concepts
94
+ ai/
95
+ chat.py # Chat engine: REPL loop, tool calling orchestration, streaming
96
+ prompts.py # System prompt for student assistant context
97
+ providers/
98
+ ollama.py # Ollama local LLM integration (free, private, offline)
99
+ api.py # Claude API / OpenAI API fallback
100
+ mcp/
101
+ server.py # MCP server exposing tool functions for Claude Desktop
102
+ models/
103
+ content.py # ContentItem, ContentTree dataclasses for course browser
104
+ cache.py # TTL-based content cache management
105
+ selectors/
106
+ blackboard_ultra.toml # CSS selectors loaded at runtime — NOT hardcoded
107
+ tests/
108
+ fixtures/
109
+ sample.ics
110
+ activity_stream.html # Saved real HTML for offline testing
111
+ ```
112
+
113
+ ### Key Design Decisions
114
+
115
+ **`bb chat` is the killer feature.** Everything else (sync, due, grades) is foundation that chat builds on. The tool function layer (`bb/tools/`) is the bridge: CLI commands and chat both call the same functions, just with different interfaces.
116
+
117
+ **Tool function architecture.** Every data query is a tool function in `bb/tools/queries.py` with: clear docstring (LLM reads this), Pydantic input/output models, and direct SQLite queries. The LLM calls these tools — it never makes up data. Tool functions are shared between `bb chat` (Ollama/API calls them) and `bb mcp-server` (Claude Desktop calls them via MCP protocol).
118
+
119
+ **AI provider priority.** `bb chat` auto-detects: Ollama running locally → use it (free, private, offline). No Ollama → check config for Claude/OpenAI API key → use that. No AI at all → graceful message suggesting install Ollama. Config: `[ai] provider = "ollama" | "claude" | "openai"` in `~/.bb/config.toml`.
120
+
121
+ **MCP server as Claude Desktop bridge.** `bb mcp-server` starts a stdio MCP server that exposes all tool functions. Students add it to Claude Desktop config → can ask Claude about their Blackboard data in natural language. This means bb-cli works both standalone (via `bb chat` with Ollama) and as a plugin for Claude Desktop.
122
+
123
+ **Selector externalization.** All Playwright CSS selectors live in `selectors/blackboard_ultra.toml`. The adapter reads them at runtime. This allows selector updates without code changes when Blackboard modifies its UI.
124
+
125
+ **Selector resilience.** Primary selector → fallback selector → LLM extraction (`bb/parsers/html_llm.py` using Ollama locally). Circuit breaker: max 20 pages per sync.
126
+
127
+ **Session encryption.** Playwright `storage_state()` is encrypted with Fernet symmetric encryption. Key stored in OS keyring; falls back to password-based derivation when keyring is unavailable.
128
+
129
+ **Session 3-tier check.** fresh (< threshold, skip verify) / uncertain (verify with headless browser) / expired (assume dead) — ported from SkipClass Pro's `smart_session_manager.py`.
130
+
131
+ **Cache layer.** Course content trees cached at `~/.bb/cache/<COURSE>/tree.json` with 2h TTL. Downloads at `~/.bb/files/<COURSE>/`. Study packs at `~/.bb/study/<COURSE>/`.
132
+
133
+ ### Data Models
134
+
135
+ Defined in `bb/adapters/base.py`:
136
+ - `Deadline` — course, title, due_at (UTC), source (ical/stream/api)
137
+ - `Announcement` — course, title, body_preview, posted_at, is_read
138
+ - `GradeItem` — course, item, score, out_of, status (pending/submitted/graded)
139
+
140
+ Content models in `bb/models/content.py`:
141
+ - `ContentItem` — type (module/file/folder/discussion/link), title, url, download_url, children, size_bytes, mime_type
142
+ - `ContentTree` — course, scraped_at, items list
143
+
144
+ Tool output models in `bb/tools/`:
145
+ - All tool functions return plain dicts or Pydantic models serializable to JSON
146
+ - LLM receives tool results as JSON → formats human-readable response
147
+
148
+ ### Notification Rules
149
+
150
+ Configured in `~/.bb/config.toml`. Default: new deadline → normal priority; deadline <24h → high; new grade → high; session expired → high. Dedup via `notified_at` field in DB.
151
+
152
+ ### Automation
153
+
154
+ `bb auto-setup` installs OS-native scheduler:
155
+ - macOS: launchd plist at `~/Library/LaunchAgents/com.bb-cli.sync.plist`
156
+ - Linux: crontab entry
157
+ - Default interval: every 4 hours, runs `bb sync` silently
158
+
159
+ ## CLI Commands
160
+
161
+ | Command | Description |
162
+ |---------|-------------|
163
+ | `bb init` | Interactive wizard — LMS URL, notification method, AI provider, saves `~/.bb/config.toml` |
164
+ | `bb auth` | Opens headed Chromium for login + MFA, saves encrypted session |
165
+ | `bb sync` | Full sync (iCal + Activity Stream); `--ical-only`, `--dry-run` flags |
166
+ | `bb due` | Upcoming deadlines table; `--days N`, `--course`, `--all`, `--json` |
167
+ | `bb ann` | Recent announcements; `--unread` filter |
168
+ | `bb grades` | Grade table; `--course` filter |
169
+ | `bb status` | Session health, last sync time, DB stats |
170
+ | `bb chat` | **Interactive AI assistant** — ask anything about your courses in natural language |
171
+ | `bb chat "query"` | Single-shot mode — ask one question, get answer, exit |
172
+ | `bb <COURSE>` | Interactive course content browser (e.g., `bb BTP200`); `--tree`, `--refresh` |
173
+ | `bb download <COURSE>` | File downloader; `--all`, `--type pdf` flags |
174
+ | `bb open <COURSE> <item>` | Open non-downloadable items in browser |
175
+ | `bb mcp-server` | Start MCP server for Claude Desktop / Cursor integration |
176
+ | `bb auto-setup` | Install OS scheduler for automatic syncing; `--disable` to stop |
177
+ | `bb cache clear` | Clear content cache |
178
+ | `bb setup-browsers` | Run `playwright install chromium` |
179
+
180
+ ### `bb chat` Special Commands
181
+
182
+ Inside the chat REPL, these slash commands are available:
183
+ - `/exit` or `/quit` — exit chat
184
+ - `/clear` — clear conversation history
185
+ - `/sync` — trigger `bb sync` without leaving chat
186
+ - `/courses` — list all courses
187
+ - `/help` — show available commands
188
+
189
+ ## Testing Approach
190
+
191
+ - Use real in-memory SQLite (not mocks) for DB tests
192
+ - Save real Blackboard HTML as fixtures for offline adapter testing
193
+ - Mock Playwright for integration tests
194
+ - Mock Ollama for chat tests — test tool routing + response format
195
+ - Test tool functions independently with fixture data in SQLite
196
+ - Test MCP server for protocol compliance
197
+ - Target: >70% coverage before PyPI release
198
+ - Typer app must have 2+ commands — single-command apps collapse into standalone mode, breaking `runner.invoke(app, ["subcommand"])` in tests
199
+ - Patch `BB_DIR` in tests via `unittest.mock.patch("bb.config.BB_DIR", tmp_path)` and `patch("bb.db.BB_DIR", tmp_path)` — `Database.__init__` uses `path=None` pattern so `BB_DIR` is resolved at call time, not import time
200
+ - WAL pragma does nothing on `:memory:` — test WAL mode with a real `tempfile.NamedTemporaryFile`
201
+
202
+ ## Gotchas & Environment Notes
203
+
204
+ - **Hatchling package discovery**: `pyproject.toml` requires `[tool.hatch.build.targets.wheel] packages = ["bb"]` — hatchling cannot auto-discover `bb` from the hyphenated project name `bb-cli`
205
+ - **uv dev deps syntax**: Use `[dependency-groups] dev = [...]` (PEP 735), NOT `[tool.uv] dev-dependencies` — the latter is deprecated and will break in future uv versions
206
+ - **SQLite migrations**: Use `connection.executescript()` (not `execute()`) for multi-statement SQL migration strings — handles multiple statements and auto-commits
207
+ - **CLI not found after uv sync**: If `uv run bb` gives `ModuleNotFoundError`, run `uv pip install -e .` to force editable install registration
208
+ - **`git filter-repo` removes remote**: After running `git filter-repo`, remote is deleted — re-add with `git remote add origin <url>` before force pushing
209
+ - **Ollama tool calling**: `qwen2.5:7b` has better tool calling accuracy than `llama3.2:3b`. Always validate tool call arguments with Pydantic before executing. If tool calling fails, fall back to keyword-based routing.
210
+ - **MCP server stdio**: MCP server communicates via stdin/stdout (stdio transport). Do NOT print anything to stdout in server mode — use stderr for logging. Use `from mcp.server.fastmcp import FastMCP` — `FastMCP` lives inside the official `mcp` package (`mcp>=1.12`), not the third-party `fastmcp` package. Pass `log_level="WARNING"` to suppress INFO noise to stderr.
211
+ - **Chat streaming**: When streaming Ollama responses, use `rich.live.Live` context for smooth token-by-token display. Don't mix `print()` with Rich Live — it causes display glitches.
212
+ - **Tool function docstrings matter**: Ollama/Claude read tool docstrings to decide when to call them. Write docstrings as if explaining to a smart intern what the function does and when to use it.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 bb-cli contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.