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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -475,7 +475,7 @@ site/
*.db-shm
*.db-journal

# Internal docs (kept locally, not published)
# Internal/planning docs (kept locally, not published)
docs/internal/
docs/local_repo_docs/
docs/plans/
Expand Down
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,9 @@ graph LR
```bash
# Study sessions (tmux + AI agent + sidebar)
studyctl study "topic" --energy 7 # Full tmux environment in one command
studyctl study "topic" --web # Also start web dashboard + auto-open browser
studyctl study "topic" --lan # LAN access with password auth (implies --web)
studyctl study "topic" --lan --password SECRET # Explicit LAN password
studyctl study --resume # Resume conversation from history
studyctl study --end # End session (quit Claude also works)
studyctl park "question" # Park tangential topic
Expand Down Expand Up @@ -140,6 +143,7 @@ Launch with `studyctl web`. Accessible from any device on the network.
- Timer with energy-adaptive colour phases (green/amber/red)
- Topic counters (wins, parked, review)
- Session summary on completion
- **Terminal panel** — embedded ttyd iframe proxied same-origin at `/terminal/` with draggable split-pane (stacked or side-by-side). Pop-out to new window and seamless return. HTTP Basic Auth when using `--lan`. Requires ttyd (optional but recommended).
- HTMX + Alpine.js — no build step

## Optional Extras
Expand All @@ -149,6 +153,10 @@ pip install 'studyctl[all]' # Everything
pip install 'studyctl[web]' # FastAPI web UI
pip install 'studyctl[content]' # PDF splitting + NotebookLM
pip install 'studyctl[notebooklm]' # NotebookLM API client

# ttyd — web terminal (enables the terminal panel in the live dashboard)
brew install ttyd # macOS
sudo apt install ttyd # Linux (or build from source)
```

## Documentation
Expand Down
16 changes: 16 additions & 0 deletions docs/cli-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ studyctl study "Python Decorators" --energy 7 # Socratic mentor session
studyctl study "Spark Internals" --mode co-study # User-driven co-study
studyctl study "topic" --timer pomodoro # Override default timer
studyctl study "topic" --agent claude --web # Explicit agent + web dashboard
studyctl study "topic" --lan # Bind to 0.0.0.0, password-protected (implies --web)
studyctl study "topic" --lan --password SECRET # Explicit password for LAN auth
studyctl study "topic" --agent ollama # Local LLM via Ollama + LiteLLM
studyctl study "topic" --agent lmstudio # Local LLM via LM Studio
studyctl study --resume # Resume conversation (-r)
Expand All @@ -77,6 +79,9 @@ studyctl park "How does asyncio compare?" # Park mid-session
- Sidebar shows timer, activity feed, counters (keyboard: `p` pause, `r` reset, `Q` end session)
- IPC files for dashboard viewports
- Optional web dashboard at `/session` via `--web`
- `--web` auto-opens a browser to the dashboard on startup
- `--lan` binds the web server and ttyd to `0.0.0.0` with HTTP Basic Auth, prints dashboard URL and password (implies `--web`). Password is auto-generated if not set via `--password` or `lan_password` in config
- `--password SECRET` sets the LAN authentication password (used with `--lan`)

**Session lifecycle:**
- **Start:** `studyctl study "topic"` — creates tmux session, agent, sidebar
Expand Down Expand Up @@ -153,10 +158,21 @@ studyctl web --host localhost # Local only

**Features:** Source/chapter filter, card count limiter (10/20/50/100/All), due cards badge, session history, 90-day study heatmap, Pomodoro timer (25min/5min), OpenDyslexic font toggle, dark/light theme, PWA installable.

**Live session dashboard** (`/session`): Real-time SSE activity feed, energy-adaptive timer, topic counters, and a **terminal panel** — an embedded ttyd iframe showing the tmux session live via same-origin proxy (`/terminal/`). The panel is draggable (stacked or side-by-side), has a layout toggle and panel-swap buttons, and can be popped out to a separate window (pop-out auto-closes when returning inline). ttyd is optional (`brew install ttyd`) but required for the terminal panel.

**Voice:** Uses Web Speech API (browser built-in). Two modes:
- **Read once** — speaker icon on card or `T` key
- **Auto-voice** — header toggle or `V` key (reads everything automatically)

**Web + terminal config** (`~/.config/studyctl/config.yaml`):

```yaml
web_port: 8567 # web dashboard port (default 8567)
ttyd_port: 7681 # ttyd web terminal port (default 7681)
browser: "" # auto-open browser: chrome, safari, firefox, brave, or empty for system default
lan_password: "" # persistent password for --lan mode (auto-generated if empty)
```

---

## agent-session-tools
Expand Down
18 changes: 13 additions & 5 deletions docs/roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -278,11 +278,19 @@ Study from any device (iPad, laptop, phone) via ttyd + web dashboard, with optio

### ttyd Integration + Embedded Web Pane

- [ ] `studyctl study --lan` starts ttyd alongside tmux session (port 7681)
- [ ] Embed ttyd as iframe in existing web dashboard
- [ ] "Open Study Agent Session" button — iframe split (default) + pop-out new window
- [ ] Doctor check for ttyd binary
- [ ] Config support for ttyd port
- [x] `studyctl study --lan` starts ttyd alongside tmux session (port 7681)
- [x] Embed ttyd as iframe in existing web dashboard
- [x] "Open Study Agent Session" button — iframe split (default) + pop-out new window
- [x] Doctor check for ttyd binary
- [x] Config support for ttyd port
- [x] Draggable split-pane layout (stacked/side-by-side)
- [x] Browser auto-open with configurable browser
- [x] Claude Code trust bypass (`hasTrustDialogAccepted`)
- [x] `window-size largest` tmux option for multi-client compatibility
- [x] Same-origin reverse proxy — ttyd proxied through FastAPI at `/terminal/` (HTTP + WebSocket)
- [x] Pop-out/return persistence — iframe stays in DOM (visibility CSS), pop-out auto-closes on return
- [x] LAN password protection — HTTP Basic Auth middleware, auto-generated or explicit `--password`
- [x] `lan_password` config option for persistent password

### Local LLMs via Ollama / LM Studio

Expand Down
32 changes: 32 additions & 0 deletions docs/setup-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ Step-by-step installation and configuration for Socratic Study Mentor.
- **Obsidian** — for study notes (any vault structure works)
- **Optional**: `notebooklm-py` for Google NotebookLM sync
- **Optional**: `sentence-transformers` for semantic search
- **Optional**: `ttyd` — enables web terminal access from browser or iPad (`brew install ttyd` on macOS, `apt install ttyd` on Linux)

> **tmux-resurrect / tmux-continuum users**: studyctl automatically cleans up
> zombie sessions on startup, so resurrect-restored sessions are handled
Expand Down Expand Up @@ -173,6 +174,27 @@ Two voice modes in the PWA:

**Accessibility:** The `Aa` button toggles [OpenDyslexic](https://opendyslexic.org) font. The sun icon toggles light/dark theme. Both are persisted across sessions.

### Remote Study (iPad on the Bus)

Use `--lan` to make the web dashboard and terminal accessible from any device on your network — phone, iPad, second laptop:

```bash
studyctl study "Python Decorators" --energy 7 --lan
# Auto-generates password and saves LAN info to session state:
# Dashboard: http://192.168.1.42:8567/session
# Password: <auto-generated>

# Or set a known password:
studyctl study "Python Decorators" --energy 7 --lan --password mysecret
```

Access the live dashboard and embedded terminal from your iPad at `http://<mac-ip>:8567/session`. Enter any username and the displayed password when prompted. The terminal panel (ttyd iframe) is proxied through the web server on the same origin, so pop-out/return works seamlessly. ttyd must be installed for the terminal panel to work (`brew install ttyd`).

**Password sources** (checked in order):
1. `--password` CLI flag
2. `lan_password` in `~/.config/studyctl/config.yaml`
3. Auto-generated (displayed in terminal output, saved to session state)

### Hosts — Cross-Machine Sync

#### Prerequisites: Passwordless SSH
Expand Down Expand Up @@ -304,6 +326,16 @@ Environment variable overrides:
- `LOG_LEVEL` — set logging level (DEBUG, INFO, WARNING, ERROR)
- `EMBEDDING_MODEL` — override embedding model

### Web Terminal Settings

```yaml
# Web terminal (optional — requires ttyd installed)
ttyd_port: 7681 # ttyd listens on this port (default 7681)
web_port: 8567 # web dashboard port (default 8567)
browser: "" # auto-open browser: chrome, safari, firefox, brave, or empty for system default
lan_password: "" # persistent LAN password (auto-generated per session if empty)
```

### TTS Voice Settings

```yaml
Expand Down
12 changes: 11 additions & 1 deletion docs/system-overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@ graph TB
TMUX --> AGENT
TMUX --> SIDEBAR
STUDY -.->|"--web"| DASH
STUDY -.->|"--lan"| TTYD["ttyd<br/>(port 7681)"]
TTYD -.->|"attaches to"| TMUX
DASH -.->|"/terminal/ proxy"| TTYD
AGENT -->|writes| IPC
SIDEBAR -->|polls| IPC
DASH -->|polls SSE| IPC
Expand Down Expand Up @@ -151,11 +154,17 @@ sequenceDiagram
U->>CLI: studyctl study "Decorators" --energy 7
CLI->>DB: start_study_session()
CLI->>IPC: Write session-state.json + empty topics/parking
CLI->>TMUX: Create session (cwd=sessions/study-decorators-xxx/)
CLI->>TMUX: Create session (cwd=sessions/study-decorators-xxx/, window-size largest)
CLI->>TMUX: Split pane 75/25
TMUX->>AGENT: Main pane: claude --append-system-prompt-file persona.md
TMUX->>TUI: Sidebar pane: python -m studyctl.tui.sidebar

opt --lan flag
CLI->>WEB: Start ttyd (attaches to tmux session, port 7681)
CLI->>WEB: Start web with Basic Auth + ttyd proxy
Note over WEB: Web dashboard proxies ttyd at /terminal/<br/>(same-origin — pop-out/return preserves WS)<br/>HTTP Basic Auth protects all routes on LAN
end

loop Every 2 seconds
TUI->>IPC: stat() mtime check
IPC-->>TUI: Update timer, activity, counters
Expand Down Expand Up @@ -527,6 +536,7 @@ Here's a complete workflow from course materials to mastery:
|-----------|----------|---------|
| Python 3.12+ | Yes | `mise install python` or system package |
| tmux 3.1+ | For `studyctl study` | `brew install tmux` / `apt install tmux` |
| ttyd | Optional — remote terminal (`--lan`) | `brew install ttyd` / `apt install ttyd` |
| Claude Code | For AI study sessions | `npm install -g @anthropic-ai/claude-code` |
| pandoc | For markdown to PDF | `brew install pandoc` |
| typst | For PDF rendering | `brew install typst` |
Expand Down
8 changes: 7 additions & 1 deletion packages/studyctl/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ dependencies = [
content = ["pymupdf>=1.25", "httpx"]
notebooklm = ["notebooklm-py>=0.3.4"]
tui = ["textual>=0.80"]
web = ["fastapi>=0.115", "uvicorn[standard]>=0.34"]
web = ["fastapi>=0.115", "uvicorn[standard]>=0.34", "httpx>=0.27", "websockets>=12.0"]
mcp = ["mcp[cli]>=1.0.0"]
all = ["studyctl[content,web,notebooklm,tui,mcp]"]

Expand All @@ -52,9 +52,15 @@ testpaths = ["tests"]
addopts = "--tb=short -m 'not integration'"
markers = [
"integration: requires external infrastructure (tmux, real DB, network)",
"e2e: end-to-end browser tests requiring Playwright + web server",
]

[tool.pyright]
pythonVersion = "3.12"
typeCheckingMode = "basic"
exclude = ["src/studyctl/tui", "src/studyctl/content", "src/studyctl/cli/_content.py", "src/studyctl/cli/_web.py", "src/studyctl/web", "src/studyctl/mcp"] # optional deps not in CI

[dependency-groups]
dev = [
"pytest-playwright>=0.7.2",
]
56 changes: 39 additions & 17 deletions packages/studyctl/src/studyctl/agent_launcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@
and launch commands. The canonical persona content is shared; each adapter's
setup() callable transforms it for that agent's specific mechanism.

Agent mechanisms (verified 2026-04-03):
Agent mechanisms (verified 2026-04-04):
Claude: --append-system-prompt-file {temp_file}
Gemini: GEMINI.md written to session cwd (auto-loaded)
Kiro: ~/.kiro/agents/study-mentor.json updated with file:// prompt ref
OpenCode: ~/.config/opencode/agents/study-mentor.md with YAML frontmatter
Crash recovery: stale backups restored on next setup
OpenCode: .opencode/agents/study-mentor.md with YAML frontmatter (permission: format)
"""

from __future__ import annotations
Expand Down Expand Up @@ -131,10 +132,19 @@ def _kiro_setup(canonical_content: str, _session_dir: Path) -> Path:
# 4. Ensure target directory exists
KIRO_AGENTS_DIR.mkdir(parents=True, exist_ok=True)
target = KIRO_AGENTS_DIR / f"{KIRO_AGENT_NAME}.json"
backup = target.with_suffix(target.suffix + _KIRO_BACKUP_SUFFIX)

# 4a. Recover from crash: if a backup exists, the previous session's
# teardown never ran. Restore the user's original config first.
if backup.exists():
logger.warning(
"Stale Kiro backup detected (previous session crashed?) — restoring %s",
backup,
)
os.replace(backup, target)

# 5. Backup existing agent JSON if present
if target.exists():
backup = target.with_suffix(target.suffix + _KIRO_BACKUP_SUFFIX)
shutil.copy2(target, backup)

# 6. Atomic write: temp file in same dir → os.replace()
Expand Down Expand Up @@ -176,6 +186,20 @@ def _kiro_teardown(_session_dir: Path) -> None:
# ---------------------------------------------------------------------------


def _mcp_command() -> list[str]:
"""Build the studyctl-mcp server command.

Prefers the installed console script (pip/uv tool install).
Falls back to uv run --project for development.
"""
binary = shutil.which("studyctl-mcp")
if binary:
return [binary]
# Dev fallback: run from the repo workspace
project_path = str(_REPO_ROOT / "packages" / "studyctl")
return ["uv", "run", "--project", project_path, "studyctl-mcp"]


def _gemini_setup(canonical_content: str, session_dir: Path) -> Path:
"""Write GEMINI.md to session dir (auto-loaded by Gemini CLI from cwd)."""
persona_path = session_dir / "GEMINI.md"
Expand All @@ -196,13 +220,12 @@ def _gemini_mcp(session_dir: Path) -> None:
gemini_dir = session_dir / ".gemini"
gemini_dir.mkdir(parents=True, exist_ok=True)

# Build MCP server command using the project path
project_path = str(_REPO_ROOT / "packages" / "studyctl")
cmd = _mcp_command()
settings = {
"mcpServers": {
"studyctl-mcp": {
"command": "uv",
"args": ["run", "--project", project_path, "studyctl-mcp"],
"command": cmd[0],
"args": cmd[1:],
},
},
}
Expand All @@ -217,8 +240,8 @@ def _gemini_mcp(session_dir: Path) -> None:
# ~/.config/opencode/agents/ or project-local agents/. The setup
# function writes the persona as a markdown file with YAML frontmatter
# in the session directory. MCP uses a different schema from others:
# "command" is a flat array, "enabled" (not "disabled"), "environment"
# (not "env").
# "command" is a flat array, "enabled" (not "disabled"),
# "type": "local" required.
# ---------------------------------------------------------------------------

_OPENCODE_AGENTS_DIR_NAME = ".opencode"
Expand All @@ -235,10 +258,12 @@ def _opencode_setup(canonical_content: str, session_dir: Path) -> Path:
'description: "AuDHD-aware Socratic study mentor"\n'
"mode: primary\n"
"temperature: 0.3\n"
"tools:\n"
" write: true\n"
" edit: true\n"
" bash: true\n"
"permission:\n"
" edit: allow\n"
" bash:\n"
' "studyctl *": allow\n'
' "session-* *": allow\n'
' "*": ask\n'
"---\n\n"
)
persona_path.write_text(frontmatter + canonical_content)
Expand All @@ -259,20 +284,17 @@ def _opencode_mcp(session_dir: Path) -> None:
OpenCode's schema differs from others:
- ``command`` is a flat array (binary + args merged)
- ``enabled`` instead of ``disabled``
- ``environment`` instead of ``env``
- ``type: "local"`` required
"""
oc_dir = session_dir / _OPENCODE_AGENTS_DIR_NAME
oc_dir.mkdir(parents=True, exist_ok=True)

project_path = str(_REPO_ROOT / "packages" / "studyctl")
config = {
"mcp": {
"studyctl-mcp": {
"command": ["uv", "run", "--project", project_path, "studyctl-mcp"],
"command": _mcp_command(),
"enabled": True,
"type": "local",
"environment": {},
},
},
}
Expand Down
3 changes: 2 additions & 1 deletion packages/studyctl/src/studyctl/cli/_doctor.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ def _get_registry():
check_studyctl_installed,
)
from studyctl.doctor.database import check_review_db, check_sessions_db
from studyctl.doctor.deps import check_optional_deps
from studyctl.doctor.deps import check_optional_deps, check_system_binaries
from studyctl.doctor.updates import check_pypi_versions

registry = CheckerRegistry()
Expand All @@ -48,6 +48,7 @@ def _get_registry():
for fn in [check_obsidian_vault, check_review_directories, check_pandoc, check_tmux_resurrect]:
registry.register("config")(fn)
registry.register("deps")(check_optional_deps)
registry.register("deps")(check_system_binaries)
registry.register("agents")(check_agent_definitions)
registry.register("agents")(check_agent_smoke_tests)
registry.register("agents")(check_local_llm_servers)
Expand Down
Loading
Loading