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.
- blackboard_cli-0.1.0/.claude/CLAUDE.md +81 -0
- blackboard_cli-0.1.0/.claude/hooks/README.md +47 -0
- blackboard_cli-0.1.0/.claude/hooks/scripts/guard-uv.sh +45 -0
- blackboard_cli-0.1.0/.claude/hooks/scripts/suggest-targeted-tests.sh +58 -0
- blackboard_cli-0.1.0/.claude/settings.json +30 -0
- blackboard_cli-0.1.0/.claude/settings.local.json +92 -0
- blackboard_cli-0.1.0/.github/workflows/ci.yml +33 -0
- blackboard_cli-0.1.0/.gitignore +22 -0
- blackboard_cli-0.1.0/CLAUDE.md +212 -0
- blackboard_cli-0.1.0/LICENSE +21 -0
- blackboard_cli-0.1.0/PKG-INFO +31 -0
- blackboard_cli-0.1.0/README.md +175 -0
- blackboard_cli-0.1.0/bb/__init__.py +1 -0
- blackboard_cli-0.1.0/bb/adapters/__init__.py +0 -0
- blackboard_cli-0.1.0/bb/adapters/base.py +56 -0
- blackboard_cli-0.1.0/bb/adapters/blackboard_ultra.py +692 -0
- blackboard_cli-0.1.0/bb/adapters/registry.py +42 -0
- blackboard_cli-0.1.0/bb/ai/__init__.py +0 -0
- blackboard_cli-0.1.0/bb/ai/chat.py +411 -0
- blackboard_cli-0.1.0/bb/ai/prompts.py +26 -0
- blackboard_cli-0.1.0/bb/ai/providers/__init__.py +0 -0
- blackboard_cli-0.1.0/bb/ai/providers/ollama.py +62 -0
- blackboard_cli-0.1.0/bb/auto_setup.py +240 -0
- blackboard_cli-0.1.0/bb/cache.py +74 -0
- blackboard_cli-0.1.0/bb/cli.py +867 -0
- blackboard_cli-0.1.0/bb/config.py +52 -0
- blackboard_cli-0.1.0/bb/db.py +361 -0
- blackboard_cli-0.1.0/bb/downloader.py +80 -0
- blackboard_cli-0.1.0/bb/hash.py +14 -0
- blackboard_cli-0.1.0/bb/mcp/__init__.py +0 -0
- blackboard_cli-0.1.0/bb/mcp/server.py +23 -0
- blackboard_cli-0.1.0/bb/models/__init__.py +0 -0
- blackboard_cli-0.1.0/bb/models/content.py +75 -0
- blackboard_cli-0.1.0/bb/notify/__init__.py +20 -0
- blackboard_cli-0.1.0/bb/notify/discord.py +30 -0
- blackboard_cli-0.1.0/bb/notify/ntfy.py +32 -0
- blackboard_cli-0.1.0/bb/notify/telegram.py +34 -0
- blackboard_cli-0.1.0/bb/notify/terminal.py +23 -0
- blackboard_cli-0.1.0/bb/parsers/__init__.py +0 -0
- blackboard_cli-0.1.0/bb/parsers/html_llm.py +71 -0
- blackboard_cli-0.1.0/bb/parsers/ical.py +88 -0
- blackboard_cli-0.1.0/bb/security/__init__.py +0 -0
- blackboard_cli-0.1.0/bb/security/session.py +138 -0
- blackboard_cli-0.1.0/bb/sync.py +121 -0
- blackboard_cli-0.1.0/bb/tools/__init__.py +40 -0
- blackboard_cli-0.1.0/bb/tools/ai_tools.py +203 -0
- blackboard_cli-0.1.0/bb/tools/queries.py +329 -0
- blackboard_cli-0.1.0/inspect_courses_page.py +40 -0
- blackboard_cli-0.1.0/pyproject.toml +64 -0
- blackboard_cli-0.1.0/selectors/.gitkeep +0 -0
- blackboard_cli-0.1.0/selectors/blackboard_ultra.toml +80 -0
- blackboard_cli-0.1.0/tests/__init__.py +0 -0
- blackboard_cli-0.1.0/tests/fixtures/activity_stream.html +111 -0
- blackboard_cli-0.1.0/tests/fixtures/sample.ics +53 -0
- blackboard_cli-0.1.0/tests/test_adapter_base.py +151 -0
- blackboard_cli-0.1.0/tests/test_ai_tools.py +237 -0
- blackboard_cli-0.1.0/tests/test_auto_setup.py +265 -0
- blackboard_cli-0.1.0/tests/test_blackboard_adapter.py +230 -0
- blackboard_cli-0.1.0/tests/test_cache.py +324 -0
- blackboard_cli-0.1.0/tests/test_chat.py +717 -0
- blackboard_cli-0.1.0/tests/test_cli.py +198 -0
- blackboard_cli-0.1.0/tests/test_cli_ann.py +201 -0
- blackboard_cli-0.1.0/tests/test_cli_auth_status.py +269 -0
- blackboard_cli-0.1.0/tests/test_cli_auto_setup.py +213 -0
- blackboard_cli-0.1.0/tests/test_cli_course.py +368 -0
- blackboard_cli-0.1.0/tests/test_cli_download.py +404 -0
- blackboard_cli-0.1.0/tests/test_cli_due.py +243 -0
- blackboard_cli-0.1.0/tests/test_cli_grades.py +220 -0
- blackboard_cli-0.1.0/tests/test_cli_import_ical.py +298 -0
- blackboard_cli-0.1.0/tests/test_config.py +129 -0
- blackboard_cli-0.1.0/tests/test_db.py +358 -0
- blackboard_cli-0.1.0/tests/test_db_downloads.py +94 -0
- blackboard_cli-0.1.0/tests/test_downloader.py +190 -0
- blackboard_cli-0.1.0/tests/test_hash.py +94 -0
- blackboard_cli-0.1.0/tests/test_html_llm.py +146 -0
- blackboard_cli-0.1.0/tests/test_ical_parser.py +248 -0
- blackboard_cli-0.1.0/tests/test_mcp_server.py +127 -0
- blackboard_cli-0.1.0/tests/test_models.py +384 -0
- blackboard_cli-0.1.0/tests/test_notifications.py +191 -0
- blackboard_cli-0.1.0/tests/test_notify.py +127 -0
- blackboard_cli-0.1.0/tests/test_notify_telegram_discord.py +100 -0
- blackboard_cli-0.1.0/tests/test_packaging.py +65 -0
- blackboard_cli-0.1.0/tests/test_registry.py +148 -0
- blackboard_cli-0.1.0/tests/test_session.py +287 -0
- blackboard_cli-0.1.0/tests/test_sync_flow.py +332 -0
- blackboard_cli-0.1.0/tests/test_tools.py +485 -0
- blackboard_cli-0.1.0/tests/test_tools_download.py +230 -0
- 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.
|