llm-slop-detector 0.5.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.
package/out/mcp.js ADDED
@@ -0,0 +1,265 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ const fs = require("fs");
5
+ const path = require("path");
6
+ const util_1 = require("util");
7
+ const index_js_1 = require("@modelcontextprotocol/sdk/server/index.js");
8
+ const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
9
+ const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
10
+ const rules_1 = require("./core/rules");
11
+ const scan_1 = require("./core/scan");
12
+ const HELP = `llm-slop-mcp [options]
13
+
14
+ Stdio MCP server that exposes the LLM Slop Detector as a tool. Intended to be
15
+ spawned by MCP clients (Claude Code, etc), not run interactively.
16
+
17
+ Options:
18
+ --pack <name,...> Enable built-in rule packs
19
+ (${rules_1.BUILTIN_PACKS.join(', ')})
20
+ --no-builtin Skip the built-in core rule list
21
+ --config <path> Path to a .llmsloprc.json file
22
+ (default: nearest ancestor of cwd)
23
+ -h, --help Show this help
24
+ -v, --version Print version
25
+
26
+ Environment variables (override CLI flags when set):
27
+ LLM_SLOP_PACKS Comma-separated list of packs
28
+ LLM_SLOP_NO_BUILTIN Set to "1" or "true" to skip built-in rules
29
+ LLM_SLOP_CONFIG Path to a .llmsloprc.json file
30
+
31
+ Tools exposed:
32
+ scan_text Input: { text, language?, packs? } -> Finding[]
33
+ list_rules Input: { source? } -> rule summary
34
+ `;
35
+ function die(msg) {
36
+ process.stderr.write(`llm-slop-mcp: ${msg}\n`);
37
+ process.exit(2);
38
+ }
39
+ function readPackageVersion() {
40
+ try {
41
+ const pkgPath = path.resolve(__dirname, '..', 'package.json');
42
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
43
+ return pkg.version ?? '0.0.0';
44
+ }
45
+ catch {
46
+ return '0.0.0';
47
+ }
48
+ }
49
+ function parseStartup(argv) {
50
+ const parsed = (0, util_1.parseArgs)({
51
+ args: argv,
52
+ allowPositionals: false,
53
+ options: {
54
+ pack: { type: 'string' },
55
+ 'no-builtin': { type: 'boolean', default: false },
56
+ config: { type: 'string' },
57
+ help: { type: 'boolean', short: 'h', default: false },
58
+ version: { type: 'boolean', short: 'v', default: false },
59
+ },
60
+ strict: true,
61
+ });
62
+ if (parsed.values.help) {
63
+ process.stdout.write(HELP);
64
+ process.exit(0);
65
+ }
66
+ if (parsed.values.version) {
67
+ process.stdout.write(readPackageVersion() + '\n');
68
+ process.exit(0);
69
+ }
70
+ const envPacks = process.env.LLM_SLOP_PACKS;
71
+ const packsRaw = envPacks ?? parsed.values.pack;
72
+ const packs = packsRaw ? packsRaw.split(',').map(s => s.trim()).filter(Boolean) : [];
73
+ for (const p of packs) {
74
+ if (!rules_1.BUILTIN_PACKS.includes(p)) {
75
+ die(`unknown pack: ${p}. Known: ${rules_1.BUILTIN_PACKS.join(', ')}`);
76
+ }
77
+ }
78
+ const envNoBuiltin = process.env.LLM_SLOP_NO_BUILTIN;
79
+ const envNoBuiltinSet = envNoBuiltin === '1' || envNoBuiltin === 'true';
80
+ const useBuiltin = !(envNoBuiltinSet || parsed.values['no-builtin']);
81
+ const envConfig = process.env.LLM_SLOP_CONFIG;
82
+ const configPath = envConfig ?? parsed.values.config;
83
+ const localRulePaths = [];
84
+ if (configPath) {
85
+ if (!fs.existsSync(configPath))
86
+ die(`--config not found: ${configPath}`);
87
+ localRulePaths.push(path.resolve(configPath));
88
+ }
89
+ else {
90
+ const found = (0, rules_1.findLocalRulePathFromCwd)(process.cwd());
91
+ if (found)
92
+ localRulePaths.push(found);
93
+ }
94
+ return {
95
+ extensionRoot: path.resolve(__dirname, '..'),
96
+ useBuiltin,
97
+ packs,
98
+ localRulePaths,
99
+ };
100
+ }
101
+ function buildRules(cfg, packOverride) {
102
+ return (0, rules_1.loadRules)({
103
+ extensionRoot: cfg.extensionRoot,
104
+ useBuiltin: cfg.useBuiltin,
105
+ enabledPacks: packOverride ?? cfg.packs,
106
+ localRulePaths: cfg.localRulePaths,
107
+ userPhrases: [],
108
+ charReplacements: {},
109
+ severityOverrides: {},
110
+ });
111
+ }
112
+ function validatePackList(packs) {
113
+ if (!Array.isArray(packs)) {
114
+ throw new Error('packs must be an array of strings');
115
+ }
116
+ const out = [];
117
+ for (const p of packs) {
118
+ if (typeof p !== 'string')
119
+ throw new Error('packs must be an array of strings');
120
+ if (!rules_1.BUILTIN_PACKS.includes(p)) {
121
+ throw new Error(`unknown pack: ${p}. Known: ${rules_1.BUILTIN_PACKS.join(', ')}`);
122
+ }
123
+ out.push(p);
124
+ }
125
+ return out;
126
+ }
127
+ function handleScanText(args, cfg, defaultRules) {
128
+ const text = args.text;
129
+ if (typeof text !== 'string')
130
+ throw new Error('text must be a string');
131
+ const language = typeof args.language === 'string' ? args.language : 'markdown';
132
+ let rules = defaultRules;
133
+ if (args.packs !== undefined) {
134
+ const packs = validatePackList(args.packs);
135
+ rules = buildRules(cfg, packs);
136
+ }
137
+ const findings = (0, scan_1.scanText)(text, rules, language);
138
+ return findings.map(f => {
139
+ const start = (0, scan_1.offsetToLineCol)(text, f.offset);
140
+ const end = (0, scan_1.offsetToLineCol)(text, f.offset + f.length);
141
+ return {
142
+ line: start.line,
143
+ col: start.col,
144
+ endLine: end.line,
145
+ endCol: end.col,
146
+ offset: f.offset,
147
+ length: f.length,
148
+ matchText: f.matchText,
149
+ code: f.code,
150
+ severity: f.severity,
151
+ message: f.message,
152
+ source: f.source,
153
+ rulePattern: f.rulePattern,
154
+ };
155
+ });
156
+ }
157
+ function handleListRules(args, rules) {
158
+ const sourceFilter = typeof args.source === 'string' ? args.source : undefined;
159
+ const chars = Array.from(rules.chars.values())
160
+ .filter(c => sourceFilter === undefined || c.source === sourceFilter)
161
+ .map(c => ({
162
+ char: c.char,
163
+ codepoint: `U+${c.char.codePointAt(0).toString(16).toUpperCase().padStart(4, '0')}`,
164
+ name: c.name,
165
+ severity: c.severity,
166
+ replacement: c.replacement,
167
+ suggestion: c.suggestion,
168
+ source: c.source,
169
+ }));
170
+ const phrases = rules.phrases
171
+ .filter(p => sourceFilter === undefined || p.source === sourceFilter)
172
+ .map(p => ({
173
+ pattern: p.pattern,
174
+ reason: p.reason,
175
+ severity: p.severity,
176
+ source: p.source,
177
+ }));
178
+ return {
179
+ sources: rules.sources,
180
+ chars,
181
+ phrases,
182
+ overridesApplied: rules.overridesApplied,
183
+ };
184
+ }
185
+ async function main() {
186
+ const cfg = parseStartup(process.argv.slice(2));
187
+ const defaultRules = buildRules(cfg);
188
+ const server = new index_js_1.Server({ name: 'llm-slop-detector', version: readPackageVersion() }, { capabilities: { tools: {} } });
189
+ server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => ({
190
+ tools: [
191
+ {
192
+ name: 'scan_text',
193
+ description: 'Scan text for LLM-style phrases and invisible Unicode using the LLM Slop Detector ruleset. Returns an array of findings with line/col positions. Use language="markdown" (default) to honour fenced code / frontmatter exclusions, "plaintext" to scan everything, or a source-code language id (typescript, python, etc) to scan only comments and docstrings.',
194
+ inputSchema: {
195
+ type: 'object',
196
+ properties: {
197
+ text: {
198
+ type: 'string',
199
+ description: 'The text to scan.',
200
+ },
201
+ language: {
202
+ type: 'string',
203
+ description: 'Language id. Defaults to "markdown". Use "plaintext", "markdown", "git-commit", or a code language id (typescript, python, rust, go, etc).',
204
+ },
205
+ packs: {
206
+ type: 'array',
207
+ items: { type: 'string', enum: [...rules_1.BUILTIN_PACKS] },
208
+ description: 'Override the server\'s enabled packs for this call.',
209
+ },
210
+ },
211
+ required: ['text'],
212
+ },
213
+ },
214
+ {
215
+ name: 'list_rules',
216
+ description: 'List the rules currently loaded by the server (chars and phrases), optionally filtered by source name.',
217
+ inputSchema: {
218
+ type: 'object',
219
+ properties: {
220
+ source: {
221
+ type: 'string',
222
+ description: 'Only include rules whose source matches this name (e.g. "built-in", "pack:academic").',
223
+ },
224
+ },
225
+ },
226
+ },
227
+ ],
228
+ }));
229
+ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
230
+ const { name, arguments: args } = request.params;
231
+ const argsObj = (args ?? {});
232
+ try {
233
+ if (name === 'scan_text') {
234
+ const findings = handleScanText(argsObj, cfg, defaultRules);
235
+ return {
236
+ content: [{ type: 'text', text: JSON.stringify(findings, null, 2) }],
237
+ };
238
+ }
239
+ if (name === 'list_rules') {
240
+ const summary = handleListRules(argsObj, defaultRules);
241
+ return {
242
+ content: [{ type: 'text', text: JSON.stringify(summary, null, 2) }],
243
+ };
244
+ }
245
+ return {
246
+ content: [{ type: 'text', text: `unknown tool: ${name}` }],
247
+ isError: true,
248
+ };
249
+ }
250
+ catch (e) {
251
+ const msg = e instanceof Error ? e.message : String(e);
252
+ return {
253
+ content: [{ type: 'text', text: msg }],
254
+ isError: true,
255
+ };
256
+ }
257
+ });
258
+ const transport = new stdio_js_1.StdioServerTransport();
259
+ await server.connect(transport);
260
+ }
261
+ main().catch(e => {
262
+ process.stderr.write(`llm-slop-mcp: fatal: ${e instanceof Error ? e.stack ?? e.message : String(e)}\n`);
263
+ process.exit(1);
264
+ });
265
+ //# sourceMappingURL=mcp.js.map
package/out/rules.js ADDED
@@ -0,0 +1,46 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.BUILTIN_PACKS = exports.LOCAL_RULES_FILENAME = void 0;
4
+ exports.severityToVscode = severityToVscode;
5
+ exports.loadRules = loadRules;
6
+ const vscode = require("vscode");
7
+ const fs = require("fs");
8
+ const path = require("path");
9
+ const rules_1 = require("./core/rules");
10
+ Object.defineProperty(exports, "BUILTIN_PACKS", { enumerable: true, get: function () { return rules_1.BUILTIN_PACKS; } });
11
+ Object.defineProperty(exports, "LOCAL_RULES_FILENAME", { enumerable: true, get: function () { return rules_1.LOCAL_RULES_FILENAME; } });
12
+ function severityToVscode(s) {
13
+ switch (s) {
14
+ case 'error': return vscode.DiagnosticSeverity.Error;
15
+ case 'warning': return vscode.DiagnosticSeverity.Warning;
16
+ case 'information': return vscode.DiagnosticSeverity.Information;
17
+ case 'hint': return vscode.DiagnosticSeverity.Hint;
18
+ }
19
+ }
20
+ function getLocalRulePaths() {
21
+ // Workspace rule files ship arbitrary regex. In an untrusted workspace we
22
+ // fall back to built-in rules only; a catastrophic-backtracking pattern in
23
+ // a random repo shouldn't be able to wedge the extension host.
24
+ if (!vscode.workspace.isTrusted)
25
+ return [];
26
+ const paths = [];
27
+ for (const folder of vscode.workspace.workspaceFolders ?? []) {
28
+ const p = path.join(folder.uri.fsPath, rules_1.LOCAL_RULES_FILENAME);
29
+ if (fs.existsSync(p))
30
+ paths.push(p);
31
+ }
32
+ return paths;
33
+ }
34
+ function loadRules(extensionUri) {
35
+ const cfg = vscode.workspace.getConfiguration('llmSlopDetector');
36
+ return (0, rules_1.loadRules)({
37
+ extensionRoot: extensionUri.fsPath,
38
+ useBuiltin: cfg.get('useBuiltinRules', true),
39
+ enabledPacks: cfg.get('enabledPacks', []),
40
+ localRulePaths: getLocalRulePaths(),
41
+ userPhrases: cfg.get('phrases', []),
42
+ charReplacements: cfg.get('charReplacements', {}),
43
+ severityOverrides: (0, rules_1.parseSeverityOverrides)(cfg.get('severityOverrides', {})),
44
+ });
45
+ }
46
+ //# sourceMappingURL=rules.js.map
package/package.json ADDED
@@ -0,0 +1,228 @@
1
+ {
2
+ "name": "llm-slop-detector",
3
+ "displayName": "LLM Slop Detector",
4
+ "description": "Highlights invisible Unicode, AI-style punctuation, and telltale LLM phrases in markdown and plain text.",
5
+ "version": "0.5.0",
6
+ "publisher": "thias-se",
7
+ "engines": {
8
+ "vscode": "^1.95.0"
9
+ },
10
+ "categories": [
11
+ "Linters",
12
+ "Other"
13
+ ],
14
+ "keywords": [
15
+ "llm",
16
+ "ai",
17
+ "slop",
18
+ "unicode",
19
+ "markdown",
20
+ "plaintext",
21
+ "linter",
22
+ "writing",
23
+ "chatgpt",
24
+ "claude"
25
+ ],
26
+ "icon": "icon.png",
27
+ "galleryBanner": {
28
+ "color": "#1e1e1e",
29
+ "theme": "dark"
30
+ },
31
+ "license": "MIT",
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "https://github.com/mandakan/llm-slop-detector.git"
35
+ },
36
+ "bugs": {
37
+ "url": "https://github.com/mandakan/llm-slop-detector/issues"
38
+ },
39
+ "activationEvents": [
40
+ "onLanguage:markdown",
41
+ "onLanguage:plaintext",
42
+ "onLanguage:git-commit",
43
+ "onLanguage:scminput",
44
+ "onStartupFinished"
45
+ ],
46
+ "main": "./out/extension.js",
47
+ "bin": {
48
+ "llm-slop": "./out/cli.js",
49
+ "llm-slop-mcp": "./out/mcp.js"
50
+ },
51
+ "capabilities": {
52
+ "untrustedWorkspaces": {
53
+ "supported": "limited",
54
+ "description": "In an untrusted workspace, local .llmsloprc.json files are skipped to avoid running arbitrary regex patterns from an unknown source. Built-in rules, packs, and user-level settings still apply."
55
+ }
56
+ },
57
+ "contributes": {
58
+ "configuration": {
59
+ "title": "LLM Slop Detector",
60
+ "properties": {
61
+ "llmSlopDetector.enabled": {
62
+ "type": "boolean",
63
+ "default": true,
64
+ "description": "Enable or disable the detector."
65
+ },
66
+ "llmSlopDetector.useBuiltinRules": {
67
+ "type": "boolean",
68
+ "default": true,
69
+ "description": "Load the built-in rule list shipped with the extension. Disable this if you want to rely solely on local rule files (.llmsloprc.json) and/or user settings."
70
+ },
71
+ "llmSlopDetector.enabledPacks": {
72
+ "type": "array",
73
+ "items": {
74
+ "type": "string",
75
+ "enum": [
76
+ "academic",
77
+ "cliches",
78
+ "fiction",
79
+ "claudeisms",
80
+ "structural",
81
+ "security"
82
+ ],
83
+ "enumDescriptions": [
84
+ "Words over-represented in LLM-authored academic writing (derived from berenslab/llm-excess-vocab, MIT)",
85
+ "General LLM cliche adjectives/nouns/verbs (derived from nanxstats/llm-cliches, MIT)",
86
+ "Fiction and creative-writing LLM tells, including NSFW markers (derived from SicariusSicariiStuff/SLOP_Detector, Apache-2.0)",
87
+ "Claude-specific mannerisms and sycophantic consent-theater phrasing (derived from SicariusSicariiStuff/SLOP_Detector, Apache-2.0)",
88
+ "Structural LLM tells: 'not X but Y', sycophantic openers, meta-commentary",
89
+ "Flags LLM-weaponized invisibles above the BMP: tag chars (ASCII-smuggler prompt injection) and variation selectors (arbitrary-data smuggling). Severity: error."
90
+ ]
91
+ },
92
+ "default": [],
93
+ "uniqueItems": true,
94
+ "description": "Optional built-in rule packs to enable on top of the core rule list. See THIRD_PARTY_NOTICES.md for attribution."
95
+ },
96
+ "llmSlopDetector.phrases": {
97
+ "type": "array",
98
+ "items": {
99
+ "type": "string"
100
+ },
101
+ "default": [],
102
+ "description": "Additional regex patterns to flag as LLM phrases, appended to the built-in list and any local rule files. Case-insensitive. Use \\b for word boundaries. For richer rule metadata (reason, severity) use a .llmsloprc.json file in your workspace root."
103
+ },
104
+ "llmSlopDetector.charReplacements": {
105
+ "type": "object",
106
+ "default": {},
107
+ "additionalProperties": {
108
+ "type": "string"
109
+ },
110
+ "description": "Override quick-fix replacements for specific characters. Key: the flagged character (e.g. \"—\" for em dash). Value: the replacement string. User overrides win over built-in and local-file rules."
111
+ },
112
+ "llmSlopDetector.severityOverrides": {
113
+ "type": "object",
114
+ "default": {},
115
+ "additionalProperties": {
116
+ "type": "string",
117
+ "enum": [
118
+ "error",
119
+ "warning",
120
+ "information",
121
+ "hint",
122
+ "off"
123
+ ]
124
+ },
125
+ "description": "Override severity for specific rules, packs, or sources. Selector keys: pack:<name> (e.g. pack:academic), phrase:<exact-pattern>, char:<literal> or char:U+XXXX, source:<name>. Values: error | warning | information | hint | off (off disables the rule entirely). Precedence: phrase/char > pack > source -- most specific wins. Example: {\"pack:academic\": \"hint\", \"phrase:\\\\bdelve(s|d|ing)?\\\\b\": \"off\"}."
126
+ },
127
+ "llmSlopDetector.exclude": {
128
+ "type": "array",
129
+ "items": {
130
+ "type": "string"
131
+ },
132
+ "default": [],
133
+ "description": "File-glob patterns to skip, using .gitignore syntax. Merged with patterns from a .slopignore file at the workspace root. Example: [\"CHANGELOG.md\", \"docs/generated/**\"]. Use !pattern to re-include after a broader ignore."
134
+ },
135
+ "llmSlopDetector.debounceMs": {
136
+ "type": "number",
137
+ "default": 150,
138
+ "minimum": 0,
139
+ "maximum": 2000,
140
+ "description": "Milliseconds to wait after the last edit before rescanning the document. Collapses rapid keystrokes into a single scan. Set to 0 for instant feedback on small files."
141
+ },
142
+ "llmSlopDetector.scanCodeComments": {
143
+ "type": "boolean",
144
+ "default": false,
145
+ "description": "Scan comments and docstrings in source code files for LLM slop. Off by default; enable to widen scanning beyond markdown and plaintext."
146
+ },
147
+ "llmSlopDetector.scanCommitMessages": {
148
+ "type": "boolean",
149
+ "default": true,
150
+ "description": "Scan Git commit editor buffers (git-commit) and VS Code's Source Control input box (scminput). On by default -- commit messages are short and findings are actionable. Turn off to silence diagnostics in those buffers."
151
+ },
152
+ "llmSlopDetector.codeCommentLanguages": {
153
+ "type": "array",
154
+ "items": {
155
+ "type": "string"
156
+ },
157
+ "default": [
158
+ "typescript",
159
+ "javascript",
160
+ "typescriptreact",
161
+ "javascriptreact",
162
+ "python",
163
+ "rust",
164
+ "go",
165
+ "java",
166
+ "csharp",
167
+ "cpp",
168
+ "c",
169
+ "ruby",
170
+ "php",
171
+ "shellscript"
172
+ ],
173
+ "uniqueItems": true,
174
+ "description": "VS Code language IDs whose comments are scanned when scanCodeComments is true. Supported: typescript, javascript, typescriptreact, javascriptreact, python, rust, go, java, csharp, cpp, c, ruby, php, shellscript, swift, kotlin, scala, dart, perl, r, yaml. Unknown language IDs are ignored."
175
+ }
176
+ }
177
+ },
178
+ "jsonValidation": [
179
+ {
180
+ "fileMatch": [
181
+ ".llmsloprc.json",
182
+ ".llmsloprc.jsonc"
183
+ ],
184
+ "url": "./schemas/llmsloprc.schema.json"
185
+ }
186
+ ],
187
+ "commands": [
188
+ {
189
+ "command": "llmSlopDetector.toggle",
190
+ "title": "LLM Slop Detector: Toggle"
191
+ },
192
+ {
193
+ "command": "llmSlopDetector.openSettings",
194
+ "title": "LLM Slop Detector: Open settings"
195
+ },
196
+ {
197
+ "command": "llmSlopDetector.showRuleSources",
198
+ "title": "LLM Slop Detector: Show loaded rule sources"
199
+ },
200
+ {
201
+ "command": "llmSlopDetector.showOnboarding",
202
+ "title": "LLM Slop Detector: Show onboarding"
203
+ },
204
+ {
205
+ "command": "llmSlopDetector.scanSelection",
206
+ "title": "LLM Slop Detector: Scan selection"
207
+ },
208
+ {
209
+ "command": "llmSlopDetector.scanWorkspace",
210
+ "title": "LLM Slop Detector: Scan workspace"
211
+ }
212
+ ]
213
+ },
214
+ "scripts": {
215
+ "compile": "tsc -p ./ && chmod +x out/cli.js out/mcp.js",
216
+ "watch": "tsc -watch -p ./",
217
+ "package": "npx --yes @vscode/vsce package",
218
+ "slop": "node ./out/cli.js"
219
+ },
220
+ "devDependencies": {
221
+ "@types/node": "^25.6.0",
222
+ "@types/vscode": "^1.95.0",
223
+ "typescript": "^5.7.0"
224
+ },
225
+ "dependencies": {
226
+ "@modelcontextprotocol/sdk": "^1.29.0"
227
+ }
228
+ }