Revert "feat: add --llm mode for LLM-friendly stdout filtering"

This reverts commit ea6f07914e.
This commit is contained in:
user
2026-02-19 20:39:36 +01:00
parent ea6f07914e
commit 6f1f4b2fc8
4 changed files with 12 additions and 147 deletions

View File

@@ -1,25 +1,6 @@
# derp - Tasks
## Current Sprint -- v1.2.9 LLM Mode (2026-02-19)
| Pri | Status | Task |
|-----|--------|------|
| P0 | [x] | `--llm` CLI flag: route logging to `info.log`, stdout for addressed messages |
| P0 | [x] | `_is_addressed()` method: DMs + nick-prefixed |
| P1 | [x] | Stdout routing: PRIVMSG in/out, PING, 001, disconnect, reconnect |
| P2 | [x] | Documentation update (USAGE.md CLI flags + LLM mode section) |
## Previous Sprint -- v1.2.8 ASN Backend Replacement (2026-02-19)
| Pri | Status | Task |
|-----|--------|------|
| P0 | [x] | Replace MaxMind ASN with iptoasn.com TSV backend (no license key) |
| P0 | [x] | Bisect-based lookup in `plugins/asn.py` (pure stdlib) |
| P1 | [x] | `update_asn()` in `scripts/update-data.sh` (SOCKS5 download) |
| P2 | [x] | Tests: load, lookup, command handler (30 cases, 906 total) |
| P2 | [x] | Documentation update (USAGE.md data directory layout) |
## Previous Sprint -- v1.2.7 Subscription Plugin Enrichment (2026-02-19)
## Current Sprint -- v1.2.7 Subscription Plugin Enrichment (2026-02-19)
| Pri | Status | Task |
|-----|--------|------|

View File

@@ -16,7 +16,6 @@ derp --config /path/to/derp.toml --verbose
|------|-------------|
| `-c, --config PATH` | Config file path |
| `-v, --verbose` | Debug logging |
| `--llm` | LLM mode: addressed messages to stdout, rest to info.log |
| `--cprofile [PATH]` | Enable cProfile, dump to PATH [derp.prof] |
| `--tracemalloc [N]` | Enable tracemalloc, capture N frames deep [10] |
| `-V, --version` | Print version |
@@ -52,7 +51,6 @@ plugins_dir = "plugins" # Plugin directory path
rate_limit = 2.0 # Max messages per second (default: 2.0)
rate_burst = 5 # Burst capacity (default: 5)
paste_threshold = 4 # Max lines before overflow to FlaskPaste (default: 4)
owner = [] # Owner hostmask patterns (fnmatch), grants admin + LLM access
admins = [] # Hostmask patterns (fnmatch), IRCOPs auto-detected
timezone = "UTC" # Timezone for calendar reminders (IANA tz name)
@@ -218,20 +216,14 @@ Default format is `"text"` (human-readable, same as before).
## Admin System
Commands marked as `admin` require elevated permissions. There are two
privilege levels:
Commands marked as `admin` require elevated permissions. Admin access is
granted via:
| Level | Source | Grants |
|-------|--------|--------|
| **Owner** | `[bot] owner` hostmask patterns | Admin + LLM mode access |
| **Admin** | `[bot] admins` patterns, IRC operators | Admin commands only |
Owner is a superset of admin -- owners automatically have admin privileges.
Only owners can interact with the bot via `--llm` mode.
1. **IRC operator status** -- detected automatically via `WHO`
2. **Hostmask patterns** -- configured in `[bot] admins`, fnmatch-style
```toml
[bot]
owner = ["me!~user@my.host"]
admins = [
"*!~user@trusted.host",
"ops!*@*.ops.net",
@@ -376,7 +368,7 @@ The script is cron-friendly (exit 0/1, quiet unless `NO_COLOR` is unset).
```
data/
GeoLite2-City.mmdb # MaxMind GeoIP (requires license key)
ip2asn-v4.tsv # iptoasn.com ASN database (no key required)
GeoLite2-ASN.mmdb # MaxMind ASN (requires license key)
tor-exit-nodes.txt # Tor exit node IPs
iprep/ # Firehol/ET blocklist feeds
firehol_level1.netset
@@ -388,8 +380,7 @@ data/
...
```
The ASN database is downloaded from iptoasn.com (no account required).
GeoLite2-City requires a free MaxMind license key -- set
GeoLite2 databases require a free MaxMind license key. Set
`MAXMIND_LICENSE_KEY` when running the update script.
## Plugin Management
@@ -497,39 +488,6 @@ On connection loss, the bot reconnects with exponential backoff and jitter:
- Jitter: +/- 25% to avoid thundering herd
- Resets to 5s after a successful connection
## LLM Mode
Run with `--llm` to split output for LLM consumption. All internal logging
(connection status, plugin loads, unaddressed chatter) goes to `info.log`.
Stdout receives only messages addressed to the bot and the bot's own replies.
```bash
derp --llm
derp --llm --config config/derp.toml
```
### What goes to stdout
- **DMs**: private messages from an owner
- **Nick-prefixed**: channel messages from an owner starting with `<botnick>:` or `<botnick>,`
- **Bot replies**: all messages sent by the bot (PRIVMSG and ACTION)
- **Status lines**: connection, ping, disconnect, reconnect events
### Output format
```
19:09 --- connected as derp
19:09 --- ping
19:09 #test <alice> derp: what is 1.1.1.1?
19:09 #test <derp> 1.1.1.1: AS13335 CLOUDFLARENET (US)
19:09 <bob> hey derp, check this out
19:09 <derp> I'm just a bot
19:09 #test * derp [rss/hackernews] New article -- URL
19:15 --- disconnected: Connection reset
19:15 --- reconnecting in 5s
19:15 --- connected as derp
```
### `!dork` -- Google Dork Query Builder
Generate Google dork queries for a target domain. Template-based, no HTTP

View File

@@ -7,7 +7,6 @@ import base64
import fnmatch
import logging
import random
import sys
import time
from datetime import datetime, timezone
from pathlib import Path
@@ -78,10 +77,9 @@ class _TokenBucket:
class Bot:
"""IRC bot: ties connection, config, and plugins together."""
def __init__(self, config: dict, registry: PluginRegistry, *, llm: bool = False) -> None:
def __init__(self, config: dict, registry: PluginRegistry) -> None:
self.config = config
self.registry = registry
self._llm = llm
self.conn = IRCConnection(
host=config["server"]["host"],
port=config["server"]["port"],
@@ -94,7 +92,6 @@ class Bot:
self._started: float = time.monotonic()
self._tasks: set[asyncio.Task] = set()
self._reconnect_delay: float = 5.0
self._owner: list[str] = config.get("bot", {}).get("owner", [])
self._admins: list[str] = config.get("bot", {}).get("admins", [])
self._opers: set[str] = set() # hostmasks of known IRC operators
self._caps: set[str] = set() # negotiated IRCv3 caps
@@ -120,18 +117,10 @@ class Bot:
self._reconnect_delay = 5.0 # reset on clean run
except (OSError, ConnectionError) as exc:
log.error("connection lost: %s", exc)
if self._llm:
ts = time.strftime("%H:%M")
sys.stdout.write(f"{ts} --- disconnected: {exc}\n")
sys.stdout.flush()
if self._running:
jitter = self._reconnect_delay * 0.25 * (2 * random.random() - 1)
delay = self._reconnect_delay + jitter
log.info("reconnecting in %.0fs...", delay)
if self._llm:
ts = time.strftime("%H:%M")
sys.stdout.write(f"{ts} --- reconnecting in {delay:.0f}s\n")
sys.stdout.flush()
await asyncio.sleep(delay)
self._reconnect_delay = min(self._reconnect_delay * 2, 300.0)
@@ -270,19 +259,11 @@ class Bot:
# Protocol-level PING/PONG
if msg.command == "PING":
await self.conn.send(format_msg("PONG", msg.params[0] if msg.params else ""))
if self._llm:
ts = time.strftime("%H:%M")
sys.stdout.write(f"{ts} --- ping\n")
sys.stdout.flush()
return
# RPL_WELCOME (001) — join channels and WHO for oper detection
if msg.command == "001":
self.nick = msg.params[0] if msg.params else self.nick
if self._llm:
ts = time.strftime("%H:%M")
sys.stdout.write(f"{ts} --- connected as {self.nick}\n")
sys.stdout.flush()
for channel in self.config["bot"]["channels"]:
await self.join(channel)
await self.conn.send(format_msg("WHO", channel))
@@ -319,16 +300,6 @@ class Bot:
self._spawn(self._handle_ctcp(msg), name="ctcp")
return
# LLM mode: route PRIVMSG to stdout or info.log
if self._llm and msg.command == "PRIVMSG" and msg.text:
ts = time.strftime("%H:%M")
if self._is_addressed(msg) and self._is_owner(msg):
prefix = f"{msg.target} " if msg.is_channel else ""
sys.stdout.write(f"{ts} {prefix}<{msg.nick}> {msg.text}\n")
sys.stdout.flush()
else:
log.info("%s <%s> %s", msg.target, msg.nick, msg.text)
# Dispatch to event handlers (fire-and-forget)
channel = msg.target if msg.is_channel else None
event_type = msg.command
@@ -388,25 +359,14 @@ class Bot:
return True
return plugin_name in allowed
def _is_owner(self, msg: Message) -> bool:
"""Check if the sender matches a configured owner hostmask pattern."""
if not msg.prefix:
return False
for pattern in self._owner:
if fnmatch.fnmatch(msg.prefix, pattern):
return True
return False
def _is_admin(self, msg: Message) -> bool:
"""Check if the message sender is a bot admin.
Returns True if the sender is an owner, a known IRC operator,
or matches a configured admin hostmask pattern (fnmatch-style).
Returns True if the sender is a known IRC operator or matches
a configured hostmask pattern (fnmatch-style).
"""
if not msg.prefix:
return False
if self._is_owner(msg):
return True
if msg.prefix in self._opers:
return True
for pattern in self._admins:
@@ -414,18 +374,6 @@ class Bot:
return True
return False
def _is_addressed(self, msg: Message) -> bool:
"""Check if a message is addressed to the bot (DM or nick-prefixed)."""
if not msg.is_channel:
return True
text = (msg.text or "").lstrip()
nick_lower = self.nick.lower()
if text.lower().startswith(nick_lower):
rest = text[len(nick_lower):]
if rest and rest[0] in ":, ":
return True
return False
def _dispatch_command(self, msg: Message) -> None:
"""Check if a PRIVMSG is a bot command and spawn it."""
text = msg.text
@@ -505,16 +453,6 @@ class Bot:
for chunk in _split_utf8(line, max_text):
await self._bucket.acquire()
await self.conn.send(format_msg("PRIVMSG", target, chunk))
if self._llm:
ts = time.strftime("%H:%M")
display = chunk
if display.startswith("\x01ACTION ") and display.endswith("\x01"):
display = display[8:-1]
out = f"{ts} {target} * {self.nick} {display}"
else:
out = f"{ts} {target} <{self.nick}> {display}"
sys.stdout.write(out + "\n")
sys.stdout.flush()
async def reply(self, msg: Message, text: str) -> None:
"""Reply to the source of a message (channel or PM)."""

View File

@@ -33,11 +33,6 @@ def build_parser() -> argparse.ArgumentParser:
action="store_true",
help="enable debug logging",
)
p.add_argument(
"--llm",
action="store_true",
help="LLM mode: addressed messages to stdout, rest to info.log",
)
p.add_argument(
"--cprofile",
metavar="PATH",
@@ -116,14 +111,7 @@ def main(argv: list[str] | None = None) -> int:
level = logging.DEBUG if args.verbose else logging.INFO
log_fmt = config.get("logging", {}).get("format", "text")
if args.llm:
handler = logging.FileHandler("info.log")
if log_fmt == "json":
handler.setFormatter(JsonFormatter())
else:
handler.setFormatter(logging.Formatter(LOG_FORMAT, datefmt=LOG_DATE))
logging.basicConfig(handlers=[handler], level=level)
elif log_fmt == "json":
handler = logging.StreamHandler()
handler.setFormatter(JsonFormatter())
logging.basicConfig(handlers=[handler], level=level)
@@ -134,7 +122,7 @@ def main(argv: list[str] | None = None) -> int:
log.info("derp %s starting", __version__)
registry = PluginRegistry()
bot = Bot(config, registry, llm=args.llm)
bot = Bot(config, registry)
bot.load_plugins()
if args.tracemalloc: