zero-slop 2.12.23 → 2.12.24
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/SKILL.md +1 -1
- package/docs/cli.md +1 -1
- package/gemini-extension.json +1 -1
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/scripts/slopscore.py +107 -3
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.24" src="https://img.shields.io/badge/version-2.12.24-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.24 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/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.24"
|
|
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.24
|
|
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.24",
|
|
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"
|
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.24",
|
|
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/slopscore.py
CHANGED
|
@@ -1265,8 +1265,30 @@ def facts(text, _other=""):
|
|
|
1265
1265
|
# a feeling verb, because the goal is catching an INVENTED inner state, and the
|
|
1266
1266
|
# comparison below cancels any that were already in the source.
|
|
1267
1267
|
INTERIOR_STATE_RX = re.compile(
|
|
1268
|
-
r"\b(
|
|
1269
|
-
r"(?:(
|
|
1268
|
+
r"\b(?P<subject>I|we)(?:(?P<contraction>['’](?:m|re))|\s+"
|
|
1269
|
+
r"(?:genuinely\s+)?(?P<verb>was|were|am|are|felt|feel|got)(?P<negative>n['’]t)?)\s+"
|
|
1270
|
+
r"(?P<modifiers>(?:(?:very|really|extremely|quite|so|genuinely|not|never|hardly|"
|
|
1271
|
+
r"barely|somewhat|slightly|partly|mostly|almost|nearly|probably|possibly|"
|
|
1272
|
+
r"perhaps|usually|sometimes|occasionally)\s+)*)"
|
|
1273
|
+
r"(?P<state>[A-Za-z]+)\b", re.I)
|
|
1274
|
+
# New copula forms are limited to feelings, rather than treating "we're
|
|
1275
|
+
# exporting" or "we are product managers" as invented interior experience.
|
|
1276
|
+
# The older felt/was/am branches retain their existing experiential coverage.
|
|
1277
|
+
INTERIOR_EMOTIONS = frozenset("""afraid alarmed angry anxious ashamed astonished
|
|
1278
|
+
bewildered bitter bored calm comfortable confident confused content delighted
|
|
1279
|
+
depressed disappointed discouraged distressed eager elated embarrassed excited
|
|
1280
|
+
exhausted fearful familiar frustrated furious glad grateful happy hopeful humbled
|
|
1281
|
+
hurt isolated lonely miserable nervous overwhelmed panicked pleased proud relieved
|
|
1282
|
+
sad safe scared shocked surprised terrified thrilled tired uncomfortable uncertain
|
|
1283
|
+
uneasy unhappy unsafe upset worried""".split())
|
|
1284
|
+
INTERIOR_COSMETIC_INTENSITY = frozenset("very really extremely quite so genuinely".split())
|
|
1285
|
+
INTERIOR_CONTEXT_RX = re.compile(
|
|
1286
|
+
r"\b(?:if|unless|provided|assuming|when|once|until|whether|wish|wished|"
|
|
1287
|
+
r"may|might|could|would|should|possibly|probably|perhaps|reportedly|"
|
|
1288
|
+
r"allegedly|apparently)\b", re.I)
|
|
1289
|
+
INTERIOR_ATTRIBUTION_RX = re.compile(
|
|
1290
|
+
r"\b(?:said|says|wrote|writes|claimed|claims|reported|reports|told|tells|"
|
|
1291
|
+
r"according\s+to)\b", re.I)
|
|
1270
1292
|
INTERIOR_COGNITION_RX = re.compile(
|
|
1271
1293
|
r"\b(?:I|we)\s+(?P<cognition>remember(?:ed)?|recall(?:ed)?|realise(?:d)?|"
|
|
1272
1294
|
r"realize(?:d)?|knew|fear(?:ed)?|hope(?:d)?|worr(?:y|ied)|panic(?:ked)?|"
|
|
@@ -1285,10 +1307,92 @@ COGNITION_CANON = {
|
|
|
1285
1307
|
}
|
|
1286
1308
|
|
|
1287
1309
|
|
|
1310
|
+
def _quoted_interior_starts(text, starts):
|
|
1311
|
+
"""Mark quotation/blockquote context in one forward delimiter scan."""
|
|
1312
|
+
quoted, opened = set(), [False, False, False, False]
|
|
1313
|
+
line_start = 0
|
|
1314
|
+
blockquote = False
|
|
1315
|
+
def word(value):
|
|
1316
|
+
return bool(value) and value in "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_"
|
|
1317
|
+
for index, char in enumerate(text):
|
|
1318
|
+
if index == line_start:
|
|
1319
|
+
cursor = index
|
|
1320
|
+
while cursor < len(text) and text[cursor] in " \t":
|
|
1321
|
+
cursor += 1
|
|
1322
|
+
blockquote = cursor < len(text) and text[cursor] == ">"
|
|
1323
|
+
if index in starts and (blockquote or any(opened)):
|
|
1324
|
+
quoted.add(index)
|
|
1325
|
+
previous = text[index - 1] if index else ""
|
|
1326
|
+
following = text[index + 1] if index + 1 < len(text) else ""
|
|
1327
|
+
if char == "“":
|
|
1328
|
+
opened[0] = True
|
|
1329
|
+
elif char == "”":
|
|
1330
|
+
opened[0] = False
|
|
1331
|
+
elif char == "‘":
|
|
1332
|
+
opened[1] = True
|
|
1333
|
+
elif char == "’" and not (word(previous) and word(following)):
|
|
1334
|
+
opened[1] = False
|
|
1335
|
+
elif char == '"':
|
|
1336
|
+
opened[2] = not opened[2]
|
|
1337
|
+
elif char == "'":
|
|
1338
|
+
if opened[3] and not word(following):
|
|
1339
|
+
opened[3] = False
|
|
1340
|
+
elif not opened[3] and not word(previous):
|
|
1341
|
+
opened[3] = True
|
|
1342
|
+
if char == "\n":
|
|
1343
|
+
line_start = index + 1
|
|
1344
|
+
return quoted
|
|
1345
|
+
|
|
1346
|
+
|
|
1347
|
+
def _interior_states(text):
|
|
1348
|
+
# Literal code is protected by the structure gate, not an author's feeling.
|
|
1349
|
+
prose = INLINE_CODE_RX.sub(lambda m: " " * len(m.group(0)), _mask_fenced(text))
|
|
1350
|
+
matches = list(INTERIOR_STATE_RX.finditer(prose))
|
|
1351
|
+
quoted = _quoted_interior_starts(prose, {match.start() for match in matches})
|
|
1352
|
+
out, match_index, start = set(), 0, 0
|
|
1353
|
+
# Context is scanned once per sentence, avoiding a suffix scan per state.
|
|
1354
|
+
ends = [m.start() for m in re.finditer(r"[.!?](?=\s|$)|\n[ \t]*\n", prose)] + [len(prose)]
|
|
1355
|
+
for end in ends:
|
|
1356
|
+
sentence = prose[start:end]
|
|
1357
|
+
context = INTERIOR_CONTEXT_RX.search(sentence)
|
|
1358
|
+
attribution = INTERIOR_ATTRIBUTION_RX.search(sentence)
|
|
1359
|
+
context_signature = None
|
|
1360
|
+
if context:
|
|
1361
|
+
# Pin the exact qualification without copying a long sentence into
|
|
1362
|
+
# every state key or exposing it as a new diagnostics field.
|
|
1363
|
+
qualification = " ".join(sentence[context.start():].lower().split())
|
|
1364
|
+
context_signature = hashlib.sha256(qualification.encode("utf-8")).hexdigest()
|
|
1365
|
+
while match_index < len(matches) and matches[match_index].start() < end:
|
|
1366
|
+
match = matches[match_index]
|
|
1367
|
+
match_index += 1
|
|
1368
|
+
subject, state = match.group("subject").lower(), match.group("state").lower()
|
|
1369
|
+
contraction = (match.group("contraction") or "").replace("’", "'").lower()
|
|
1370
|
+
if contraction and contraction != ("'m" if subject == "i" else "'re"):
|
|
1371
|
+
continue
|
|
1372
|
+
if (contraction or match.group("verb").lower() == "are") and state not in INTERIOR_EMOTIONS:
|
|
1373
|
+
continue
|
|
1374
|
+
modifiers = [word.lower() for word in match.group("modifiers").split()
|
|
1375
|
+
if word.lower() not in INTERIOR_COSMETIC_INTENSITY]
|
|
1376
|
+
if match.group("negative"):
|
|
1377
|
+
modifiers.insert(0, "not")
|
|
1378
|
+
parts = [subject, *modifiers, state]
|
|
1379
|
+
if match.start() in quoted:
|
|
1380
|
+
parts.insert(0, "quoted")
|
|
1381
|
+
if attribution and start + attribution.start() < match.start():
|
|
1382
|
+
parts.insert(0, "attributed")
|
|
1383
|
+
if context_signature:
|
|
1384
|
+
# A hypothetical/qualified feeling must not license a new
|
|
1385
|
+
# unqualified assertion. Keep its actual context conservatively.
|
|
1386
|
+
parts.append("context=" + context_signature)
|
|
1387
|
+
out.add(":".join(parts))
|
|
1388
|
+
start = end + 1
|
|
1389
|
+
return out
|
|
1390
|
+
|
|
1391
|
+
|
|
1288
1392
|
def interior_claims(text):
|
|
1289
1393
|
"""Inner-state assertions, reduced to a comparable core so paraphrase of an
|
|
1290
1394
|
existing one does not read as a new invention."""
|
|
1291
|
-
out =
|
|
1395
|
+
out = _interior_states(text)
|
|
1292
1396
|
for m in INTERIOR_COGNITION_RX.finditer(text):
|
|
1293
1397
|
word = m.group("cognition").lower()
|
|
1294
1398
|
out.add(COGNITION_CANON.get(word, word))
|