IRC chat log forensic analyzer for moderation disputes. Extracts a time window from a WeeChat log, traces the participants, and feeds the transcript to an LLM for analysis — with an interactive TUI for review.
- Binary search over large log files (O(log n)) — no full scan needed
- Nick-change tracking: follows a user through alias changes within the window
- Participant expansion: auto-adds nicks that were directly mentioned or replied to; suggests indirect participants
- Sentiment analysis: lightweight lexicon (default) or transformer-based (
-S); optional second-pass LLM context via--inject-ratings - Interactive TUI: colorized transcript, nick visibility toggle, filter/search, LLM chat panel, runtime prompt selector
- LLM integration: one-shot forensic analysis or iterative chat via OpenRouter (streaming)
- Custom system prompt: bring your own prompt file to tailor the LLM role; append channel rules with
--rules - Token/cost estimate: shown before sending anything to the LLM
- Export: HTML or Markdown transcript dump
- Python 3.11+
- pyrg (local dependency — ripgrep wrapper)
- An OpenRouter API key for LLM features
# From the repo root
pip install -e .
# With transformer-based sentiment analysis
pip install -e ".[sentiment]"Copy .env.example to .env in your working directory (or ~/.config/pycl/.env):
cp .env.example .env# Required for LLM analysis
OPENROUTER_API_KEY=sk-or-...
# Model to use (default: anthropic/claude-opus-4-8)
PYCL_MODEL=anthropic/claude-opus-4-8
# Initial chat panel height in the TUI (6–50, default: 13)
PYCL_CHAT_HEIGHT=13
# Default system prompt file (overridden by --prompt on the CLI)
# PYCL_PROMPT_FILE=/path/to/prompt.md
# Default channel rules file (overridden by --rules on the CLI)
# PYCL_RULES_FILE=/path/to/rules.mdpycl analyze #madrid.04.2025.log \
--nicks alice,bob \
--from "day 13 at 18:00" \
--to "day 13 at 19:00"pycl analyze #madrid.04.2025.log \
--from "2025-04-13 18:00:00" \
--period "+1h"Without --nicks the full window is loaded and sent to the LLM. A warning is printed when the window exceeds 500 lines.
pycl analyze #madrid.04.2025.log \
--nicks alice,bob \
--from "day 13 at 18" --period "+2h" \
--no-tuiAfter the pager closes, a token count and cost estimate for the LLM request are printed.
pycl analyze #madrid.04.2025.log \
--nicks alice,bob \
--from "day 13 at 18" --to "day 13 at 19" \
--export htmlpycl analyze #madrid.04.2025.log \
--nicks alice,bob \
--from "day 13 at 18" --to "day 13 at 19" \
--prompt rules/IG\ rules.md| Flag | Default | Description |
|---|---|---|
--nicks NICKS |
(none) | Comma-separated nicks to trace. Omit for full window. |
--from EXPR |
(required) | Window start — ISO datetime or natural language ("day 13 at 18") |
--to EXPR |
— | Window end (mutually exclusive with --period) |
--period DELTA |
— | Duration from --from (e.g. +1h, +30m) |
--model MODEL |
PYCL_MODEL / built-in |
OpenRouter model ID |
--prompt FILE |
PYCL_PROMPT_FILE / built-in |
System prompt file (.md) |
--rules FILE |
PYCL_RULES_FILE |
Channel rules file (.md) — appended after the system prompt |
-S, --sentiment |
off | Use pysentimiento transformer (requires pycl[sentiment]) |
--inject-ratings |
off | Prepend per-user sentiment scores to the LLM system prompt (second-pass context) |
--participant-window SECS |
60 |
Seconds window for indirect participant suggestions |
--no-tui |
off | Static Rich output + pager instead of the TUI |
--export {html,md} |
— | Export transcript to file |
--no-cache |
off | Force recalculation (ignore all caches) |
--debug |
off | Print internal debug info to stderr |
| Key | Action |
|---|---|
f |
Open Filter bar — hides non-matching lines |
/ |
Open Search bar — highlights matches, all lines visible |
n / Shift+N |
Next / previous search match |
a |
Add nick to trace |
p |
Open Prompt selector — switch system prompt at runtime |
Ctrl+L |
Run forensic LLM analysis (one-shot, streaming) |
e |
Export transcript to HTML |
+ / - |
Grow / shrink chat panel |
q |
Quit |
Esc |
Close filter/search bar |
Within the chat panel, type a message and press Enter to chat with the LLM using the transcript as context.
WeeChat tab-separated format:
YYYY-MM-DD HH:MM:SS\t<nick>\t<message>
YYYY-MM-DD HH:MM:SS\t-!-\t<event body>
Year/month are inferred from the filename (#channel.04.2025.log) for natural-language time expressions.
See docs/LOG_FORMAT.md for the complete format specification and LLM interpretation guide.
src/pycl/
├── cli.py Entry point, argument parsing, output routing
├── window.py Binary search (mmap) for time-window extraction
├── readers/ Log format parsers
│ ├── base.py BaseReader protocol
│ └── weechat.py WeeChat tab-separated format parser
├── filtering.py Nick-change expansion, participant detection (via pyrg)
├── identity.py Identity dataclass helpers and color assignment
├── cache.py 3-layer cache: window slice / filtered events / LLM response
├── render.py Rich 256-color transcript rendering
├── sentiment.py Lexicon and transformer sentiment backends
├── llm.py OpenRouter client (streaming), token/cost estimation
├── config.py .env loading, typed env getters
├── defaults.py Constants (nick palette, heights, prompt version)
└── tui/
├── app.py PyclApp — main Textual application, prompt selector modal
├── transcript.py Scrollable transcript with filter/search modes
├── sidebar.py Identity list and participant suggestions
├── chat.py LLM chat panel with streaming and busy indicator
└── styles.tcss Layout and theming
Cache files are stored in .cache/ next to the log file. Each entry has a metadata sidecar (mtime + size) for automatic invalidation when the source file changes.
Replaces the built-in forensic framework entirely with the contents of a Markdown file:
pycl analyze log.txt --nicks alice --from "18:00" --to "19:00" \
--prompt my_prompt.mdAppends the rules file after the forensic framework (or custom prompt) so the LLM keeps the analysis structure and can also cite specific rule violations:
pycl analyze log.txt --nicks alice --from "18:00" --to "19:00" \
--rules "rules/IG rules.md"The LLM receives:
[forensic analyst role — built-in or custom]
---
## Channel rules in effect
[contents of rules file]
| File | Use case |
|---|---|
prompts/moderator_audit.md |
Evaluate whether the operators acted within the rules — kick reasons, ban compliance, mode limits, akick renewal |
prompts/incident_report.md |
Structured formal report — participants, timeline, rule violations, severity 1-5, recommended action — suitable for submitting to network admins |
prompts/quick_triage.md |
Fast decision: single paragraph + classification (IGNORE / WARN / KICK / BAN / ESCALATE) + key evidence — no deep analysis |
The built-in prompt (SYSTEM_PROMPT_claude.md) covers deep forensic analysis of user-vs-user disputes.
Both flags can be combined to use a custom base prompt with specific rules appended:
pycl analyze log.txt --nicks alice --from "18:00" --to "19:00" \
--prompt prompts/senior_mod.md \
--rules "rules/IG rules.md"Set defaults in .env to avoid specifying them on every invocation:
PYCL_PROMPT_FILE=prompts/senior_mod.md
PYCL_RULES_FILE=rules/IG rules.mdThe --sentiment / -S flag activates the pysentimiento transformer backend (Spanish, POS/NEG/NEU + emotion). Without it, a fast lexicon-based backend runs by default.
Use --inject-ratings to prepend per-user aggregate scores to the LLM system prompt as a second-pass context block:
pycl analyze log.txt --nicks alice,bob --from "18:00" --to "19:00" \
-S --inject-ratingsThis is useful when sentiment scores already give a clear signal and you want the LLM to interpret them as part of the forensic analysis.
| Variable | Description |
|---|---|
OPENROUTER_API_KEY |
API key (required for LLM) |
PYCL_MODEL |
Default OpenRouter model ID |
PYCL_CHAT_HEIGHT |
Initial TUI chat panel height (6–50) |
PYCL_PROMPT_FILE |
Default system prompt file path |
PYCL_RULES_FILE |
Default channel rules file path |
- Run the LLM analysis only with the nicks with negative sentiment.