zero-slop 2.12.21 → 2.12.23

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 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.21" src="https://img.shields.io/badge/version-2.12.21-72528F?color=C15732">
17
+ <img alt="Version 2.12.23" src="https://img.shields.io/badge/version-2.12.23-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.21 deslop draft.md --genre professional
179
+ npx --yes zero-slop@2.12.23 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.21"
5
+ version: "2.12.23"
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
  ---
@@ -74,7 +74,7 @@ export function validateResult(result) {
74
74
  || !validReport(result.before) || !validReport(result.after) || !number(result.scoreChange, -100, 100)
75
75
  || typeof result.factsPreserved !== "boolean" || typeof result.passedFinalChecks !== "boolean"
76
76
  || ![result.independentModelChecks, result.modelRequests, result.rolesCompleted, result.finishingRounds, result.durationMs].every(integer)
77
- || result.modelRequests > 1 || !string(result.scorerVersion) || !string(result.note)) {
77
+ || result.modelRequests > 2 || !string(result.scorerVersion) || !string(result.note)) {
78
78
  throw new DeslopError("invalid_response", "The MCP service returned an invalid structured result.");
79
79
  }
80
80
  // Return the original object, including additive fields; never rewrite its status.
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.21
6
+ npm install --global zero-slop@2.12.23
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/docs/rest-api.md CHANGED
@@ -102,7 +102,7 @@ The successful response contains these 14 fields:
102
102
  | `factsPreserved` | Whether tracked source details survived the checks |
103
103
  | `passedFinalChecks` | Whether the rewritten text passed the final checks |
104
104
  | `independentModelChecks` | Count of independent model checks; currently zero |
105
- | `modelRequests` | Editing-model requests made, zero or one |
105
+ | `modelRequests` | Provider attempts started, from zero to two; failed attempts count |
106
106
  | `rolesCompleted` | Completed pipeline responsibilities |
107
107
  | `finishingRounds` | Additional local finishing rounds |
108
108
  | `scorerVersion` | Deployed Zero Slop release |
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zero-slop",
3
- "version": "2.12.21",
3
+ "version": "2.12.23",
4
4
  "description": "Find and fix canned AI writing while preserving the facts, voice, and format.",
5
5
  "mcpServers": {
6
6
  "zero-slop": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zero-slop",
3
- "version": "2.12.21",
3
+ "version": "2.12.23",
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.21",
4
+ "version": "2.12.23",
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/rescue.py CHANGED
@@ -18,12 +18,89 @@ import sys
18
18
 
19
19
  MAX_INPUT_BYTES = 4 * 1024 * 1024
20
20
  PROTECTED = re.compile(
21
- r"```[\s\S]*?```|`[^`\n]+`|\[[^\]\n]+\]\([^)]+\)|https?://[^\s<]+|"
22
- r"“[^”\n]*”|‘[^’\n]*’|\"[^\"\n]*\""
21
+ r"```[\s\S]*?```|^[ \t]*>[^\n]*(?:\n[ \t]*>[^\n]*)*|"
22
+ r"`[^`\n]+`|\[[^\]\n]+\]\([^)]+\)|https?://[^\s<]+",
23
+ re.MULTILINE,
23
24
  )
24
25
  TOKEN = re.compile(r"\ue000(\d+)\ue001")
25
26
 
26
27
 
28
+ def _protected_ranges(text: str) -> list[tuple[int, int]]:
29
+ """Scan quote delimiters once; an unmatched opener never rescans a suffix."""
30
+ base = []
31
+ for match in PROTECTED.finditer(text):
32
+ end = match.end()
33
+ if match.group(0).startswith(("http://", "https://")):
34
+ # A closing prose quote may be adjacent to a URL. Leave its
35
+ # delimiter to the quote scanner, preserving all original bytes.
36
+ boundary = re.search(r"[\"'“”‘’][.!?,;:)]*$", match.group(0))
37
+ if boundary:
38
+ end = match.start() + boundary.start()
39
+ base.append((match.start(), end))
40
+ quotes: list[list[tuple[int, int]]] = [[], [], [], []]
41
+ opened: list[int | None] = [None, None, None, None]
42
+ base_index = 0
43
+
44
+ def word(char: str) -> bool:
45
+ return bool(char) and char in "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_"
46
+
47
+ for index, char in enumerate(text):
48
+ while base_index < len(base) and index >= base[base_index][1]:
49
+ base_index += 1
50
+ in_base = base_index < len(base) and base[base_index][0] <= index
51
+ if in_base:
52
+ continue
53
+ previous = text[index - 1] if index else ""
54
+ following = text[index + 1] if index + 1 < len(text) else ""
55
+ kind: int | None = None
56
+ starts = False
57
+ closes = False
58
+ if char == "“":
59
+ kind, starts = 0, True
60
+ elif char == "”":
61
+ kind, closes = 0, True
62
+ elif char == "‘":
63
+ kind, starts = 1, True
64
+ elif char == "’":
65
+ kind, closes = 1, not (word(previous) and word(following))
66
+ elif char == '"':
67
+ kind, starts, closes = 2, True, True
68
+ elif char == "'":
69
+ kind, starts, closes = 3, not word(previous), not word(following)
70
+ if kind is None:
71
+ continue
72
+ start = opened[kind]
73
+ if start is not None and closes:
74
+ quotes[kind].append((start, index + 1))
75
+ opened[kind] = None
76
+ elif start is None and starts:
77
+ opened[kind] = index
78
+
79
+ # An incomplete quoted passage is still source material. Abstain from
80
+ # editing its remaining suffix rather than guessing where the quote ended.
81
+ for kind, start in enumerate(opened):
82
+ if start is not None:
83
+ quotes[kind].append((start, len(text)))
84
+
85
+ # Each list is already ordered. Merge the fixed five lists in linear time,
86
+ # including nested quotes and protected code/links inside an outer quote.
87
+ lists = [base, *quotes]
88
+ positions = [0] * len(lists)
89
+ merged: list[tuple[int, int]] = []
90
+ while True:
91
+ available = [kind for kind, spans in enumerate(lists) if positions[kind] < len(spans)]
92
+ if not available:
93
+ break
94
+ kind = min(available, key=lambda key: lists[key][positions[key]][0])
95
+ start, end = lists[kind][positions[kind]]
96
+ positions[kind] += 1
97
+ if merged and start <= merged[-1][1]:
98
+ merged[-1] = (merged[-1][0], max(merged[-1][1], end))
99
+ else:
100
+ merged.append((start, end))
101
+ return merged
102
+
103
+
27
104
  def _without_wrapper(match: re.Match[str]) -> str:
28
105
  prefix, word = match.group(1), match.group(2)
29
106
  # Capitalize the exposed sentence, but leave names such as iOS and eBay
@@ -37,12 +114,15 @@ def rescue_text(text: str) -> str:
37
114
  """Return a bounded, source-preserving edit; return clean text unchanged."""
38
115
  original = str(text or "").strip()
39
116
  protected: list[str] = []
40
-
41
- def mask(match: re.Match[str]) -> str:
42
- protected.append(match.group(0))
43
- return f"\ue000{len(protected) - 1}\ue001"
44
-
45
- masked = PROTECTED.sub(mask, original)
117
+ parts: list[str] = []
118
+ cursor = 0
119
+ for start, end in _protected_ranges(original):
120
+ parts.append(original[cursor:start])
121
+ protected.append(original[start:end])
122
+ parts.append(f"\ue000{len(protected) - 1}\ue001")
123
+ cursor = end
124
+ parts.append(original[cursor:])
125
+ masked = "".join(parts)
46
126
  out = re.sub(r"[\u00a0\u202f]", " ", masked)
47
127
  # Drop only a complete introductory clause; "Today" would add event timing.
48
128
  # A single unpunctuated line break may only wrap the current sentence.
@@ -60,6 +140,13 @@ def rescue_text(text: str) -> str:
60
140
  r"(^|[.!?][ \t\r\n]+|\r?\n[ \t]*\r?\n[ \t]*)as we move forward,[ \t]+the team will\b",
61
141
  lambda m: m.group(1) + "The team will", out, flags=re.IGNORECASE,
62
142
  )
143
+ # Remove one hype modifier without substituting emotion or paraphrasing
144
+ # an unspecified benefit. Preserve the announcement and its exact subject.
145
+ out = re.sub(
146
+ r"(^|[.!?][ \t\r\n]+|\r?\n[ \t]*\r?\n[ \t]*)([Ww]e)(['’]re| are) "
147
+ r"thrilled to announce a game[-\u2011]changing update\b",
148
+ lambda m: m.group(1) + m.group(2) + m.group(3) + " thrilled to announce an update", out,
149
+ )
63
150
  rules: list[tuple[str, str | object]] = [
64
151
  (
65
152
  r"\bwe are thrilled to unveil ([^,\n]+),\s+a transformative release that "