chill-agent 1.5.0 → 1.6.0

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 (187) hide show
  1. package/.claude-plugin/contrast-mcp-launcher.mjs +31 -0
  2. package/.claude-plugin/plugin.json +8 -3
  3. package/.cline-plugin/plugin.js +6 -0
  4. package/.codex-plugin/plugin.json +4 -3
  5. package/.cursor-plugin/marketplace.json +16 -0
  6. package/.cursor-plugin/plugin.json +2 -2
  7. package/.devin-plugin/plugin.json +2 -2
  8. package/.kimi-plugin/plugin.json +11 -29
  9. package/.omp-plugin/marketplace.json +15 -0
  10. package/.openclaw/skills/ponytail/SKILL.md +2 -7
  11. package/.openclaw/skills/ponytail-audit/SKILL.md +3 -6
  12. package/.openclaw/skills/ponytail-debt/SKILL.md +3 -4
  13. package/.openclaw/skills/ponytail-gain/SKILL.md +3 -4
  14. package/.openclaw/skills/ponytail-help/SKILL.md +3 -3
  15. package/.openclaw/skills/ponytail-review/SKILL.md +3 -6
  16. package/.opencode/plugins/ponytail.mjs +1 -1
  17. package/.qoder-plugin/plugin.json +12 -26
  18. package/AGENTS.md +32 -19
  19. package/__init__.py +1 -1
  20. package/assets/antislop-banner.png +0 -0
  21. package/assets/band/band.png +0 -0
  22. package/assets/band/board.png +0 -0
  23. package/assets/benchmark-3model.svg +21 -0
  24. package/assets/benchmark-agentic.svg +62 -0
  25. package/assets/codex-icon.png +0 -0
  26. package/assets/compare/code/after.webp +0 -0
  27. package/assets/compare/code/before.webp +0 -0
  28. package/assets/compare/text/after.webp +0 -0
  29. package/assets/compare/text/before.webp +0 -0
  30. package/assets/compare/ui/w-all.webp +0 -0
  31. package/assets/compare/ui/w-antislop.webp +0 -0
  32. package/assets/compare/ui/w-design.webp +0 -0
  33. package/assets/compare/ui/without.webp +0 -0
  34. package/assets/logo-dark.png +0 -0
  35. package/assets/logo-dark.svg +115 -0
  36. package/assets/logo-greenpt-dark.svg +27 -0
  37. package/assets/logo-greenpt.svg +27 -0
  38. package/assets/logo.png +0 -0
  39. package/assets/photoreunion-logo-dark.svg +4 -0
  40. package/assets/photoreunion-logo-light.svg +4 -0
  41. package/assets/profile.png +0 -0
  42. package/assets/retriever-logo-dark.svg +4 -0
  43. package/assets/retriever-logo-light.svg +4 -0
  44. package/assets/stories-banner.png +0 -0
  45. package/assets/v5/audit-es.png +0 -0
  46. package/assets/v5/audit-ja.png +0 -0
  47. package/assets/v5/audit-ko.png +0 -0
  48. package/assets/v5/audit-zh-CN.png +0 -0
  49. package/assets/v5/audit.png +0 -0
  50. package/assets/v5/beforeafter-es.png +0 -0
  51. package/assets/v5/beforeafter-ja.png +0 -0
  52. package/assets/v5/beforeafter-ko.png +0 -0
  53. package/assets/v5/beforeafter-zh-CN.png +0 -0
  54. package/assets/v5/beforeafter.png +0 -0
  55. package/assets/v5/chart-es.png +0 -0
  56. package/assets/v5/chart-ja.png +0 -0
  57. package/assets/v5/chart-ko.png +0 -0
  58. package/assets/v5/chart-zh-CN.png +0 -0
  59. package/assets/v5/chart.png +0 -0
  60. package/assets/v5/hero-es.jpg +0 -0
  61. package/assets/v5/hero-ja.jpg +0 -0
  62. package/assets/v5/hero-ko.jpg +0 -0
  63. package/assets/v5/hero-zh-CN.jpg +0 -0
  64. package/assets/v5/hero.jpg +0 -0
  65. package/assets/v5/ladder-es.png +0 -0
  66. package/assets/v5/ladder-ja.png +0 -0
  67. package/assets/v5/ladder-ko.png +0 -0
  68. package/assets/v5/ladder-zh-CN.png +0 -0
  69. package/assets/v5/ladder.png +0 -0
  70. package/assets/v5/review-es.png +0 -0
  71. package/assets/v5/review-ja.png +0 -0
  72. package/assets/v5/review-ko.png +0 -0
  73. package/assets/v5/review-zh-CN.png +0 -0
  74. package/assets/v5/review.png +0 -0
  75. package/assets/v5/tests-es.png +0 -0
  76. package/assets/v5/tests-ja.png +0 -0
  77. package/assets/v5/tests-ko.png +0 -0
  78. package/assets/v5/tests-zh-CN.png +0 -0
  79. package/assets/v5/tests.png +0 -0
  80. package/assets/waitlist-banner-es.png +0 -0
  81. package/assets/waitlist-banner-ja.png +0 -0
  82. package/assets/waitlist-banner-ko.png +0 -0
  83. package/assets/waitlist-banner-zh-CN.png +0 -0
  84. package/assets/waitlist-banner.png +0 -0
  85. package/benchmarks/.env.example +2 -0
  86. package/benchmarks/agentic/README.md +201 -0
  87. package/benchmarks/agentic/answer.py +288 -0
  88. package/benchmarks/agentic/compare.py +93 -0
  89. package/benchmarks/agentic/complete.py +178 -0
  90. package/benchmarks/agentic/judge.py +198 -0
  91. package/benchmarks/agentic/run.py +543 -0
  92. package/benchmarks/agentic/tasks.py +1544 -0
  93. package/benchmarks/arms/baseline.js +2 -0
  94. package/benchmarks/arms/caveman-SKILL.md +67 -0
  95. package/benchmarks/arms/caveman.js +8 -0
  96. package/benchmarks/arms/ponytail.js +8 -0
  97. package/benchmarks/behavior.js +54 -0
  98. package/benchmarks/behavior.yaml +40 -0
  99. package/benchmarks/correctness.js +291 -0
  100. package/benchmarks/loc.js +17 -19
  101. package/benchmarks/promptfooconfig.gemini.yaml +32 -0
  102. package/benchmarks/promptfooconfig.gpt-newest.yaml +33 -0
  103. package/benchmarks/promptfooconfig.yaml +41 -0
  104. package/benchmarks/results/2026-06-12-caveman-vs-ponytail.md +71 -0
  105. package/benchmarks/results/2026-06-12-v4-hardening-vs-caveman.md +125 -0
  106. package/benchmarks/results/2026-06-15-llama3.2-local.md +82 -0
  107. package/benchmarks/results/2026-06-16-correctness-gate-fix.md +113 -0
  108. package/benchmarks/results/2026-06-16-robustness-audit.md +135 -0
  109. package/benchmarks/results/2026-06-17-agentic-safety.md +164 -0
  110. package/benchmarks/results/2026-06-17-cost-verification.md +93 -0
  111. package/benchmarks/results/2026-06-18-agentic.md +219 -0
  112. package/benchmarks/results/2026-06-22-issue-245-217-comprehension.md +98 -0
  113. package/benchmarks/results/2026-10-07-agentic.md +186 -0
  114. package/cli/index.mjs +241 -0
  115. package/cli/lib/banner.mjs +21 -0
  116. package/cli/lib/install.mjs +498 -0
  117. package/cli/lib/settings.mjs +48 -0
  118. package/cli/package-lock.json +87 -0
  119. package/cli/package.json +42 -0
  120. package/cli/scripts/smoke-test.mjs +95 -0
  121. package/cli/scripts/smoke-worker.mjs +411 -0
  122. package/cli/scripts/sync-skills.mjs +81 -0
  123. package/examples/email-validation.md +156 -0
  124. package/examples/group-by.md +35 -0
  125. package/examples/infinite-scroll.md +58 -0
  126. package/examples/number-formatting.md +37 -0
  127. package/examples/rate-limit.md +272 -0
  128. package/examples/react-countdown.md +390 -0
  129. package/examples/url-params.md +41 -0
  130. package/gemini-extension.json +2 -2
  131. package/hooks/band/board.ts +560 -0
  132. package/hooks/band/logo.ts +3 -0
  133. package/hooks/band/metrics.ts +311 -0
  134. package/hooks/band/register.tsx +495 -0
  135. package/hooks/band/types.d.ts +34 -0
  136. package/hooks/claude-band.json +6 -0
  137. package/hooks/claude-codex-hooks.json +34 -4
  138. package/hooks/copilot-hooks.json +12 -2
  139. package/hooks/cursor-hooks.json +2 -2
  140. package/hooks/ponytail-band-import.js +274 -0
  141. package/hooks/ponytail-config.js +175 -0
  142. package/hooks/ponytail-map.js +111 -0
  143. package/hooks/ponytail-subagent.js +94 -0
  144. package/hooks/qoder-hooks.json +19 -4
  145. package/i18n/README.ko.md +230 -0
  146. package/package.json +12 -7
  147. package/pi-extension/test/extension.test.js +367 -0
  148. package/pi-extension/test/helpers.test.js +179 -0
  149. package/plugin.yaml +21 -12
  150. package/rules/antislop.mdc +18 -0
  151. package/scripts/build-openclaw-skills.js +60 -0
  152. package/scripts/check-repo.mjs +389 -0
  153. package/scripts/publish-openclaw-skills.js +75 -0
  154. package/scripts/skill-index.mjs +81 -0
  155. package/skills/antislop/SKILL.md +700 -0
  156. package/skills/antislop/VERSION +1 -0
  157. package/skills/antislop-code/SKILL.md +128 -0
  158. package/skills/antislop-copywriting/SKILL.md +407 -0
  159. package/skills/antislop-human/LICENSE-web-quality-skills +21 -0
  160. package/skills/antislop-human/SKILL.md +204 -0
  161. package/skills/antislop-human/contrast-check.py +168 -0
  162. package/skills/antislop-human/contrast-mcp.py +121 -0
  163. package/skills/antislop-human/references/A11Y-PATTERNS.md +249 -0
  164. package/skills/antislop-human/references/WCAG.md +127 -0
  165. package/skills/antislop-layoutmobile/SKILL.md +170 -0
  166. package/skills/antislop-ui/LICENSE-web-quality-skills +21 -0
  167. package/skills/antislop-ui/SKILL.md +344 -0
  168. package/skills/antislop-ui/references/CORE-WEB-VITALS.md +181 -0
  169. package/skills/antislop-ui/references/WEB-BASELINE.md +28 -0
  170. package/skills/plugin.json +5 -0
  171. package/src/package.json +3 -0
  172. package/tests/activation-map.test.js +63 -0
  173. package/tests/band-import.test.js +111 -0
  174. package/tests/band.test.tsx +520 -0
  175. package/tests/behavior.test.js +80 -0
  176. package/tests/gemini-extension.test.js +2 -2
  177. package/tests/grok-plugin.test.js +1 -1
  178. package/tests/hermes-plugin.test.js +1 -1
  179. package/tests/hooks-windows.test.js +205 -0
  180. package/tests/kimi-plugin.test.js +1 -1
  181. package/tests/map.test.js +30 -0
  182. package/tests/package-scripts.test.js +35 -0
  183. package/tests/package.test.js +23 -0
  184. package/tests/uninstall.test.js +202 -0
  185. /package/bin/{chill-agent.js → chill-agent.mjs} +0 -0
  186. /package/tests/{manifests.test.js → manifests.test.mjs} +0 -0
  187. /package/tests/{rules.test.js → rules.test.mjs} +0 -0
@@ -0,0 +1,498 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+ import os from 'node:os'
4
+ import { fileURLToPath } from 'node:url'
5
+
6
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
7
+
8
+ export const CORE = 'antislop'
9
+
10
+ export const VERSION = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8')).version
11
+
12
+ /** The release an existing install came from, or null when it predates the VERSION file.
13
+ * Nothing else on disk names it, so without this an update cannot tell the user what
14
+ * they already have.
15
+ */
16
+ export function installedVersion(targetPath) {
17
+ const file = path.join(targetPath, CORE, 'VERSION')
18
+ return fs.existsSync(file) ? fs.readFileSync(file, 'utf8').trim() : null
19
+ }
20
+
21
+ // Every row carries the entry file it writes, so a new agent cannot be added
22
+ // without one: `entry` is what `updatePointers` needs and nothing else supplies it.
23
+ // `readsAlso` lists the other project folders the agent loads skills from besides its
24
+ // own, so a duplicate can be named instead of discovered later as a missing skill.
25
+ export const AGENTS = [
26
+ { id: 'claude', label: 'Claude Code', dir: '.claude/skills', entry: 'CLAUDE.md' },
27
+ // Antigravity reads a project's .agents/skills, but not the one under the home dir.
28
+ { id: 'antigravity', label: 'Antigravity', dir: '.agents/skills', globalDir: '.gemini/config/skills', entry: 'AGENTS.md' },
29
+ // Codex reads $HOME/.agents/skills at user scope and calls its own $CODEX_HOME/skills
30
+ // the deprecated user location, so a global install belongs in the shared folder. In a
31
+ // project it reads .codex/skills and walks .agents/skills up from the working directory.
32
+ { id: 'codex', label: 'Codex', dir: '.codex/skills', globalDir: '.agents/skills', readsAlso: ['.agents/skills'], entry: 'AGENTS.md' },
33
+ // OpenCode documents ~/.config/opencode for global skills; ~/.opencode is undocumented.
34
+ { id: 'opencode', label: 'OpenCode', dir: '.opencode/skills', globalDir: '.config/opencode/skills', readsAlso: ['.claude/skills', '.agents/skills'], entry: 'AGENTS.md' },
35
+ { id: 'cursor', label: 'Cursor', dir: '.cursor/skills', entry: 'AGENTS.md' },
36
+ // Cline reads .claude/skills as well as its own folder, so the two collide in one
37
+ // project. A global skill outranks a project one here, the reverse of every other row.
38
+ { id: 'cline', label: 'Cline', dir: '.cline/skills', readsAlso: ['.claude/skills'], entry: 'AGENTS.md' },
39
+ // Amp shares the project's .agents/skills but keeps a global folder of its own, and it
40
+ // also loads .claude/skills. Its user path outranks the project one.
41
+ { id: 'amp', label: 'Amp', dir: '.agents/skills', globalDir: '.config/agents/skills', readsAlso: ['.claude/skills'], entry: 'AGENTS.md' },
42
+ { id: 'gemini', label: 'Gemini CLI', dir: '.gemini/skills', entry: 'GEMINI.md' },
43
+ // Hermes reads a project's .hermes/skills and .agents/skills, project tier first.
44
+ { id: 'hermes', label: 'Hermes', dir: '.hermes/skills', readsAlso: ['.agents/skills'], entry: 'AGENTS.md' },
45
+ // Copilot reads the shared .agents/skills folder, so it shares Antigravity's target,
46
+ // and .claude/skills beside it, which is where a Claude Code install lands.
47
+ { id: 'copilot', label: 'GitHub Copilot', dir: '.agents/skills', readsAlso: ['.claude/skills'], entry: 'AGENTS.md' },
48
+ // Kimi Code reads the shared folder at both scopes. Its own .kimi-code/skills is never
49
+ // written here, and $KIMI_CODE_HOME/skills moves with an env var the installer cannot see.
50
+ { id: 'kimi', label: 'Kimi Code', dir: '.agents/skills', entry: 'AGENTS.md' },
51
+ // Pi keeps a folder of its own at both scopes and also walks the shared .agents/skills,
52
+ // so choosing it beside Antigravity, Copilot, Kimi, or Amp fills two folders it reads.
53
+ { id: 'pi', label: 'Pi', dir: '.pi/skills', globalDir: '.pi/agent/skills', readsAlso: ['.agents/skills'], entry: 'AGENTS.md' },
54
+ ]
55
+
56
+ export function skillSourceDir() {
57
+ const bundled = path.join(__dirname, '..', 'skills')
58
+ if (fs.existsSync(bundled)) return bundled
59
+ const repo = path.join(__dirname, '..', '..', 'skills')
60
+ if (fs.existsSync(repo)) return repo
61
+ return null
62
+ }
63
+
64
+ function resolveBase(location) {
65
+ return location === 'global' ? os.homedir() : process.cwd()
66
+ }
67
+
68
+ // Some agents keep global skills outside ~/<dir>, like OpenCode's ~/.config.
69
+ function skillPath(agent, location) {
70
+ const dir = location === 'global' ? (agent.globalDir ?? agent.dir) : agent.dir
71
+ return path.join(resolveBase(location), dir)
72
+ }
73
+
74
+ // Agents that share a folder share a target, so the skills are copied once and the
75
+ // conflict count matches the folders on disk rather than the agents selected.
76
+ export function resolveTargets(location, selected = AGENTS.map((a) => a.id)) {
77
+ const byPath = new Map()
78
+ for (const agent of AGENTS.filter((a) => selected.includes(a.id))) {
79
+ const target = skillPath(agent, location)
80
+ const found = byPath.get(target)
81
+ if (found) found.agents.push(agent)
82
+ else byPath.set(target, { agents: [agent], path: target, exists: fs.existsSync(target) })
83
+ }
84
+ return [...byPath.values()]
85
+ }
86
+
87
+ // Seven rows read more than one project folder, so installing into two of them puts the
88
+ // same names in both. Only Pi settles which copy wins, so name it either way.
89
+ export function detectDuplicateReads({ targets, location }) {
90
+ if (location !== 'project') return []
91
+ const paths = new Set(targets.map((t) => t.path))
92
+ const found = []
93
+ for (const t of targets) {
94
+ for (const agent of t.agents) {
95
+ const others = (agent.readsAlso ?? [])
96
+ .map((d) => path.join(resolveBase(location), d))
97
+ .filter((p) => paths.has(p))
98
+ if (others.length > 0) found.push({ agent, paths: [t.path, ...others] })
99
+ }
100
+ }
101
+ return found
102
+ }
103
+
104
+ // Agents whose folder already exists, used to pre-check the picker. A missing
105
+ // folder does not mean a missing agent, so the user can still add one.
106
+ export function detectAgents(location) {
107
+ return AGENTS.filter((a) => fs.existsSync(path.dirname(skillPath(a, location)))).map((a) => a.id)
108
+ }
109
+
110
+ // The plugin doors keep a copy of their own under a vendor path, and none of those paths
111
+ // is documented as a stable interface. These probes only read, and a miss stays silent.
112
+ function readJSON(file) {
113
+ try {
114
+ return JSON.parse(fs.readFileSync(file, 'utf8'))
115
+ } catch {
116
+ return null
117
+ }
118
+ }
119
+
120
+ function listDirs(dir) {
121
+ try {
122
+ return fs.readdirSync(dir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name)
123
+ } catch {
124
+ return []
125
+ }
126
+ }
127
+
128
+ // A door that cloned the repo carries the VERSION file; one cloned before it shipped
129
+ // does not, and an unknown version is reported as unknown rather than guessed.
130
+ function cloneVersion(dir) {
131
+ try {
132
+ return fs.readFileSync(path.join(dir, 'skills', CORE, 'VERSION'), 'utf8').trim()
133
+ } catch {
134
+ return null
135
+ }
136
+ }
137
+
138
+ function findClone(dir) {
139
+ return fs.existsSync(dir) ? { path: dir, version: cloneVersion(dir) } : null
140
+ }
141
+
142
+ // A store can keep an old copy beside the new one, and folder order then decided which
143
+ // version was reported. The highest version wins; an unknown one sorts below any known.
144
+ function newest(copies) {
145
+ copies.sort((a, b) => (a.version ?? '').localeCompare(b.version ?? '', undefined, { numeric: true }))
146
+ return copies.length ? copies[copies.length - 1] : null
147
+ }
148
+
149
+ // Kimi and Oh My Pi name the installed copy after the plugin or the marketplace,
150
+ // so the copy is found by what it contains, not by its folder name.
151
+ function findCopyUnder(dir) {
152
+ const copies = []
153
+ for (const name of listDirs(dir)) {
154
+ const candidate = path.join(dir, name)
155
+ if (fs.existsSync(path.join(candidate, 'skills', CORE))) {
156
+ copies.push({ path: candidate, version: cloneVersion(candidate) })
157
+ continue
158
+ }
159
+ // A store can nest an install one level deeper, so check that too.
160
+ for (const inner of listDirs(candidate)) {
161
+ const nested = path.join(candidate, inner)
162
+ if (fs.existsSync(path.join(nested, 'skills', CORE))) {
163
+ copies.push({ path: nested, version: cloneVersion(nested) })
164
+ }
165
+ }
166
+ }
167
+ return newest(copies)
168
+ }
169
+
170
+ // Cline stores each install under a hashed path that varies by source type, so
171
+ // the copy is found by walking the store instead, skipping dependency folders.
172
+ function findNestedCopy(dir, depth, copies = []) {
173
+ for (const name of listDirs(dir)) {
174
+ if (name === 'node_modules' || name === '.git' || name.startsWith('.replace-')) continue
175
+ const candidate = path.join(dir, name)
176
+ if (fs.existsSync(path.join(candidate, 'skills', CORE))) {
177
+ copies.push({ path: candidate, version: cloneVersion(candidate) })
178
+ } else if (depth > 0) {
179
+ findNestedCopy(candidate, depth - 1, copies)
180
+ }
181
+ }
182
+ return newest(copies)
183
+ }
184
+
185
+ // Oh My Pi moves plugin state to $XDG_DATA_HOME once that root is initialized,
186
+ // so both places are searched rather than guessing which one is live.
187
+ function ompHomes() {
188
+ const homes = []
189
+ if (process.env.XDG_DATA_HOME) homes.push(path.join(process.env.XDG_DATA_HOME, 'omp'))
190
+ homes.push(path.join(os.homedir(), '.omp'))
191
+ return [...new Set(homes)]
192
+ }
193
+
194
+ // The cache folder is <marketplace>___<plugin>___<version>, so the newest
195
+ // install is the highest version rather than whatever the filesystem lists first.
196
+ function findOmp() {
197
+ for (const home of ompHomes()) {
198
+ const plugins = path.join(home, 'plugins')
199
+ const cache = path.join(plugins, 'cache', 'plugins')
200
+ const cached = newest(
201
+ listDirs(cache)
202
+ .map((name) => ({ name, parts: name.split('___') }))
203
+ .filter((m) => m.parts.length === 3 && m.parts[1] === CORE)
204
+ .map((m) => ({ path: path.join(cache, m.name), version: m.parts[2] })),
205
+ )
206
+ if (cached) return cached
207
+ const copy = findCopyUnder(plugins)
208
+ if (copy) return copy
209
+ }
210
+ return null
211
+ }
212
+
213
+ const claudePlugins = () => path.join(os.homedir(), '.claude', 'plugins', 'installed_plugins.json')
214
+
215
+ export const PLUGIN_DOORS = [
216
+ {
217
+ id: 'claude',
218
+ label: 'Claude Code',
219
+ update: 'claude plugin update antislop@anti-slop',
220
+ find() {
221
+ const entry = readJSON(claudePlugins())?.plugins?.['antislop@anti-slop']?.[0]
222
+ return entry ? { path: entry.installPath ?? claudePlugins(), version: entry.version ?? null } : null
223
+ },
224
+ },
225
+ {
226
+ id: 'antigravity',
227
+ label: 'Antigravity',
228
+ update: 'agy plugin install https://github.com/miqdadbadjuber/anti-slop',
229
+ find: () => findClone(path.join(os.homedir(), '.gemini', 'config', 'plugins', CORE)),
230
+ },
231
+ {
232
+ id: 'codex',
233
+ label: 'Codex',
234
+ update: 'codex plugin marketplace upgrade anti-slop',
235
+ find() {
236
+ const cache = path.join(os.homedir(), '.codex', 'plugins', 'cache', 'anti-slop', CORE)
237
+ return newest(listDirs(cache).map((version) => ({ path: cache, version })))
238
+ },
239
+ },
240
+ {
241
+ id: 'cursor',
242
+ label: 'Cursor',
243
+ update: 'agent plugin marketplace update https://github.com/miqdadbadjuber/anti-slop',
244
+ find: () => findClone(path.join(os.homedir(), '.cursor', 'plugins', 'local', 'anti-slop')),
245
+ },
246
+ {
247
+ id: 'kimi',
248
+ label: 'Kimi Code',
249
+ update: 'In a Kimi Code session: /plugins install https://github.com/miqdadbadjuber/anti-slop',
250
+ find: () =>
251
+ findCopyUnder(
252
+ path.join(process.env.KIMI_CODE_HOME || path.join(os.homedir(), '.kimi-code'), 'plugins', 'managed'),
253
+ ),
254
+ },
255
+ {
256
+ id: 'cline',
257
+ label: 'Cline',
258
+ update: 'cline plugin install https://github.com/miqdadbadjuber/anti-slop.git --force',
259
+ find: () => {
260
+ const home = process.env.CLINE_DIR || path.join(os.homedir(), '.cline')
261
+ return findNestedCopy(path.join(home, 'plugins', '_installed'), 6)
262
+ },
263
+ },
264
+ {
265
+ id: 'omp',
266
+ label: 'Oh My Pi',
267
+ update: 'omp plugin marketplace update anti-slop\nomp plugin upgrade antislop@anti-slop',
268
+ find: findOmp,
269
+ },
270
+ ]
271
+
272
+ // A door that moves its files breaks its own probe and nothing else: the installer stops
273
+ // naming that door instead of failing, which is the whole reason a miss has to be silent.
274
+ export function detectPluginDoors() {
275
+ return PLUGIN_DOORS.flatMap((door) => {
276
+ let found = null
277
+ try {
278
+ found = door.find()
279
+ } catch {
280
+ found = null
281
+ }
282
+ return found ? [{ door, ...found }] : []
283
+ })
284
+ }
285
+
286
+ export function detectConflicts({ skills, targets }) {
287
+ const conflicts = []
288
+ for (const t of targets) {
289
+ for (const skill of skills) {
290
+ if (fs.existsSync(path.join(t.path, skill))) {
291
+ conflicts.push({ skill, agents: t.agents.map((a) => a.id), path: path.join(t.path, skill) })
292
+ }
293
+ }
294
+ }
295
+ return conflicts
296
+ }
297
+
298
+ function copyDir(src, dest) {
299
+ fs.rmSync(dest, { recursive: true, force: true })
300
+ fs.mkdirSync(dest, { recursive: true })
301
+ for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
302
+ // README.md is GitHub-facing only; never ship it into a user's agent setup.
303
+ if (entry.name === 'README.md') continue
304
+ const s = path.join(src, entry.name)
305
+ const d = path.join(dest, entry.name)
306
+ if (entry.isDirectory()) copyDir(s, d)
307
+ else fs.copyFileSync(s, d)
308
+ }
309
+ }
310
+
311
+ export function installSkills({ skills, targets, overwrite = false }) {
312
+ const source = skillSourceDir()
313
+ // Fail with the real reason instead of a TypeError from path.join(null, skill).
314
+ if (!source) throw new Error('Could not find the antislop skills. Reinstall the antislop package.')
315
+ const written = []
316
+ for (const t of targets) {
317
+ for (const skill of skills) {
318
+ const src = path.join(source, skill)
319
+ if (!fs.existsSync(src)) continue
320
+ const dest = path.join(t.path, skill)
321
+ if (fs.existsSync(dest) && !overwrite) continue
322
+ copyDir(src, dest)
323
+ written.push({ skill, agents: t.agents.map((a) => a.id), path: dest })
324
+ }
325
+ }
326
+ return written
327
+ }
328
+
329
+ const POINTER_START = '<!-- antislop:start -->'
330
+ const POINTER_END = '<!-- antislop:end -->'
331
+
332
+ const SKILL_LINES = {
333
+ [CORE]: 'Core filter, always on: `antislop`',
334
+ 'antislop-ui': 'UI / visual: `antislop-ui`',
335
+ 'antislop-copywriting': 'Copy & text: `antislop-copywriting`',
336
+ 'antislop-human': 'People: `antislop-human`',
337
+ 'antislop-layoutmobile': 'Mobile / responsive: `antislop-layoutmobile`',
338
+ 'antislop-code': 'Code comments: `antislop-code`',
339
+ }
340
+
341
+ export const SKILL_NAMES = Object.keys(SKILL_LINES)
342
+
343
+ // Explicit paths avoid resolving a same-named, older user-level skill. No `@`
344
+ // import: the core should only be read when the task needs it.
345
+ function pointerBlock(skills, skillPaths) {
346
+ return [
347
+ POINTER_START,
348
+ '## antislop',
349
+ 'For UI, copy, people, mobile layout, or code comments work, read these installed skill files directly (use these paths even if a same-named global skill exists):',
350
+ ...skills.filter((s) => SKILL_LINES[s] && skillPaths.has(s)).map((s) => `- ${SKILL_LINES[s]}: \`${skillPaths.get(s)}\``),
351
+ 'Before starting, follow the core\'s "Two Usage Modes" section in strict order: explicit session instruction first, then global preference, then ask. A session instruction always wins. For a resolved mode, say `antislop active: <mode> (session override).` or `antislop active: <mode> (global preference).` once before presenting findings or making edits, using the actual mode and source. Acknowledging the user\'s request without naming the source does not replace this notice.',
352
+ 'Only an explicit choice of antislop during or after selects a session mode. A request to review, audit, or avoid file edits does not select a mode; read the global preference in that case. Another skill\'s mode does not select antislop\'s mode.',
353
+ 'If the mode is unresolved, ask during/after and end the response; wait for the answer before any UI review, planning, or concept. For read-only tasks, put the active-mode notice only at the start of the final answer, never in progress messages. For editing tasks, announce before the first edit and omit it from the final answer.',
354
+ 'To update antislop later: `npx antislop-ai --update`, or run `npx antislop-ai` and pick Overwrite them.',
355
+ POINTER_END,
356
+ ]
357
+ }
358
+
359
+ // A marker inside a code fence is someone's example, not our block. Fence runs are matched
360
+ // by length, and a fence left open at EOF is a typo rather than a boundary.
361
+ function scanMarkers(lines) {
362
+ const fenced = new Array(lines.length).fill(false)
363
+ let mark = null
364
+ let len = 0
365
+ let from = -1
366
+ lines.forEach((line, i) => {
367
+ const m = /^\s*(`{3,}|~{3,})/.exec(line)
368
+ if (m) {
369
+ if (!mark) {
370
+ mark = m[1][0]
371
+ len = m[1].length
372
+ from = i
373
+ } else if (m[1][0] === mark && m[1].length >= len && /^[ \t]*$/.test(line.slice(m[0].length))) {
374
+ mark = null
375
+ from = -1
376
+ }
377
+ fenced[i] = true
378
+ return
379
+ }
380
+ fenced[i] = Boolean(mark)
381
+ })
382
+ if (mark) for (let i = from; i < lines.length; i++) fenced[i] = false
383
+
384
+ const starts = []
385
+ const ends = []
386
+ lines.forEach((line, i) => {
387
+ if (fenced[i]) return
388
+ const t = line.trim()
389
+ if (t === POINTER_START) starts.push(i)
390
+ else if (t === POINTER_END) ends.push(i)
391
+ })
392
+ return { starts, ends }
393
+ }
394
+
395
+ function writeBlock(entry, block) {
396
+ const existing = fs.existsSync(entry) ? fs.readFileSync(entry, 'utf8') : ''
397
+ // Follow the file's dominant ending. Keying on "contains any CRLF" would flip a
398
+ // mostly-LF file, which is the damage this is here to avoid.
399
+ const crlf = (existing.match(/\r\n/g) || []).length
400
+ const eol = crlf > (existing.match(/\n/g) || []).length - crlf ? '\r\n' : '\n'
401
+ const lines = existing.split(/\r?\n/)
402
+ const { starts, ends } = scanMarkers(lines)
403
+ const start = starts.length ? starts[0] : -1
404
+ const end = start === -1 ? -1 : ends.find((i) => i > start) ?? -1
405
+ const paired = start !== -1 && end !== -1
406
+
407
+ // Only marker lines are ever removed. Reading a lone start as "the block runs to EOF"
408
+ // would delete everything the author wrote after a marker they mistyped.
409
+ const removed = new Set([...starts, ...ends])
410
+ if (paired) for (let i = start; i <= end; i++) removed.add(i)
411
+
412
+ const insertAt = paired ? start : lines.length
413
+ const head = lines.slice(0, insertAt).filter((_, i) => !removed.has(i))
414
+ const tail = lines.slice(insertAt).filter((_, i) => !removed.has(insertAt + i))
415
+
416
+ // Tidy the two seams only: a /\n{3,}/g sweep over the whole document would also
417
+ // collapse blank lines the author wrote inside their code fences.
418
+ while (head.length && head[head.length - 1].trim() === '') head.pop()
419
+ while (tail.length && tail[0].trim() === '') tail.shift()
420
+
421
+ const body = [...head, '', ...block, ...(tail.length ? ['', ...tail] : [])]
422
+ .join(eol)
423
+ .replace(/^\r?\n+/, '')
424
+ .trimEnd()
425
+
426
+ fs.writeFileSync(entry, body + eol)
427
+ }
428
+
429
+ // From the home folder the project scope is the global one, and an entry file written
430
+ // there (~/CLAUDE.md, ~/AGENTS.md) is loaded by agents in every project under it.
431
+ export function atHome() {
432
+ try {
433
+ return fs.realpathSync(process.cwd()) === fs.realpathSync(os.homedir())
434
+ } catch {
435
+ return false // a HOME that does not exist, as in some CI containers
436
+ }
437
+ }
438
+
439
+ export function updatePointers({ targets, skills }) {
440
+ if (atHome()) return []
441
+ const entries = new Map()
442
+ for (const t of targets) {
443
+ if (fs.existsSync(path.join(t.path, CORE))) {
444
+ for (const a of t.agents) {
445
+ if (!entries.has(a.entry)) entries.set(a.entry, new Map())
446
+ const skillPaths = entries.get(a.entry)
447
+ for (const skill of skills) {
448
+ const file = path.join(t.path, skill, 'SKILL.md')
449
+ if (!skillPaths.has(skill) && fs.existsSync(file)) {
450
+ skillPaths.set(skill, path.relative(process.cwd(), file).split(path.sep).join('/'))
451
+ }
452
+ }
453
+ }
454
+ }
455
+ }
456
+
457
+ const written = []
458
+ for (const [name, skillPaths] of entries) {
459
+ const entry = path.join(process.cwd(), name)
460
+ writeBlock(entry, pointerBlock(skills, skillPaths))
461
+ written.push(entry)
462
+ }
463
+ return written
464
+ }
465
+
466
+ // `--update` replaces every antislop folder already on this machine, at both scopes and
467
+ // with no prompts. Each folder keeps the skill selection it was installed with. The scope
468
+ // list is a parameter so a test never writes into the home directory it runs under.
469
+ export function updateAll({ locations = ['project', 'global'] } = {}) {
470
+ const results = []
471
+ const projectTargets = []
472
+ const projectSkills = new Set()
473
+
474
+ // From home, each folder would be updated a second time as a project folder.
475
+ const home = atHome()
476
+
477
+ for (const location of locations) {
478
+ if (location === 'project' && home) continue
479
+ for (const target of resolveTargets(location)) {
480
+ if (!fs.existsSync(path.join(target.path, CORE))) continue
481
+ const skills = SKILL_NAMES.filter((s) => fs.existsSync(path.join(target.path, s)))
482
+ if (skills.length === 0) continue
483
+ const from = installedVersion(target.path)
484
+ installSkills({ skills, targets: [target], overwrite: true })
485
+ results.push({ location, path: target.path, agents: target.agents.map((a) => a.id), skills, from, to: VERSION })
486
+ if (location === 'project') {
487
+ projectTargets.push(target)
488
+ for (const s of skills) projectSkills.add(s)
489
+ }
490
+ }
491
+ }
492
+
493
+ // One block per entry file, so the union of every project folder's skills is the list.
494
+ const pointers = projectTargets.length > 0
495
+ ? updatePointers({ targets: projectTargets, skills: [...projectSkills] })
496
+ : []
497
+ return { results, pointers }
498
+ }
@@ -0,0 +1,48 @@
1
+ import fs from 'node:fs'
2
+ import os from 'node:os'
3
+ import path from 'node:path'
4
+
5
+ const modes = ['during', 'after', 'ask']
6
+
7
+ export function resolveSettingsPath({ platform = process.platform, env = process.env, home = os.homedir() } = {}) {
8
+ const paths = platform === 'win32' ? path.win32 : path.posix
9
+ const base = platform === 'win32' && env.APPDATA ? env.APPDATA : paths.join(home, '.config')
10
+ return paths.join(base, 'antislop', 'settings.json')
11
+ }
12
+
13
+ export const settingsPath = resolveSettingsPath()
14
+
15
+ export function readSettings(file = settingsPath) {
16
+ let settings
17
+ try {
18
+ settings = JSON.parse(fs.readFileSync(file, 'utf8'))
19
+ } catch (error) {
20
+ if (error.code === 'ENOENT') return {}
21
+ throw new Error(`Cannot read ${file}: ${error.message}`)
22
+ }
23
+ if (!settings || typeof settings !== 'object' || Array.isArray(settings)) {
24
+ throw new Error(`Expected a JSON object in ${file}`)
25
+ }
26
+ return settings
27
+ }
28
+
29
+ export function configureMode(args, file = settingsPath) {
30
+ if (args.length > 1 || (args.length === 1 && !modes.includes(args[0]))) {
31
+ throw new Error('Usage: antislop-ai --mode [during|after|ask]')
32
+ }
33
+ let settings
34
+ try {
35
+ settings = readSettings(file)
36
+ } catch (error) {
37
+ // A file we cannot parse is never overwritten, so the message is the only way out.
38
+ throw new Error(`${error.message} Fix or delete it to start over.`)
39
+ }
40
+ if (args.length) {
41
+ settings.mode = args[0]
42
+ fs.mkdirSync(path.dirname(file), { recursive: true })
43
+ fs.writeFileSync(file, JSON.stringify(settings, null, 2) + '\n')
44
+ }
45
+ const mode = settings.mode === undefined ? 'ask' : settings.mode
46
+ if (!modes.includes(mode)) throw new Error(`Invalid mode in ${file}. Set --mode during, after, or ask.`)
47
+ return `antislop global mode: ${mode}${settings.mode === undefined ? ' (default)' : ''}\n${file}`
48
+ }
@@ -0,0 +1,87 @@
1
+ {
2
+ "name": "antislop-ai",
3
+ "version": "3.2.26",
4
+ "lockfileVersion": 3,
5
+ "requires": true,
6
+ "packages": {
7
+ "": {
8
+ "name": "antislop-ai",
9
+ "version": "3.2.26",
10
+ "license": "MIT",
11
+ "dependencies": {
12
+ "@clack/prompts": "^1.7.0",
13
+ "picocolors": "^1.1.1"
14
+ },
15
+ "bin": {
16
+ "antislop-ai": "index.mjs"
17
+ },
18
+ "engines": {
19
+ "node": ">=20.12.0"
20
+ }
21
+ },
22
+ "node_modules/@clack/core": {
23
+ "version": "1.4.3",
24
+ "resolved": "https://registry.npmjs.org/@clack/core/-/core-1.4.3.tgz",
25
+ "integrity": "sha512-/kr3UWNtdJfxZtPgDqUOmG2pvwlmcLGheex5yiZKdwbzZJxhV+HMNR9QNmyY5cGwTNV6LrR7Jtp+KjhUAP1qBQ==",
26
+ "license": "MIT",
27
+ "dependencies": {
28
+ "fast-wrap-ansi": "^0.2.0",
29
+ "sisteransi": "^1.0.5"
30
+ },
31
+ "engines": {
32
+ "node": ">= 20.12.0"
33
+ }
34
+ },
35
+ "node_modules/@clack/prompts": {
36
+ "version": "1.7.0",
37
+ "resolved": "https://registry.npmjs.org/@clack/prompts/-/prompts-1.7.0.tgz",
38
+ "integrity": "sha512-y7/yvZ2TPAnR9+jnc00klvNNLkJiXFFrQA/hlLCcxA9a2A4zQIOimyFQ9XfwYKiGD1fb5GY8vbKIIgO8d5Tb2A==",
39
+ "license": "MIT",
40
+ "dependencies": {
41
+ "@clack/core": "1.4.3",
42
+ "fast-string-width": "^3.0.2",
43
+ "fast-wrap-ansi": "^0.2.0",
44
+ "sisteransi": "^1.0.5"
45
+ },
46
+ "engines": {
47
+ "node": ">= 20.12.0"
48
+ }
49
+ },
50
+ "node_modules/fast-string-truncated-width": {
51
+ "version": "3.0.3",
52
+ "resolved": "https://registry.npmjs.org/fast-string-truncated-width/-/fast-string-truncated-width-3.0.3.tgz",
53
+ "integrity": "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==",
54
+ "license": "MIT"
55
+ },
56
+ "node_modules/fast-string-width": {
57
+ "version": "3.0.2",
58
+ "resolved": "https://registry.npmjs.org/fast-string-width/-/fast-string-width-3.0.2.tgz",
59
+ "integrity": "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==",
60
+ "license": "MIT",
61
+ "dependencies": {
62
+ "fast-string-truncated-width": "^3.0.2"
63
+ }
64
+ },
65
+ "node_modules/fast-wrap-ansi": {
66
+ "version": "0.2.2",
67
+ "resolved": "https://registry.npmjs.org/fast-wrap-ansi/-/fast-wrap-ansi-0.2.2.tgz",
68
+ "integrity": "sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==",
69
+ "license": "MIT",
70
+ "dependencies": {
71
+ "fast-string-width": "^3.0.2"
72
+ }
73
+ },
74
+ "node_modules/picocolors": {
75
+ "version": "1.1.1",
76
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
77
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
78
+ "license": "ISC"
79
+ },
80
+ "node_modules/sisteransi": {
81
+ "version": "1.0.5",
82
+ "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz",
83
+ "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==",
84
+ "license": "MIT"
85
+ }
86
+ }
87
+ }
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "antislop-ai",
3
+ "version": "3.2.26",
4
+ "description": "Interactive installer for the antislop skills. Anti Slop: Rules for AI Coding Agents.",
5
+ "type": "module",
6
+ "bin": {
7
+ "antislop-ai": "index.mjs"
8
+ },
9
+ "license": "MIT",
10
+ "author": "antislop",
11
+ "homepage": "https://antislop.ai.id",
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "git+https://github.com/miqdadbadjuber/anti-slop.git"
15
+ },
16
+ "bugs": "https://github.com/miqdadbadjuber/anti-slop/issues",
17
+ "keywords": [
18
+ "ai-slop",
19
+ "design-rules",
20
+ "copy-rules",
21
+ "agent-skills",
22
+ "ui",
23
+ "accessibility"
24
+ ],
25
+ "engines": {
26
+ "node": ">=20.12.0"
27
+ },
28
+ "files": [
29
+ "index.mjs",
30
+ "lib/",
31
+ "skills/"
32
+ ],
33
+ "scripts": {
34
+ "test": "node scripts/smoke-test.mjs",
35
+ "sync-skills": "node scripts/sync-skills.mjs",
36
+ "prepublishOnly": "npm run sync-skills"
37
+ },
38
+ "dependencies": {
39
+ "@clack/prompts": "^1.7.0",
40
+ "picocolors": "^1.1.1"
41
+ }
42
+ }