simple_english 0.4.2 → 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.
- checksums.yaml +4 -4
- data/README.md +13 -0
- data/lib/simple_english/cli.rb +19 -11
- data/lib/simple_english/lint/finding.rb +16 -1
- data/lib/simple_english/model_context_protocol.rb +134 -0
- data/lib/simple_english/setup/config.rb +12 -1
- data/lib/simple_english/version.rb +1 -1
- data/lib/simple_english.rb +1 -0
- metadata +4 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8021741a0615e3d936a8a9bdabe7579fb1fa9641ce5061bf2d3a0e2bb24dd4d9
|
|
4
|
+
data.tar.gz: 87fb729f7e72ef9af5d3c63b305fdff9bea3b3aec034429ac52eaffb54639559
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: '058e1303247e2ca2aee046e1ff1c9341ff96fae26f4c4fcbb67410cd7652d0d806be163c0abb7c393ce81a725bc8b94a0a4faaeee09c89d8f04daf738a7dd677'
|
|
7
|
+
data.tar.gz: 57e8431a0c71e31a3f14d3ab6f776dfe10e0f49f60a279c7b0d4a53f7d432594a4d79af76700cc6fbc45b26767258eca3a957817cd8574f05fd64c4cff09f223
|
data/README.md
CHANGED
|
@@ -214,6 +214,19 @@ verifies. Closing the Vale gaps needs scripts or an external
|
|
|
214
214
|
tagger, and that erases Vale's main advantage: one binary with no
|
|
215
215
|
service behind it.
|
|
216
216
|
|
|
217
|
+
## Agent integrations
|
|
218
|
+
|
|
219
|
+
Pi, Claude Code, and Codex can lint the prose they write. The adapters
|
|
220
|
+
live under `integrations/`, unreleased. Try them from a checkout:
|
|
221
|
+
|
|
222
|
+
```bash
|
|
223
|
+
claude --plugin-dir integrations/claude-code # the hook feeds findings back
|
|
224
|
+
pi install integrations/pi -l # the se_lint tool
|
|
225
|
+
se mcp # an MCP server, for any MCP client
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
See [docs/DEVELOPMENT.md](docs/DEVELOPMENT.md) for the full story.
|
|
229
|
+
|
|
217
230
|
## Develop
|
|
218
231
|
|
|
219
232
|
To change the linter, add rules, or run the tests, read [docs/DEVELOPMENT.md](docs/DEVELOPMENT.md).
|
data/lib/simple_english/cli.rb
CHANGED
|
@@ -76,6 +76,12 @@ module SimpleEnglish
|
|
|
76
76
|
0
|
|
77
77
|
end
|
|
78
78
|
|
|
79
|
+
desc "mcp", "Run the MCP stdio server (one JSON-RPC message per line)"
|
|
80
|
+
def mcp
|
|
81
|
+
SimpleEnglish::ModelContextProtocol.run
|
|
82
|
+
0
|
|
83
|
+
end
|
|
84
|
+
|
|
79
85
|
desc "serve", "Run the lint daemon in the foreground"
|
|
80
86
|
method_option :port, type: :numeric, default: SimpleEnglish::Client::DEFAULT_PORT,
|
|
81
87
|
desc: "Port to listen on"
|
|
@@ -132,11 +138,22 @@ module SimpleEnglish
|
|
|
132
138
|
# "-" stays as-is for stdin. Directories expand to all lintable files.
|
|
133
139
|
# Glob output keeps a "./" prefix when the argument is ".". Strip it so
|
|
134
140
|
# paths and config ignore globs always see the same form.
|
|
141
|
+
# These trees hold third-party prose, not the user's. The tool lints
|
|
142
|
+
# what the user owns. Skipped only during directory expansion. An
|
|
143
|
+
# explicit path always lints.
|
|
144
|
+
# Dir.glob never returns dotfiles, so ".git" is inert today. It
|
|
145
|
+
# guards a future FNM_DOTMATCH from turning git internals back on.
|
|
146
|
+
# .gitignore is not read. Users put ignore: globs in the config for
|
|
147
|
+
# build output and generated files.
|
|
148
|
+
SKIPPED_DIRS = [".git", "node_modules", "vendor"].freeze
|
|
149
|
+
|
|
135
150
|
def self.expand_paths(argv)
|
|
136
151
|
extensions = (SimpleEnglish::Extractor::EXTENSION_LANGUAGES.keys.map { |e| e.delete_prefix(".") } + ["md"]).uniq.join(",")
|
|
137
152
|
argv.flat_map do |path|
|
|
138
153
|
if File.directory?(path)
|
|
139
|
-
Dir.glob(File.join(path, "**/*.{#{extensions}}"))
|
|
154
|
+
Dir.glob(File.join(path, "**/*.{#{extensions}}"))
|
|
155
|
+
.reject { |p| p.split(File::SEPARATOR).any? { |s| SKIPPED_DIRS.include?(s) } }
|
|
156
|
+
.sort
|
|
140
157
|
else
|
|
141
158
|
path
|
|
142
159
|
end
|
|
@@ -179,16 +196,7 @@ module SimpleEnglish
|
|
|
179
196
|
)
|
|
180
197
|
else
|
|
181
198
|
results.each do |path, finding|
|
|
182
|
-
|
|
183
|
-
if finding.column
|
|
184
|
-
location += ":#{finding.column}"
|
|
185
|
-
if finding.end_line && finding.end_column
|
|
186
|
-
finish = (finding.end_line == finding.line) ? finding.end_column :
|
|
187
|
-
"#{finding.end_line}:#{finding.end_column}"
|
|
188
|
-
location += "-#{finish}"
|
|
189
|
-
end
|
|
190
|
-
end
|
|
191
|
-
puts "#{location}: [#{finding.rule}] #{finding.message}"
|
|
199
|
+
puts finding.to_line(path)
|
|
192
200
|
end
|
|
193
201
|
end
|
|
194
202
|
end
|
|
@@ -5,5 +5,20 @@
|
|
|
5
5
|
# daemon can have a start column without an end position.
|
|
6
6
|
|
|
7
7
|
module SimpleEnglish
|
|
8
|
-
Finding = Struct.new(:line, :column, :end_line, :end_column, :rule, :message)
|
|
8
|
+
Finding = Struct.new(:line, :column, :end_line, :end_column, :rule, :message) do
|
|
9
|
+
# One finding as a text line: `path:line:col-end: [rule] message`.
|
|
10
|
+
# The CLI text output, the MCP tool, and the hook feedback all share
|
|
11
|
+
# this format. Keep them in step by rendering through this method.
|
|
12
|
+
def to_line(path)
|
|
13
|
+
location = "#{path}:#{line}"
|
|
14
|
+
if column
|
|
15
|
+
location += ":#{column}"
|
|
16
|
+
if end_line && end_column
|
|
17
|
+
finish = (end_line == line) ? end_column : "#{end_line}:#{end_column}"
|
|
18
|
+
location += "-#{finish}"
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
"#{location}: [#{rule}] #{message}"
|
|
22
|
+
end
|
|
23
|
+
end
|
|
9
24
|
end
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
# MCP JSON-RPC server for the lint engine. `respond` is pure: request
|
|
6
|
+
# Hash in, response Hash or nil (notifications) out. `run` drives it
|
|
7
|
+
# over stdio, one JSON-RPC message per line. Protocol facts:
|
|
8
|
+
# integrations/verification.md.
|
|
9
|
+
|
|
10
|
+
module SimpleEnglish
|
|
11
|
+
module ModelContextProtocol
|
|
12
|
+
SUPPORTED_VERSIONS = ["2025-06-18", "2025-03-26", "2024-11-05"].freeze
|
|
13
|
+
LATEST_VERSION = "2025-06-18"
|
|
14
|
+
SERVER_NAME = "simple-english"
|
|
15
|
+
|
|
16
|
+
module_function
|
|
17
|
+
|
|
18
|
+
def run(io: $stdin, out: $stdout, linter: method(:default_linter))
|
|
19
|
+
# A piped stdout buffers until 4 KB or EOF. An MCP client keeps
|
|
20
|
+
# stdin open while it waits for the answer, so an unflushed
|
|
21
|
+
# response never arrives and the client drops the server.
|
|
22
|
+
out.sync = true
|
|
23
|
+
while (line = io.gets)
|
|
24
|
+
line = line.strip
|
|
25
|
+
next if line.empty?
|
|
26
|
+
begin
|
|
27
|
+
request = JSON.parse(line)
|
|
28
|
+
rescue JSON::ParserError
|
|
29
|
+
next
|
|
30
|
+
end
|
|
31
|
+
next unless request.is_a?(Hash)
|
|
32
|
+
response =
|
|
33
|
+
begin
|
|
34
|
+
respond(request, linter: linter)
|
|
35
|
+
rescue => e
|
|
36
|
+
# A broken tool call (unreadable file, daemon hiccup) must not
|
|
37
|
+
# kill the loop: answer it, then serve the next line.
|
|
38
|
+
request.key?("id") ? error(request["id"], -32603, "lint failed: #{e.class}: #{e.message}") : nil
|
|
39
|
+
end
|
|
40
|
+
out.puts JSON.generate(response) if response
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# The CLI's lint pipeline as a callable: .simple-english.yml decides
|
|
45
|
+
# which paths and rules count, then the daemon lints the file.
|
|
46
|
+
def default_linter(path)
|
|
47
|
+
config = SimpleEnglish::Config.load
|
|
48
|
+
return [] if SimpleEnglish::Config.ignore?(config, path)
|
|
49
|
+
findings = SimpleEnglish.lint_file(path)
|
|
50
|
+
findings.nil? ? nil : SimpleEnglish::Config.filter(config, path, findings)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def respond(request, linter:)
|
|
54
|
+
return nil unless request.key?("id")
|
|
55
|
+
id = request.fetch("id")
|
|
56
|
+
method = request["method"]
|
|
57
|
+
return error(id, -32600, "Invalid Request") unless method.is_a?(String)
|
|
58
|
+
params = request["params"]
|
|
59
|
+
params = {} unless params.is_a?(Hash)
|
|
60
|
+
case method
|
|
61
|
+
when "initialize" then ok(id, initialize_result(params["protocolVersion"]))
|
|
62
|
+
when "tools/list" then ok(id, {"tools" => [lint_tool]})
|
|
63
|
+
when "tools/call" then tools_call(id, params, linter)
|
|
64
|
+
else error(id, -32601, "Method not found: #{method}")
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def initialize_result(requested_version)
|
|
69
|
+
version = SUPPORTED_VERSIONS.include?(requested_version) ? requested_version : LATEST_VERSION
|
|
70
|
+
{
|
|
71
|
+
"protocolVersion" => version,
|
|
72
|
+
"capabilities" => {"tools" => {}},
|
|
73
|
+
"serverInfo" => {"name" => SERVER_NAME, "version" => SimpleEnglish::VERSION}
|
|
74
|
+
}
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def lint_tool
|
|
78
|
+
{
|
|
79
|
+
"name" => "lint",
|
|
80
|
+
"description" =>
|
|
81
|
+
"Lint Markdown prose and code comments against plain-English rules " \
|
|
82
|
+
"(short sentences, active voice, no jargon, no filler). " \
|
|
83
|
+
"Input: a file path. Returns one line per finding, or 'clean' when " \
|
|
84
|
+
"nothing is flagged. Fix the findings, then call again until clean.",
|
|
85
|
+
"inputSchema" => {
|
|
86
|
+
"type" => "object",
|
|
87
|
+
"properties" => {"path" => {"type" => "string", "description" => "File to lint"}},
|
|
88
|
+
"required" => ["path"]
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def tools_call(id, params, linter)
|
|
94
|
+
tool = params["name"]
|
|
95
|
+
return error(id, -32602, "Unknown tool: #{tool}") unless tool == "lint"
|
|
96
|
+
arguments = params["arguments"]
|
|
97
|
+
arguments = {} unless arguments.is_a?(Hash)
|
|
98
|
+
path = arguments["path"]
|
|
99
|
+
return ok(id, tool_error("the path argument is required.")) if path.nil? || path.to_s.empty?
|
|
100
|
+
return ok(id, tool_error("#{path} is a directory. Give one file.")) if File.directory?(path)
|
|
101
|
+
return ok(id, tool_error("file not found: #{path}")) unless File.exist?(path)
|
|
102
|
+
findings =
|
|
103
|
+
begin
|
|
104
|
+
linter.call(path)
|
|
105
|
+
rescue SimpleEnglish::Config::ConfigError => e
|
|
106
|
+
return ok(id, tool_error("invalid .simple-english.yml: #{e.message}"))
|
|
107
|
+
end
|
|
108
|
+
if findings.nil?
|
|
109
|
+
# A tool error, not a protocol error: clients show the text to the
|
|
110
|
+
# model, and this message is the one that tells it how to recover.
|
|
111
|
+
ok(id, tool_error("the se daemon did not answer. Run `se serve --detached` and retry."))
|
|
112
|
+
else
|
|
113
|
+
ok(id, {"content" => [{"type" => "text", "text" => render(path, findings)}], "isError" => false})
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def render(path, findings)
|
|
118
|
+
return "clean" if findings.empty?
|
|
119
|
+
findings.map { |finding| finding.to_line(path) }.join("\n")
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def tool_error(text)
|
|
123
|
+
{"content" => [{"type" => "text", "text" => text}], "isError" => true}
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def ok(id, result)
|
|
127
|
+
{"jsonrpc" => "2.0", "id" => id, "result" => result}
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def error(id, code, message)
|
|
131
|
+
{"jsonrpc" => "2.0", "id" => id, "error" => {"code" => code, "message" => message}}
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
@@ -22,8 +22,19 @@ module SimpleEnglish
|
|
|
22
22
|
raise ConfigError, ".simple-english.yml: #{e.message}"
|
|
23
23
|
end
|
|
24
24
|
|
|
25
|
+
# Hooks pass absolute paths while ignore globs are written
|
|
26
|
+
# relative to the repo root (the CWD). Match both forms.
|
|
27
|
+
# realpath because CWD may be a symlink-resolved path while the
|
|
28
|
+
# caller's string still carries the symlink (TMPDIR on macOS).
|
|
25
29
|
def ignore?(config, path)
|
|
26
|
-
|
|
30
|
+
candidates = [path, relative_candidate(path)].compact
|
|
31
|
+
candidates.any? { |candidate| config[:ignore].any? { |pattern| matches?(pattern, candidate) } }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def relative_candidate(path)
|
|
35
|
+
pwd = Dir.pwd + "/"
|
|
36
|
+
resolved = File.exist?(path) ? File.realpath(path) : File.expand_path(path)
|
|
37
|
+
resolved.start_with?(pwd) ? resolved.delete_prefix(pwd) : nil
|
|
27
38
|
end
|
|
28
39
|
|
|
29
40
|
# File.fnmatch has no globstar: `**` never crosses directories.
|
data/lib/simple_english.rb
CHANGED
|
@@ -21,6 +21,7 @@ require_relative "simple_english/client/language_tool"
|
|
|
21
21
|
require_relative "simple_english/client/daemon"
|
|
22
22
|
require_relative "simple_english/daemon/engine"
|
|
23
23
|
require_relative "simple_english/daemon/http"
|
|
24
|
+
require_relative "simple_english/model_context_protocol"
|
|
24
25
|
require_relative "simple_english/daemon/server"
|
|
25
26
|
|
|
26
27
|
module SimpleEnglish
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: simple_english
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- TonyCTHsu
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-10-
|
|
11
|
+
date: 2026-10-05 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: tree_sitter_language_pack
|
|
@@ -153,6 +153,7 @@ files:
|
|
|
153
153
|
- lib/simple_english/lint/segment.rb
|
|
154
154
|
- lib/simple_english/lint/span.rb
|
|
155
155
|
- lib/simple_english/lint/suppressions.rb
|
|
156
|
+
- lib/simple_english/model_context_protocol.rb
|
|
156
157
|
- lib/simple_english/setup/config.rb
|
|
157
158
|
- lib/simple_english/setup/fingerprint.rb
|
|
158
159
|
- lib/simple_english/setup/install.rb
|
|
@@ -165,7 +166,7 @@ licenses:
|
|
|
165
166
|
metadata:
|
|
166
167
|
homepage_uri: https://github.com/TonyCTHsu/simple-english
|
|
167
168
|
source_code_uri: https://github.com/TonyCTHsu/simple-english
|
|
168
|
-
changelog_uri: https://github.com/TonyCTHsu/simple-english/blob/v0.
|
|
169
|
+
changelog_uri: https://github.com/TonyCTHsu/simple-english/blob/v0.5.0/CHANGELOG.md
|
|
169
170
|
post_install_message:
|
|
170
171
|
rdoc_options: []
|
|
171
172
|
require_paths:
|