zero-slop 2.12.15 → 2.12.18
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.
- package/README.md +2 -2
- package/SECURITY.md +12 -0
- package/SKILL.md +1 -1
- package/docs/cli.md +1 -1
- package/gemini-extension.json +1 -1
- package/package.json +4 -1
- package/plugin.json +1 -1
- package/scripts/calibrate.py +7 -2
- package/scripts/learn.py +58 -16
- package/scripts/slopscore.py +5 -0
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@ Zero Slop is a free, open-source agent skill that finds and removes AI slop whil
|
|
|
14
14
|
|
|
15
15
|
<p align="center">
|
|
16
16
|
<a href="https://github.com/manavmishra/ZeroSlop/actions/workflows/validate.yml"><img alt="Validate" src="https://github.com/manavmishra/ZeroSlop/actions/workflows/validate.yml/badge.svg"></a>
|
|
17
|
-
<img alt="Version 2.12.
|
|
17
|
+
<img alt="Version 2.12.18" src="https://img.shields.io/badge/version-2.12.18-72528F?color=C15732">
|
|
18
18
|
<a href="https://www.npmjs.com/package/zero-slop"><img alt="npm version" src="https://img.shields.io/npm/v/zero-slop?color=C15732"></a>
|
|
19
19
|
<a href="https://www.npmjs.com/package/zero-slop"><img alt="npm downloads" src="https://img.shields.io/npm/dm/zero-slop?color=17634F"></a>
|
|
20
20
|
<a href="https://github.com/manavmishra/ZeroSlop/stargazers"><img alt="GitHub stars" src="https://img.shields.io/github/stars/manavmishra/ZeroSlop?style=flat&color=C15732"></a>
|
|
@@ -176,7 +176,7 @@ python3 scripts/slopscore.py --batch drafts/ --gate 25
|
|
|
176
176
|
The CLI sends a file to the hosted editor without changing the file on disk:
|
|
177
177
|
|
|
178
178
|
```sh
|
|
179
|
-
npx --yes zero-slop@2.12.
|
|
179
|
+
npx --yes zero-slop@2.12.18 deslop draft.md --genre professional
|
|
180
180
|
```
|
|
181
181
|
|
|
182
182
|
Use `-` for stdin and `--json` for structured output. `--require-approved` prints the result but exits nonzero when review is needed. Requires Node.js 22+; offline `score` also needs Python 3. [CLI options and privacy](docs/cli.md).
|
package/SECURITY.md
CHANGED
|
@@ -17,6 +17,18 @@ The installed skill ships ten standard-library Python modules:
|
|
|
17
17
|
| `scripts/safeio.py` | locks and atomic file replacement | only on behalf of the two writers above | none |
|
|
18
18
|
| `scripts/version_check.py` | optional release check | none | one metadata-only GitHub API request |
|
|
19
19
|
|
|
20
|
+
The Anthropic plugin folder additionally carries three host execution adapters:
|
|
21
|
+
`mcp_stdio.py`, `mcp_local_tools.py`, and `mcp_workflow_tools.py`. They expose the
|
|
22
|
+
existing local checks through a declared stdio MCP server; they do not run shell
|
|
23
|
+
commands or accept caller-selected file paths. Learning writes still require
|
|
24
|
+
affirmative consent, with separate consent for activation. The optional version
|
|
25
|
+
tool uses the same metadata-only release check. These adapters and their routing
|
|
26
|
+
overlay are absent from the portable skill, npm package, and OpenAI ZIP. The
|
|
27
|
+
Anthropic folder retains the separate remote connector for explicitly requested
|
|
28
|
+
hosted editing; local checks do not automatically invoke it.
|
|
29
|
+
The declared Python entry point in a repository subfolder can be held for manual
|
|
30
|
+
directory review. A declaration or local test does not establish scan approval.
|
|
31
|
+
|
|
20
32
|
These local Python checks do not transmit the draft. Editing inside an assistant
|
|
21
33
|
follows that assistant's privacy settings. The version checker sends only a GET
|
|
22
34
|
for the latest public release tag, times out after 2.5 seconds, fails open, and can be
|
package/SKILL.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: zero-slop
|
|
3
3
|
license: MIT
|
|
4
4
|
metadata:
|
|
5
|
-
version: "2.12.
|
|
5
|
+
version: "2.12.18"
|
|
6
6
|
author: manavmishra
|
|
7
7
|
description: Edit drafts into natural prose, inspect AI-sounding patterns, or review how a specified audience might respond passage by passage. Zero Slop runs inside the user's existing AI assistant with local tools that protect source details. Use for humanizing or de-slopping writing, polishing outward-facing prose, social drafts, final editorial checks, or an explicit simulated reader review. Preserve facts, voice and format; reader simulations are hypotheses, not human feedback.
|
|
8
8
|
---
|
package/docs/cli.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
Use `deslop` for hosted editing with MCP parity. Use `score` for offline checks.
|
|
4
4
|
|
|
5
5
|
```sh
|
|
6
|
-
npm install --global zero-slop@2.12.
|
|
6
|
+
npm install --global zero-slop@2.12.18
|
|
7
7
|
zero-slop deslop draft.md --genre professional
|
|
8
8
|
zero-slop deslop - --genre email < draft.txt
|
|
9
9
|
zero-slop deslop draft.md --json --require-approved
|
package/gemini-extension.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zero-slop",
|
|
3
|
-
"version": "2.12.
|
|
3
|
+
"version": "2.12.18",
|
|
4
4
|
"description": "An editing skill, offline writing checks, and a CLI for the hosted Zero Slop MCP pipeline. Find stock AI phrasing, rewrite, and check source details.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"zero-slop": "bin/zero-slop.mjs"
|
|
@@ -56,6 +56,9 @@
|
|
|
56
56
|
"!scripts/build_onepager_pdf.py",
|
|
57
57
|
"!scripts/build_plugin.py",
|
|
58
58
|
"!scripts/build_marketplace_package.py",
|
|
59
|
+
"!scripts/mcp_stdio.py",
|
|
60
|
+
"!scripts/mcp_local_tools.py",
|
|
61
|
+
"!scripts/mcp_workflow_tools.py",
|
|
59
62
|
"!scripts/build_skill_zip.py",
|
|
60
63
|
"!scripts/check_svg.py",
|
|
61
64
|
"!scripts/check_distribution_manifests.py",
|
package/plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
|
|
3
3
|
"name": "zero-slop",
|
|
4
|
-
"version": "2.12.
|
|
4
|
+
"version": "2.12.18",
|
|
5
5
|
"description": "Find and fix canned AI writing while preserving the facts, voice, and format.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Manav Mishra",
|
package/scripts/calibrate.py
CHANGED
|
@@ -95,8 +95,13 @@ def freqs(texts):
|
|
|
95
95
|
|
|
96
96
|
|
|
97
97
|
def excess_weights(human_dir, ai_dir):
|
|
98
|
-
|
|
99
|
-
|
|
98
|
+
return excess_weights_from_texts(read_corpus(human_dir), read_corpus(ai_dir))
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def excess_weights_from_texts(human_texts, ai_texts):
|
|
102
|
+
"""Same frequency comparison for supplied texts; no paths or persistence."""
|
|
103
|
+
hc, hn = freqs(human_texts)
|
|
104
|
+
ac, an = freqs(ai_texts)
|
|
100
105
|
out = {}
|
|
101
106
|
for term, n_ai in ac.items():
|
|
102
107
|
if n_ai < MIN_OBS or len(term) < 4:
|
package/scripts/learn.py
CHANGED
|
@@ -656,8 +656,8 @@ def diff_spans(produced, shipped):
|
|
|
656
656
|
return out
|
|
657
657
|
|
|
658
658
|
|
|
659
|
-
def
|
|
660
|
-
"""
|
|
659
|
+
def read_edit_feedback(path):
|
|
660
|
+
"""Read a bounded feedback file once; semantic validation is separate."""
|
|
661
661
|
source = Path(path)
|
|
662
662
|
if not source.is_file():
|
|
663
663
|
raise SystemExit(f"feedback is not a readable file: {source}")
|
|
@@ -667,6 +667,16 @@ def load_edit_feedback(path, produced, shipped, diffs):
|
|
|
667
667
|
payload = json.loads(source.read_text())
|
|
668
668
|
except (OSError, UnicodeDecodeError, json.JSONDecodeError) as exc:
|
|
669
669
|
raise SystemExit(f"feedback must be readable UTF-8 JSON: {exc}") from exc
|
|
670
|
+
return payload
|
|
671
|
+
|
|
672
|
+
|
|
673
|
+
def load_edit_feedback(path, produced, shipped, diffs):
|
|
674
|
+
"""Load optional per-edit reason labels bound to an exact before/after pair."""
|
|
675
|
+
return validate_edit_feedback(read_edit_feedback(path), produced, shipped, diffs)
|
|
676
|
+
|
|
677
|
+
|
|
678
|
+
def validate_edit_feedback(payload, produced, shipped, diffs):
|
|
679
|
+
"""Validate source-bound edit labels without accepting a caller file path."""
|
|
670
680
|
if (not isinstance(payload, dict)
|
|
671
681
|
or set(payload) != {"schema", "source_sha256", "target_sha256", "edits"}
|
|
672
682
|
or payload.get("schema") != 1
|
|
@@ -693,9 +703,28 @@ def load_edit_feedback(path, produced, shipped, diffs):
|
|
|
693
703
|
return labels
|
|
694
704
|
|
|
695
705
|
|
|
696
|
-
@state_locked(lambda *a, **k: OBS)
|
|
697
706
|
def reflect(produced, shipped, doc_id=None, *, reason="unspecified", genre="general",
|
|
698
707
|
feedback=None):
|
|
708
|
+
prod_text = required_text(produced, "produced draft")
|
|
709
|
+
ship_text = required_text(shipped, "shipped draft")
|
|
710
|
+
payload = None
|
|
711
|
+
if feedback:
|
|
712
|
+
# Keep the file interface and its existing bound for CLI callers.
|
|
713
|
+
payload = read_edit_feedback(feedback)
|
|
714
|
+
return reflect_text(prod_text, ship_text, reason=reason, genre=genre,
|
|
715
|
+
feedback=payload, labels=(Path(produced).name, Path(shipped).name))
|
|
716
|
+
|
|
717
|
+
|
|
718
|
+
@state_locked(lambda *a, **k: OBS)
|
|
719
|
+
def reflect_text(prod_text, ship_text, *, reason="unspecified", genre="general",
|
|
720
|
+
feedback=None, labels=("supplied draft", "supplied final text")):
|
|
721
|
+
"""Record explicitly approved supplied edits using the existing safety gates.
|
|
722
|
+
|
|
723
|
+
No source files are written. Private evidence remains under ZERO_SLOP_HOME;
|
|
724
|
+
callers must obtain human opt-in before invoking this state-changing operation.
|
|
725
|
+
"""
|
|
726
|
+
if not isinstance(prod_text, str) or not isinstance(ship_text, str):
|
|
727
|
+
raise ValueError("Reflection inputs must be text")
|
|
699
728
|
reason = validate_label(reason, REASON_LABELS, "reason label")
|
|
700
729
|
genre = validate_label(genre, GENRES, "genre")
|
|
701
730
|
base = load(DATA / "patterns.json")
|
|
@@ -710,8 +739,6 @@ def reflect(produced, shipped, doc_id=None, *, reason="unspecified", genre="gene
|
|
|
710
739
|
obs.setdefault("fix_observations", {})
|
|
711
740
|
|
|
712
741
|
today = str(date.today())
|
|
713
|
-
prod_text = required_text(produced, "produced draft")
|
|
714
|
-
ship_text = required_text(shipped, "shipped draft")
|
|
715
742
|
# A vote is one unique edit pair, regardless of filenames or caller-supplied
|
|
716
743
|
# labels. Otherwise three copies of the same before/after text could cross
|
|
717
744
|
# the recurrence threshold by changing only --doc-id. The optional label is
|
|
@@ -719,8 +746,8 @@ def reflect(produced, shipped, doc_id=None, *, reason="unspecified", genre="gene
|
|
|
719
746
|
doc = hashlib.sha256(
|
|
720
747
|
(prod_text + "\0" + ship_text).encode()).hexdigest()[:16]
|
|
721
748
|
diffs = diff_spans(prod_text, ship_text)
|
|
722
|
-
edit_labels = (
|
|
723
|
-
if feedback else {})
|
|
749
|
+
edit_labels = (validate_edit_feedback(feedback, prod_text, ship_text, diffs)
|
|
750
|
+
if feedback is not None else {})
|
|
724
751
|
caught_words = {word for d in diffs if already_caught(d["span"], pats, lex)
|
|
725
752
|
for word in norm(d["span"]).split()}
|
|
726
753
|
|
|
@@ -818,7 +845,7 @@ def reflect(produced, shipped, doc_id=None, *, reason="unspecified", genre="gene
|
|
|
818
845
|
|
|
819
846
|
write_json(OBS, obs, private=True)
|
|
820
847
|
|
|
821
|
-
print(f"reflect: {
|
|
848
|
+
print(f"reflect: {labels[0]} → {labels[1]}\n")
|
|
822
849
|
print(f" {agreed} edit(s) Zero Slop had already flagged — the writer agreed")
|
|
823
850
|
print(f" {skipped} content-specific cut(s) ignored (figures, proper nouns)")
|
|
824
851
|
print(f" {recorded} new phrase(s) saved for review")
|
|
@@ -1242,11 +1269,8 @@ def merge(path, apply_, cat, weight):
|
|
|
1242
1269
|
return 0
|
|
1243
1270
|
|
|
1244
1271
|
|
|
1245
|
-
@state_locked(lambda *a, **k: LOCAL)
|
|
1246
1272
|
def confirm(target):
|
|
1247
1273
|
"""Refresh evidence. Patterns that keep firing stay; the rest decay out."""
|
|
1248
|
-
p = LOCAL
|
|
1249
|
-
d = load_learned(p, "local")
|
|
1250
1274
|
t = Path(target)
|
|
1251
1275
|
if not t.exists():
|
|
1252
1276
|
raise SystemExit(f"confirmation path does not exist: {t}")
|
|
@@ -1260,6 +1284,15 @@ def confirm(target):
|
|
|
1260
1284
|
raise SystemExit(f"cannot read confirmation corpus at {t}: {exc}") from exc
|
|
1261
1285
|
if not text.strip():
|
|
1262
1286
|
raise SystemExit(f"confirmation corpus contains no text: {t}")
|
|
1287
|
+
return confirm_text(text, sample_count=len(files))
|
|
1288
|
+
|
|
1289
|
+
|
|
1290
|
+
@state_locked(lambda *a, **k: LOCAL)
|
|
1291
|
+
def confirm_text(text, *, sample_count=1):
|
|
1292
|
+
"""Refresh private rules against supplied text, without caller paths."""
|
|
1293
|
+
if not isinstance(text, str) or not text.strip():
|
|
1294
|
+
raise ValueError("Confirmation input must contain text")
|
|
1295
|
+
d = load_learned(LOCAL, "local")
|
|
1263
1296
|
today, n = str(date.today()), 0
|
|
1264
1297
|
for pat in d.get("patterns", []):
|
|
1265
1298
|
try:
|
|
@@ -1270,9 +1303,9 @@ def confirm(target):
|
|
|
1270
1303
|
n += 1
|
|
1271
1304
|
except re.error:
|
|
1272
1305
|
continue
|
|
1273
|
-
write_json(
|
|
1306
|
+
write_json(LOCAL, d, private=True)
|
|
1274
1307
|
print(f"confirmed {n}/{len(d.get('patterns', []))} learned pattern(s) "
|
|
1275
|
-
f"against {
|
|
1308
|
+
f"against {sample_count} sample(s)")
|
|
1276
1309
|
return 0
|
|
1277
1310
|
|
|
1278
1311
|
|
|
@@ -1427,10 +1460,7 @@ def build_voice(name, sample_path):
|
|
|
1427
1460
|
complete writing style. The resulting profile changes scoring only when
|
|
1428
1461
|
the caller explicitly selects it with ``--voice NAME``.
|
|
1429
1462
|
"""
|
|
1430
|
-
import slopscore
|
|
1431
1463
|
name = safe_voice_name(name)
|
|
1432
|
-
base = slopscore.load_patterns()
|
|
1433
|
-
terms = list(base.get("lexicon", {})) + list(base.get("riders", {}))
|
|
1434
1464
|
src = Path(sample_path)
|
|
1435
1465
|
if not src.exists():
|
|
1436
1466
|
raise SystemExit(f"voice sample path does not exist: {src}")
|
|
@@ -1444,6 +1474,18 @@ def build_voice(name, sample_path):
|
|
|
1444
1474
|
raise SystemExit(f"cannot read voice sample at {src}: {exc}") from exc
|
|
1445
1475
|
if not blob.strip():
|
|
1446
1476
|
raise SystemExit(f"voice sample contains no text: {src}")
|
|
1477
|
+
return build_voice_text(name, blob)
|
|
1478
|
+
|
|
1479
|
+
|
|
1480
|
+
def build_voice_text(name, text):
|
|
1481
|
+
"""Same exact-term profile from supplied text, never an arbitrary source path."""
|
|
1482
|
+
import slopscore
|
|
1483
|
+
name = safe_voice_name(name)
|
|
1484
|
+
if not isinstance(text, str) or not text.strip():
|
|
1485
|
+
raise ValueError("Voice sample must contain text")
|
|
1486
|
+
base = slopscore.load_patterns()
|
|
1487
|
+
terms = list(base.get("lexicon", {})) + list(base.get("riders", {}))
|
|
1488
|
+
blob = text.lower()
|
|
1447
1489
|
keep = sorted({term for term in terms
|
|
1448
1490
|
if re.search(r"\b" + re.escape(term.lower()) + r"\b", blob)})
|
|
1449
1491
|
prof = {"_comment": f"Named scoring profile for {name}. Exact matches for "
|
package/scripts/slopscore.py
CHANGED
|
@@ -1490,6 +1490,11 @@ def load_adjudication(path, original):
|
|
|
1490
1490
|
payload = json.loads(source.read_text(encoding="utf-8"))
|
|
1491
1491
|
except (OSError, UnicodeDecodeError, json.JSONDecodeError) as exc:
|
|
1492
1492
|
raise ValueError(f"cannot read adjudication file: {exc}") from exc
|
|
1493
|
+
return validate_adjudication(payload, original)
|
|
1494
|
+
|
|
1495
|
+
|
|
1496
|
+
def validate_adjudication(payload, original):
|
|
1497
|
+
"""Validate the same source-bound ruling supplied as JSON, without file I/O."""
|
|
1493
1498
|
if not isinstance(payload, dict):
|
|
1494
1499
|
raise ValueError("adjudication file must be a JSON object")
|
|
1495
1500
|
expected = {"schema", "original_sha256", "allow_dropped_figures"}
|