jevmod 0.2.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 (38) hide show
  1. jevmod-0.2.0/LICENSE +21 -0
  2. jevmod-0.2.0/PKG-INFO +293 -0
  3. jevmod-0.2.0/README.md +249 -0
  4. jevmod-0.2.0/jevmod/__init__.py +67 -0
  5. jevmod-0.2.0/jevmod/__main__.py +50 -0
  6. jevmod-0.2.0/jevmod/adapters/__init__.py +0 -0
  7. jevmod-0.2.0/jevmod/adapters/discord_bot.py +324 -0
  8. jevmod-0.2.0/jevmod/adapters/reddit_bot.py +91 -0
  9. jevmod-0.2.0/jevmod/adapters/telegram_bot.py +206 -0
  10. jevmod-0.2.0/jevmod/api/__init__.py +0 -0
  11. jevmod-0.2.0/jevmod/api/demo.py +199 -0
  12. jevmod-0.2.0/jevmod/api/server.py +185 -0
  13. jevmod-0.2.0/jevmod/categories.json +69 -0
  14. jevmod-0.2.0/jevmod/cli.py +97 -0
  15. jevmod-0.2.0/jevmod/core/__init__.py +17 -0
  16. jevmod-0.2.0/jevmod/core/policy.py +155 -0
  17. jevmod-0.2.0/jevmod/core/service.py +117 -0
  18. jevmod-0.2.0/jevmod/core/store.py +215 -0
  19. jevmod-0.2.0/jevmod/judge.py +189 -0
  20. jevmod-0.2.0/jevmod/keys.py +146 -0
  21. jevmod-0.2.0/jevmod/mcp_server.py +76 -0
  22. jevmod-0.2.0/jevmod.egg-info/PKG-INFO +293 -0
  23. jevmod-0.2.0/jevmod.egg-info/SOURCES.txt +36 -0
  24. jevmod-0.2.0/jevmod.egg-info/dependency_links.txt +1 -0
  25. jevmod-0.2.0/jevmod.egg-info/entry_points.txt +2 -0
  26. jevmod-0.2.0/jevmod.egg-info/requires.txt +36 -0
  27. jevmod-0.2.0/jevmod.egg-info/top_level.txt +1 -0
  28. jevmod-0.2.0/pyproject.toml +52 -0
  29. jevmod-0.2.0/setup.cfg +4 -0
  30. jevmod-0.2.0/tests/test_api.py +77 -0
  31. jevmod-0.2.0/tests/test_cli.py +43 -0
  32. jevmod-0.2.0/tests/test_demo.py +63 -0
  33. jevmod-0.2.0/tests/test_examples.py +104 -0
  34. jevmod-0.2.0/tests/test_judge.py +71 -0
  35. jevmod-0.2.0/tests/test_keys.py +97 -0
  36. jevmod-0.2.0/tests/test_mcp.py +69 -0
  37. jevmod-0.2.0/tests/test_offline.py +97 -0
  38. jevmod-0.2.0/tests/test_redteam.py +137 -0
jevmod-0.2.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Omar Hernandez
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.
jevmod-0.2.0/PKG-INFO ADDED
@@ -0,0 +1,293 @@
1
+ Metadata-Version: 2.4
2
+ Name: jevmod
3
+ Version: 0.2.0
4
+ Summary: Moderation for communities and apps, powered by Jev: spam, scams, harassment, adult, off-topic and your own plain-language rules, with a probability per category and thresholds you own.
5
+ Author: Omar Hernandez
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/ohernandezdev/jevmod
8
+ Keywords: moderation,discord,telegram,reddit,content-moderation,typesafe,jev
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Topic :: Communications :: Chat
12
+ Requires-Python: >=3.10
13
+ Description-Content-Type: text/markdown
14
+ License-File: LICENSE
15
+ Requires-Dist: typesafe-sdk>=0.6
16
+ Requires-Dist: fastapi>=0.110
17
+ Requires-Dist: uvicorn[standard]>=0.29
18
+ Requires-Dist: pydantic>=2
19
+ Provides-Extra: discord
20
+ Requires-Dist: discord.py>=2.3; extra == "discord"
21
+ Provides-Extra: telegram
22
+ Requires-Dist: python-telegram-bot>=21; extra == "telegram"
23
+ Provides-Extra: reddit
24
+ Requires-Dist: praw>=7.7; extra == "reddit"
25
+ Provides-Extra: mcp
26
+ Requires-Dist: mcp>=2; extra == "mcp"
27
+ Provides-Extra: keys
28
+ Requires-Dist: keyring>=25; extra == "keys"
29
+ Provides-Extra: all
30
+ Requires-Dist: discord.py>=2.3; extra == "all"
31
+ Requires-Dist: python-telegram-bot>=21; extra == "all"
32
+ Requires-Dist: praw>=7.7; extra == "all"
33
+ Requires-Dist: mcp>=2; extra == "all"
34
+ Requires-Dist: keyring>=25; extra == "all"
35
+ Provides-Extra: examples
36
+ Requires-Dist: langchain-core>=0.3; extra == "examples"
37
+ Requires-Dist: httpx; extra == "examples"
38
+ Provides-Extra: dev
39
+ Requires-Dist: pytest; extra == "dev"
40
+ Requires-Dist: ruff; extra == "dev"
41
+ Requires-Dist: mypy; extra == "dev"
42
+ Requires-Dist: httpx; extra == "dev"
43
+ Dynamic: license-file
44
+
45
+ # jevmod
46
+
47
+ Moderation for communities and apps: every message gets a probability for **spam, scam, harassment, nsfw,
48
+ off-topic, self-harm, doxxing, sexual content involving minors**, and for **rules you write in plain English**.
49
+ You set the thresholds and the actions. Every decision is logged with its numbers.
50
+
51
+ It runs on [Jev](https://typesafe.ai), TypeSafe's System One model: you ask yes/no questions about a message and
52
+ get probabilities back, no text generation. About **$0.04 per 1,000 messages** with all categories on.
53
+
54
+ ![jevmod check and the HTTP API in a terminal](docs/jevmod.gif)
55
+
56
+ ```
57
+ $ jevmod check "FREE NITRO for the first 100!! claim at discord-gifts.ru/nitro"
58
+ scam 0.99 'FREE NITRO for the first 100!! claim at discord-gifts.ru/nitro' [scam 0.99, spam 0.98, harassment 0.02]
59
+ ```
60
+
61
+ Flag-only by default: nothing is deleted until you turn that on. Fails open: if Jev is unreachable, messages are
62
+ left alone and the failure is logged. Self-harm is flag-only by design so a moderator can reach out.
63
+
64
+ | you are | you get | start |
65
+ |---|---|---|
66
+ | a community owner, not technical | a Discord bot you tune with commands (Telegram with fewer commands, Reddit by env vars) | [Run the bot](#run-the-bot) |
67
+ | a developer | a CLI, a Python package, an npm package, or one HTTP call | [Developer](#developer) |
68
+ | a coding agent, or someone using one | an MCP server and a Claude Code skill that wires jevmod into a codebase | [Agents](#agents) |
69
+
70
+ On the [benchmark](BENCHMARK.md) (2,531 messages from OpenAI's moderation eval, Jigsaw and YouTube spam) jevmod
71
+ had the best AUROC in every category it was compared on in OpenAI's human-labelled set: harassment 0.93 and
72
+ sexual 0.98 against Llama Guard 3 8B, ShieldGemma 2B and toxic-bert; self-harm 0.99 and minors 0.98 against Llama
73
+ Guard, the only other system with those labels. Calibration was measured too; caveats are in the same file.
74
+
75
+ ## The key, once
76
+
77
+ Every surface needs a TypeSafe API key (free tier at [console.typesafe.ai](https://console.typesafe.ai)).
78
+
79
+ ```bash
80
+ pip install "jevmod[keys]"
81
+ jevmod init # asks for the key without echo, verifies it with one call, stores it in the OS keyring
82
+ ```
83
+
84
+ Resolution order everywhere (CLI, API, bots, MCP, `Moderator`): `TYPESAFE_API_KEY` in the environment, then the
85
+ keyring, then a `.env` in the current directory. In a container or a headless server `jevmod init --env-file`
86
+ writes `.env` with mode 600. `jevmod init --forget` removes the keyring entry (`pip uninstall` does not). Keys never
87
+ go into git; `.env` is ignored.
88
+
89
+ ## Run the bot
90
+
91
+ ### Discord
92
+
93
+ 1. [Developer Portal](https://discord.com/developers/applications) → New Application → Bot → **Reset Token** →
94
+ enable **Message Content Intent** (the only privileged intent used).
95
+ 2. OAuth2 → URL Generator → scopes `bot` + `applications.commands`; permissions: Read Messages, Send Messages,
96
+ Manage Messages, Moderate Members, Manage Channels, Embed Links, Add Reactions. Open the URL, add it to your server.
97
+ 3. `pip install "jevmod[discord]"`, set `DISCORD_TOKEN`, run `jevmod discord`.
98
+
99
+ The bot creates a private `#jevmod-log` channel and starts flagging there.
100
+
101
+ | command (server managers only) | what |
102
+ |---|---|
103
+ | `/mod status` | settings and this month's usage |
104
+ | `/mod set <category> <action> [threshold]` | any category → `off`, `flag`, `delete`, `timeout` |
105
+ | `/mod rule <name> <text> [action] [threshold]` | a rule in your words: "No politics. News about the game is fine." (max 5) |
106
+ | `/mod trust <role>` | messages from that role are never judged |
107
+ | `/mod topic <text>` | what the current channel is for (turns on `offtopic`) |
108
+ | `/mod log`, `/mod recent` | choose the log channel; last decisions with probabilities |
109
+ | `/mod forget`, `/mod forget_user @member` | delete everything stored about the server, or one member |
110
+
111
+ React ❌ on a log entry to mark a false positive (that category's threshold goes up a notch), ✅ to confirm a
112
+ correct call (down a notch, floor 0.5).
113
+
114
+ ### Telegram and Reddit
115
+
116
+ Telegram: [@BotFather](https://t.me/BotFather) → `/newbot`, make the bot a group admin, `pip install
117
+ "jevmod[telegram]"`, set `TELEGRAM_TOKEN`, run `jevmod telegram`. Admin commands: `/mod_status`, `/mod_set`,
118
+ `/mod_rule`, `/mod_topic`, `/mod_log`.
119
+
120
+ Reddit: for your own subreddit with your own "script" app credentials, non-commercial (Reddit's API terms).
121
+ `pip install "jevmod[reddit]"`, fill the `REDDIT_*` variables from `.env.example`, run `jevmod reddit`. Reports by
122
+ default; removal and bans are opt-in.
123
+
124
+ ### What the bots send where
125
+
126
+ Only the **message text** and the **channel topic** go to TypeSafe. Author names and ids never do. Locally,
127
+ jevmod keeps a decision log (category, probabilities, action, first 300 characters); rows older than 30 days are
128
+ purged on every batch.
129
+ `/mod forget` deletes everything; leaving the server does the same. Members whose message is removed get a direct
130
+ message saying an automated system did it and how to appeal.
131
+
132
+ ## Developer
133
+
134
+ ### CLI
135
+
136
+ ```bash
137
+ pip install jevmod
138
+ jevmod check "some text" # exit 0 clean, 1 something triggered, 2 error
139
+ cat comments.txt | jevmod check --json --rule "No politics. Game news is fine." -
140
+ ```
141
+
142
+ One message per line on stdin, one Jev request per 50. `--topic` turns on the off-topic check, `--threshold` sets
143
+ one for every category, `--json` prints one object per line with every probability. `examples/cli/` has a file
144
+ screener and a pre-commit hook.
145
+
146
+ ### Python
147
+
148
+ ```python
149
+ from jevmod import Moderator, Policy
150
+
151
+ d = Moderator().check("FREE NITRO for the first 100!! claim at discord-gifts.ru/nitro", channel_topic="gaming")
152
+ d.action, d.category, d.probability # ('flag', 'scam', 0.99)
153
+ d.scores # {'spam': 0.98, 'scam': 0.99, 'harassment': 0.02, 'nsfw': 0.01, ...}
154
+
155
+ p = Policy()
156
+ p.set_category("scam", "delete", 0.7)
157
+ p.set_rule("no_politics", "No political discussion. Game news is fine.", action="flag", threshold=0.8)
158
+ Moderator(policy=p).check_many(["...", "..."], channel_topic="support") # one request for the batch
159
+ ```
160
+
161
+ ### npm
162
+
163
+ ```ts
164
+ import { Moderator, Policy } from "jevmod";
165
+ const d = await new Moderator().check("FREE NITRO ...", { channelTopic: "gaming" });
166
+ d.action, d.category, d.scores // same shape as Python and the HTTP API
167
+ ```
168
+
169
+ Node 20+. Same questions (`jevmod/categories.json` is copied byte for byte and CI fails if it drifts), same
170
+ policy, same decision. `JevmodClient` talks to a deployed HTTP API instead, so browsers and edge functions never
171
+ hold the TypeSafe key. Express middleware in `packages/jevmod-js/examples/`. Details in
172
+ [packages/jevmod-js/README.md](packages/jevmod-js/README.md).
173
+
174
+ ### HTTP API
175
+
176
+ ```bash
177
+ JEVMOD_ADMIN_TOKEN=... jevmod api # or: docker compose up -d
178
+ curl -X POST localhost:8080/v1/keys -H "Authorization: Bearer $JEVMOD_ADMIN_TOKEN" \
179
+ -H "Content-Type: application/json" -d '{"tenant":"my-app"}' # {"api_key":"jm_...", shown once}
180
+ curl -X POST localhost:8080/v1/moderate -H "Authorization: Bearer jm_..." -H "Content-Type: application/json" \
181
+ -d '{"messages":[{"id":"a","text":"FREE NITRO for the first 100!! claim at discord-gifts.ru/nitro"}]}'
182
+ ```
183
+
184
+ | endpoint | what |
185
+ |---|---|
186
+ | `POST /v1/moderate` | up to 50 messages → decisions; the `X-Request-Id` you send comes back as `request_id` and as a response header |
187
+ | `GET/PUT /v1/policy` | thresholds, actions, rules for this tenant |
188
+ | `GET /v1/decisions` | the audit log |
189
+ | `DELETE /v1/tenant` | forget this tenant |
190
+ | `POST /v1/keys` (admin) | mint a tenant key, stored hashed |
191
+ | `GET /v1/health`, `GET /metrics` | liveness, Prometheus counters |
192
+
193
+ OpenAPI at `/docs`, a Postman collection in `postman/`. Any chatbot, forum or comment system that can make an HTTP
194
+ call can use it; the bots are adapters over the same service.
195
+
196
+ ### Examples, one folder per surface
197
+
198
+ | folder | what |
199
+ |---|---|
200
+ | [`examples/sdk/`](examples/sdk) | `Moderator` basics, a custom policy with a rule, batching 50 per request |
201
+ | [`examples/cli/`](examples/cli) | `screen_file.sh` exits 1 on hits; `pre-commit.sh` blocks flagged text files |
202
+ | [`examples/api/`](examples/api) | curl, a stdlib Python client, a Node client against a local `jevmod api` |
203
+ | [`examples/discord/`](examples/discord) | run the bot; `custom_adapter.py` puts any chat platform on `ModerationService` in 20 lines |
204
+ | [`examples/agent_harness/`](examples/agent_harness) | `@guarded` decorator, Claude Agent SDK `PreToolUse`/`PostToolUse` hooks, LangChain callback |
205
+ | [`examples/input_validation/`](examples/input_validation) | FastAPI dependency that answers 422, a pydantic `ModeratedText` field |
206
+ | [`packages/jevmod-js/examples/`](packages/jevmod-js/examples) | Express middleware and a plain Node script |
207
+
208
+ ## Agents
209
+
210
+ **MCP server.** `pip install "jevmod[mcp]"` then `jevmod mcp` (stdio). Tools: `moderate(texts, channel_topic?,
211
+ rules?)` returns one decision per text; `categories()` describes each category and its default threshold.
212
+
213
+ ```
214
+ claude mcp add jevmod -- jevmod mcp # Claude Code
215
+ {"mcpServers": {"jevmod": {"command": "jevmod", "args": ["mcp"]}}} # Cursor, Codex, others
216
+ ```
217
+
218
+ **Claude Code plugin.** This repository is its own marketplace:
219
+
220
+ ```
221
+ /plugin marketplace add ohernandezdev/jevmod
222
+ /plugin install jevmod@jevmod
223
+ ```
224
+
225
+ `jevmod-integrate` adds moderation to an existing codebase (detects the stack, picks SDK/npm/HTTP/MCP, wires the
226
+ key, inserts the call where it belongs, adds a real test). `jevmod-moderate` screens text or datasets from the
227
+ terminal while working. [AGENTS.md](AGENTS.md) has every signature; [docs/llms.txt](docs/llms.txt) indexes the docs.
228
+
229
+ ## Categories
230
+
231
+ | category | true when | default |
232
+ |---|---|---|
233
+ | `spam` | unsolicited promotion, invite farming, bare link drops, mass mentions | flag ≥ 0.85 |
234
+ | `scam` | fake giveaways, phishing domains, impersonated support, "DM me for a deal" | flag ≥ 0.75 |
235
+ | `harassment` | insults, slurs, threats, targeted abuse, in any language | flag ≥ 0.75 |
236
+ | `nsfw` | sexual or gore content for a general audience (below the threshold means SFW) | flag ≥ 0.80 |
237
+ | `offtopic` | unrelated to `channel_topic`; needs a topic to mean anything | off, 0.90 |
238
+ | `selfharm` | the author is in crisis or considering self-harm; alert moderators, never punish | flag ≥ 0.80 |
239
+ | `doxxing` | reveals or hunts private data about a real person | flag ≥ 0.80 |
240
+ | `minors` | sexualises a minor or shows grooming behaviour | flag ≥ 0.70 |
241
+ | `rule:<name>` | your rule in plain language, exceptions included, up to 5 | flag ≥ 0.80 |
242
+
243
+ Every check returns all enabled categories at once, in one request. The questions are in
244
+ [`jevmod/categories.json`](jevmod/categories.json): one yes/no question per category with explicit true/false
245
+ criteria, the pattern of TypeSafe's guardrails cookbook. Jev's probabilities move about ±0.03 between runs, so
246
+ anything within that band of a threshold will flip; the ❌/✅ feedback and `PUT /v1/policy` exist to move the line.
247
+
248
+ ## How it works
249
+
250
+ ![architecture](docs/diagrams/architecture.svg)
251
+
252
+ Message → pre-filter (trusted authors, under eight letters without a link, repeats of judged text never reach
253
+ Jev) → batch for 2 s per community → **one** Jev request for the batch → probabilities → policy → action → audit
254
+ log. Batched messages are sent as a dict keyed by position; as a list, probabilities leaked between neighbours,
255
+ which the 98-message adversarial [red team](tests/data/redteam.csv) caught and which now runs as a regression
256
+ suite in CI. More in [docs/diagrams/](docs/diagrams) and [PLAN.md](PLAN.md).
257
+
258
+ ## Self-host
259
+
260
+ One image, one variable picks the role: `api`, `discord`, `telegram`, `reddit`. SQLite on a volume.
261
+
262
+ ```bash
263
+ cp .env.example .env && docker compose up -d # API on :8080
264
+ docker compose --profile discord up -d # add the Discord bot
265
+ ```
266
+
267
+ A $4/month VM, Fly.io or Railway with a volume is enough. Failure policy: Jev unreachable → decisions come back
268
+ `reason="error_open"` and nothing is acted on. There is no quota by default; `JEVMOD_MONTHLY_QUOTA=5000` pauses
269
+ judging for a tenant after 5,000 judged messages in a month as a cost guard, tells the owner once, deletes nothing.
270
+
271
+ ## Public demo
272
+
273
+ The landing page's live check talks to `jevmod demo`, a separate role that keeps the key on the server and stops
274
+ at a monthly budget (`JEVMOD_DEMO_BUDGET_USD`, default $0.50), with per-visitor limits and a CORS allow-list. It
275
+ logs what visitors try (text, scores, hashed IP) for the operator. `deploy/demo/` has a Caddy + Docker compose
276
+ for a small VPS with HTTPS in two commands.
277
+
278
+ ## Development
279
+
280
+ ```bash
281
+ git clone https://github.com/ohernandezdev/jevmod && cd jevmod
282
+ python -m venv .venv && .venv/bin/pip install -e ".[all,dev,examples]" # Windows: .venv\Scripts\pip
283
+ ruff check . && mypy jevmod && pytest # offline tests run without a key; the rest hit the real API
284
+ cd packages/jevmod-js && npm ci && npm test # same for the npm package
285
+ ```
286
+
287
+ Tests never mock Jev. `tests/test_redteam.py` is the adversarial regression set; `benchmark/` reproduces
288
+ [BENCHMARK.md](BENCHMARK.md).
289
+
290
+ ## License
291
+
292
+ MIT, © Omar Hernandez. See [DISCLAIMER.md](DISCLAIMER.md): decisions are probabilistic, the operator owns the
293
+ thresholds, the actions and legal compliance; not affiliated with TypeSafe, Discord, Telegram or Reddit.
jevmod-0.2.0/README.md ADDED
@@ -0,0 +1,249 @@
1
+ # jevmod
2
+
3
+ Moderation for communities and apps: every message gets a probability for **spam, scam, harassment, nsfw,
4
+ off-topic, self-harm, doxxing, sexual content involving minors**, and for **rules you write in plain English**.
5
+ You set the thresholds and the actions. Every decision is logged with its numbers.
6
+
7
+ It runs on [Jev](https://typesafe.ai), TypeSafe's System One model: you ask yes/no questions about a message and
8
+ get probabilities back, no text generation. About **$0.04 per 1,000 messages** with all categories on.
9
+
10
+ ![jevmod check and the HTTP API in a terminal](docs/jevmod.gif)
11
+
12
+ ```
13
+ $ jevmod check "FREE NITRO for the first 100!! claim at discord-gifts.ru/nitro"
14
+ scam 0.99 'FREE NITRO for the first 100!! claim at discord-gifts.ru/nitro' [scam 0.99, spam 0.98, harassment 0.02]
15
+ ```
16
+
17
+ Flag-only by default: nothing is deleted until you turn that on. Fails open: if Jev is unreachable, messages are
18
+ left alone and the failure is logged. Self-harm is flag-only by design so a moderator can reach out.
19
+
20
+ | you are | you get | start |
21
+ |---|---|---|
22
+ | a community owner, not technical | a Discord bot you tune with commands (Telegram with fewer commands, Reddit by env vars) | [Run the bot](#run-the-bot) |
23
+ | a developer | a CLI, a Python package, an npm package, or one HTTP call | [Developer](#developer) |
24
+ | a coding agent, or someone using one | an MCP server and a Claude Code skill that wires jevmod into a codebase | [Agents](#agents) |
25
+
26
+ On the [benchmark](BENCHMARK.md) (2,531 messages from OpenAI's moderation eval, Jigsaw and YouTube spam) jevmod
27
+ had the best AUROC in every category it was compared on in OpenAI's human-labelled set: harassment 0.93 and
28
+ sexual 0.98 against Llama Guard 3 8B, ShieldGemma 2B and toxic-bert; self-harm 0.99 and minors 0.98 against Llama
29
+ Guard, the only other system with those labels. Calibration was measured too; caveats are in the same file.
30
+
31
+ ## The key, once
32
+
33
+ Every surface needs a TypeSafe API key (free tier at [console.typesafe.ai](https://console.typesafe.ai)).
34
+
35
+ ```bash
36
+ pip install "jevmod[keys]"
37
+ jevmod init # asks for the key without echo, verifies it with one call, stores it in the OS keyring
38
+ ```
39
+
40
+ Resolution order everywhere (CLI, API, bots, MCP, `Moderator`): `TYPESAFE_API_KEY` in the environment, then the
41
+ keyring, then a `.env` in the current directory. In a container or a headless server `jevmod init --env-file`
42
+ writes `.env` with mode 600. `jevmod init --forget` removes the keyring entry (`pip uninstall` does not). Keys never
43
+ go into git; `.env` is ignored.
44
+
45
+ ## Run the bot
46
+
47
+ ### Discord
48
+
49
+ 1. [Developer Portal](https://discord.com/developers/applications) → New Application → Bot → **Reset Token** →
50
+ enable **Message Content Intent** (the only privileged intent used).
51
+ 2. OAuth2 → URL Generator → scopes `bot` + `applications.commands`; permissions: Read Messages, Send Messages,
52
+ Manage Messages, Moderate Members, Manage Channels, Embed Links, Add Reactions. Open the URL, add it to your server.
53
+ 3. `pip install "jevmod[discord]"`, set `DISCORD_TOKEN`, run `jevmod discord`.
54
+
55
+ The bot creates a private `#jevmod-log` channel and starts flagging there.
56
+
57
+ | command (server managers only) | what |
58
+ |---|---|
59
+ | `/mod status` | settings and this month's usage |
60
+ | `/mod set <category> <action> [threshold]` | any category → `off`, `flag`, `delete`, `timeout` |
61
+ | `/mod rule <name> <text> [action] [threshold]` | a rule in your words: "No politics. News about the game is fine." (max 5) |
62
+ | `/mod trust <role>` | messages from that role are never judged |
63
+ | `/mod topic <text>` | what the current channel is for (turns on `offtopic`) |
64
+ | `/mod log`, `/mod recent` | choose the log channel; last decisions with probabilities |
65
+ | `/mod forget`, `/mod forget_user @member` | delete everything stored about the server, or one member |
66
+
67
+ React ❌ on a log entry to mark a false positive (that category's threshold goes up a notch), ✅ to confirm a
68
+ correct call (down a notch, floor 0.5).
69
+
70
+ ### Telegram and Reddit
71
+
72
+ Telegram: [@BotFather](https://t.me/BotFather) → `/newbot`, make the bot a group admin, `pip install
73
+ "jevmod[telegram]"`, set `TELEGRAM_TOKEN`, run `jevmod telegram`. Admin commands: `/mod_status`, `/mod_set`,
74
+ `/mod_rule`, `/mod_topic`, `/mod_log`.
75
+
76
+ Reddit: for your own subreddit with your own "script" app credentials, non-commercial (Reddit's API terms).
77
+ `pip install "jevmod[reddit]"`, fill the `REDDIT_*` variables from `.env.example`, run `jevmod reddit`. Reports by
78
+ default; removal and bans are opt-in.
79
+
80
+ ### What the bots send where
81
+
82
+ Only the **message text** and the **channel topic** go to TypeSafe. Author names and ids never do. Locally,
83
+ jevmod keeps a decision log (category, probabilities, action, first 300 characters); rows older than 30 days are
84
+ purged on every batch.
85
+ `/mod forget` deletes everything; leaving the server does the same. Members whose message is removed get a direct
86
+ message saying an automated system did it and how to appeal.
87
+
88
+ ## Developer
89
+
90
+ ### CLI
91
+
92
+ ```bash
93
+ pip install jevmod
94
+ jevmod check "some text" # exit 0 clean, 1 something triggered, 2 error
95
+ cat comments.txt | jevmod check --json --rule "No politics. Game news is fine." -
96
+ ```
97
+
98
+ One message per line on stdin, one Jev request per 50. `--topic` turns on the off-topic check, `--threshold` sets
99
+ one for every category, `--json` prints one object per line with every probability. `examples/cli/` has a file
100
+ screener and a pre-commit hook.
101
+
102
+ ### Python
103
+
104
+ ```python
105
+ from jevmod import Moderator, Policy
106
+
107
+ d = Moderator().check("FREE NITRO for the first 100!! claim at discord-gifts.ru/nitro", channel_topic="gaming")
108
+ d.action, d.category, d.probability # ('flag', 'scam', 0.99)
109
+ d.scores # {'spam': 0.98, 'scam': 0.99, 'harassment': 0.02, 'nsfw': 0.01, ...}
110
+
111
+ p = Policy()
112
+ p.set_category("scam", "delete", 0.7)
113
+ p.set_rule("no_politics", "No political discussion. Game news is fine.", action="flag", threshold=0.8)
114
+ Moderator(policy=p).check_many(["...", "..."], channel_topic="support") # one request for the batch
115
+ ```
116
+
117
+ ### npm
118
+
119
+ ```ts
120
+ import { Moderator, Policy } from "jevmod";
121
+ const d = await new Moderator().check("FREE NITRO ...", { channelTopic: "gaming" });
122
+ d.action, d.category, d.scores // same shape as Python and the HTTP API
123
+ ```
124
+
125
+ Node 20+. Same questions (`jevmod/categories.json` is copied byte for byte and CI fails if it drifts), same
126
+ policy, same decision. `JevmodClient` talks to a deployed HTTP API instead, so browsers and edge functions never
127
+ hold the TypeSafe key. Express middleware in `packages/jevmod-js/examples/`. Details in
128
+ [packages/jevmod-js/README.md](packages/jevmod-js/README.md).
129
+
130
+ ### HTTP API
131
+
132
+ ```bash
133
+ JEVMOD_ADMIN_TOKEN=... jevmod api # or: docker compose up -d
134
+ curl -X POST localhost:8080/v1/keys -H "Authorization: Bearer $JEVMOD_ADMIN_TOKEN" \
135
+ -H "Content-Type: application/json" -d '{"tenant":"my-app"}' # {"api_key":"jm_...", shown once}
136
+ curl -X POST localhost:8080/v1/moderate -H "Authorization: Bearer jm_..." -H "Content-Type: application/json" \
137
+ -d '{"messages":[{"id":"a","text":"FREE NITRO for the first 100!! claim at discord-gifts.ru/nitro"}]}'
138
+ ```
139
+
140
+ | endpoint | what |
141
+ |---|---|
142
+ | `POST /v1/moderate` | up to 50 messages → decisions; the `X-Request-Id` you send comes back as `request_id` and as a response header |
143
+ | `GET/PUT /v1/policy` | thresholds, actions, rules for this tenant |
144
+ | `GET /v1/decisions` | the audit log |
145
+ | `DELETE /v1/tenant` | forget this tenant |
146
+ | `POST /v1/keys` (admin) | mint a tenant key, stored hashed |
147
+ | `GET /v1/health`, `GET /metrics` | liveness, Prometheus counters |
148
+
149
+ OpenAPI at `/docs`, a Postman collection in `postman/`. Any chatbot, forum or comment system that can make an HTTP
150
+ call can use it; the bots are adapters over the same service.
151
+
152
+ ### Examples, one folder per surface
153
+
154
+ | folder | what |
155
+ |---|---|
156
+ | [`examples/sdk/`](examples/sdk) | `Moderator` basics, a custom policy with a rule, batching 50 per request |
157
+ | [`examples/cli/`](examples/cli) | `screen_file.sh` exits 1 on hits; `pre-commit.sh` blocks flagged text files |
158
+ | [`examples/api/`](examples/api) | curl, a stdlib Python client, a Node client against a local `jevmod api` |
159
+ | [`examples/discord/`](examples/discord) | run the bot; `custom_adapter.py` puts any chat platform on `ModerationService` in 20 lines |
160
+ | [`examples/agent_harness/`](examples/agent_harness) | `@guarded` decorator, Claude Agent SDK `PreToolUse`/`PostToolUse` hooks, LangChain callback |
161
+ | [`examples/input_validation/`](examples/input_validation) | FastAPI dependency that answers 422, a pydantic `ModeratedText` field |
162
+ | [`packages/jevmod-js/examples/`](packages/jevmod-js/examples) | Express middleware and a plain Node script |
163
+
164
+ ## Agents
165
+
166
+ **MCP server.** `pip install "jevmod[mcp]"` then `jevmod mcp` (stdio). Tools: `moderate(texts, channel_topic?,
167
+ rules?)` returns one decision per text; `categories()` describes each category and its default threshold.
168
+
169
+ ```
170
+ claude mcp add jevmod -- jevmod mcp # Claude Code
171
+ {"mcpServers": {"jevmod": {"command": "jevmod", "args": ["mcp"]}}} # Cursor, Codex, others
172
+ ```
173
+
174
+ **Claude Code plugin.** This repository is its own marketplace:
175
+
176
+ ```
177
+ /plugin marketplace add ohernandezdev/jevmod
178
+ /plugin install jevmod@jevmod
179
+ ```
180
+
181
+ `jevmod-integrate` adds moderation to an existing codebase (detects the stack, picks SDK/npm/HTTP/MCP, wires the
182
+ key, inserts the call where it belongs, adds a real test). `jevmod-moderate` screens text or datasets from the
183
+ terminal while working. [AGENTS.md](AGENTS.md) has every signature; [docs/llms.txt](docs/llms.txt) indexes the docs.
184
+
185
+ ## Categories
186
+
187
+ | category | true when | default |
188
+ |---|---|---|
189
+ | `spam` | unsolicited promotion, invite farming, bare link drops, mass mentions | flag ≥ 0.85 |
190
+ | `scam` | fake giveaways, phishing domains, impersonated support, "DM me for a deal" | flag ≥ 0.75 |
191
+ | `harassment` | insults, slurs, threats, targeted abuse, in any language | flag ≥ 0.75 |
192
+ | `nsfw` | sexual or gore content for a general audience (below the threshold means SFW) | flag ≥ 0.80 |
193
+ | `offtopic` | unrelated to `channel_topic`; needs a topic to mean anything | off, 0.90 |
194
+ | `selfharm` | the author is in crisis or considering self-harm; alert moderators, never punish | flag ≥ 0.80 |
195
+ | `doxxing` | reveals or hunts private data about a real person | flag ≥ 0.80 |
196
+ | `minors` | sexualises a minor or shows grooming behaviour | flag ≥ 0.70 |
197
+ | `rule:<name>` | your rule in plain language, exceptions included, up to 5 | flag ≥ 0.80 |
198
+
199
+ Every check returns all enabled categories at once, in one request. The questions are in
200
+ [`jevmod/categories.json`](jevmod/categories.json): one yes/no question per category with explicit true/false
201
+ criteria, the pattern of TypeSafe's guardrails cookbook. Jev's probabilities move about ±0.03 between runs, so
202
+ anything within that band of a threshold will flip; the ❌/✅ feedback and `PUT /v1/policy` exist to move the line.
203
+
204
+ ## How it works
205
+
206
+ ![architecture](docs/diagrams/architecture.svg)
207
+
208
+ Message → pre-filter (trusted authors, under eight letters without a link, repeats of judged text never reach
209
+ Jev) → batch for 2 s per community → **one** Jev request for the batch → probabilities → policy → action → audit
210
+ log. Batched messages are sent as a dict keyed by position; as a list, probabilities leaked between neighbours,
211
+ which the 98-message adversarial [red team](tests/data/redteam.csv) caught and which now runs as a regression
212
+ suite in CI. More in [docs/diagrams/](docs/diagrams) and [PLAN.md](PLAN.md).
213
+
214
+ ## Self-host
215
+
216
+ One image, one variable picks the role: `api`, `discord`, `telegram`, `reddit`. SQLite on a volume.
217
+
218
+ ```bash
219
+ cp .env.example .env && docker compose up -d # API on :8080
220
+ docker compose --profile discord up -d # add the Discord bot
221
+ ```
222
+
223
+ A $4/month VM, Fly.io or Railway with a volume is enough. Failure policy: Jev unreachable → decisions come back
224
+ `reason="error_open"` and nothing is acted on. There is no quota by default; `JEVMOD_MONTHLY_QUOTA=5000` pauses
225
+ judging for a tenant after 5,000 judged messages in a month as a cost guard, tells the owner once, deletes nothing.
226
+
227
+ ## Public demo
228
+
229
+ The landing page's live check talks to `jevmod demo`, a separate role that keeps the key on the server and stops
230
+ at a monthly budget (`JEVMOD_DEMO_BUDGET_USD`, default $0.50), with per-visitor limits and a CORS allow-list. It
231
+ logs what visitors try (text, scores, hashed IP) for the operator. `deploy/demo/` has a Caddy + Docker compose
232
+ for a small VPS with HTTPS in two commands.
233
+
234
+ ## Development
235
+
236
+ ```bash
237
+ git clone https://github.com/ohernandezdev/jevmod && cd jevmod
238
+ python -m venv .venv && .venv/bin/pip install -e ".[all,dev,examples]" # Windows: .venv\Scripts\pip
239
+ ruff check . && mypy jevmod && pytest # offline tests run without a key; the rest hit the real API
240
+ cd packages/jevmod-js && npm ci && npm test # same for the npm package
241
+ ```
242
+
243
+ Tests never mock Jev. `tests/test_redteam.py` is the adversarial regression set; `benchmark/` reproduces
244
+ [BENCHMARK.md](BENCHMARK.md).
245
+
246
+ ## License
247
+
248
+ MIT, © Omar Hernandez. See [DISCLAIMER.md](DISCLAIMER.md): decisions are probabilistic, the operator owns the
249
+ thresholds, the actions and legal compliance; not affiliated with TypeSafe, Discord, Telegram or Reddit.
@@ -0,0 +1,67 @@
1
+ """jevmod: moderation for communities and apps, powered by Jev.
2
+
3
+ Developer API, three lines:
4
+
5
+ from jevmod import Moderator
6
+ mod = Moderator() # TYPESAFE_API_KEY in the environment
7
+ d = mod.check("FREE NITRO click discord-gifts.ru") # -> Decision(action="flag", category="scam", probability=0.97)
8
+
9
+ `check_many([...])` judges a batch in one request. Thresholds and actions come from a `Policy` you can pass in.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from collections.abc import Sequence
15
+
16
+ from .core import ACTIONS, Decision, ModerationService, Policy, Store, decide
17
+ from .judge import CATEGORIES, Judge, Message, Verdict
18
+
19
+ __all__ = [
20
+ "ACTIONS",
21
+ "CATEGORIES",
22
+ "Decision",
23
+ "Judge",
24
+ "Message",
25
+ "ModerationService",
26
+ "Moderator",
27
+ "Policy",
28
+ "Store",
29
+ "Verdict",
30
+ "decide",
31
+ ]
32
+
33
+ try:
34
+ from importlib.metadata import version as _v
35
+
36
+ __version__ = _v("jevmod")
37
+ except Exception: # pragma: no cover
38
+ __version__ = "0.0.0+local"
39
+
40
+
41
+ class Moderator:
42
+ """Stateless convenience wrapper for developers: no SQLite, no tenants, just judge + policy."""
43
+
44
+ def __init__(self, policy: Policy | None = None, judge: Judge | None = None) -> None:
45
+ self.policy = policy or Policy()
46
+ self.judge = judge or Judge()
47
+
48
+ def check(self, text: str, *, author: str = "", channel_topic: str = "", author_trusted: bool = False) -> Decision:
49
+ return self.check_many([text], author=author, channel_topic=channel_topic, author_trusted=author_trusted)[0]
50
+
51
+ def check_many(
52
+ self,
53
+ texts: Sequence[str],
54
+ *,
55
+ author: str = "",
56
+ channel_topic: str = "",
57
+ author_trusted: bool = False,
58
+ ids: Sequence[str] | None = None,
59
+ ) -> list[Decision]:
60
+ msgs = [
61
+ Message(
62
+ ids[i] if ids else str(i), t, author=author, channel_topic=channel_topic, author_trusted=author_trusted
63
+ )
64
+ for i, t in enumerate(texts)
65
+ ]
66
+ verdicts = self.judge.judge(msgs, self.policy.enabled_categories(), self.policy.rules)
67
+ return [decide(self.policy, v) for v in verdicts]