sciwrite-lint 0.2.0__py3-none-any.whl
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.
- sciwrite_lint/__init__.py +3 -0
- sciwrite_lint/__main__.py +527 -0
- sciwrite_lint/_network.py +195 -0
- sciwrite_lint/api.py +1484 -0
- sciwrite_lint/checks/__init__.py +1 -0
- sciwrite_lint/checks/_section_utils.py +111 -0
- sciwrite_lint/checks/cite_purpose.py +122 -0
- sciwrite_lint/checks/claim_support.py +96 -0
- sciwrite_lint/checks/cross_section_consistency.py +185 -0
- sciwrite_lint/checks/dangling_cite.py +93 -0
- sciwrite_lint/checks/dangling_ref.py +116 -0
- sciwrite_lint/checks/full_paper_consistency.py +604 -0
- sciwrite_lint/checks/ref_internal_checks.py +919 -0
- sciwrite_lint/checks/reference_accuracy.py +277 -0
- sciwrite_lint/checks/reference_exists.py +119 -0
- sciwrite_lint/checks/reference_unreliable.py +244 -0
- sciwrite_lint/checks/registry.py +136 -0
- sciwrite_lint/checks/retracted_cite.py +96 -0
- sciwrite_lint/checks/structure_promises.py +115 -0
- sciwrite_lint/checks/unreferenced_figure.py +70 -0
- sciwrite_lint/claims.py +330 -0
- sciwrite_lint/claude_backend.py +94 -0
- sciwrite_lint/claude_cli.py +405 -0
- sciwrite_lint/cli/__init__.py +1 -0
- sciwrite_lint/cli/check.py +480 -0
- sciwrite_lint/cli/config.py +229 -0
- sciwrite_lint/cli/fetch.py +250 -0
- sciwrite_lint/cli/misc.py +1202 -0
- sciwrite_lint/cli/rank.py +470 -0
- sciwrite_lint/cli/verify.py +437 -0
- sciwrite_lint/config.py +646 -0
- sciwrite_lint/cross_paper.py +174 -0
- sciwrite_lint/eval_claims.py +1196 -0
- sciwrite_lint/fulltext.py +851 -0
- sciwrite_lint/llm_utils.py +386 -0
- sciwrite_lint/local_pdfs.py +122 -0
- sciwrite_lint/manuscript_store.py +674 -0
- sciwrite_lint/models.py +139 -0
- sciwrite_lint/pdf/__init__.py +1 -0
- sciwrite_lint/pdf/grobid.py +785 -0
- sciwrite_lint/pdf/pdf_download.py +258 -0
- sciwrite_lint/pipeline.py +2694 -0
- sciwrite_lint/prompt_safety.py +30 -0
- sciwrite_lint/rate_limiter.py +227 -0
- sciwrite_lint/references/__init__.py +1 -0
- sciwrite_lint/references/citations.py +715 -0
- sciwrite_lint/references/crossref.py +282 -0
- sciwrite_lint/references/embedding_store.py +380 -0
- sciwrite_lint/references/matching.py +273 -0
- sciwrite_lint/references/metadata.py +273 -0
- sciwrite_lint/references/reference_store.py +823 -0
- sciwrite_lint/references/retraction_watch.py +178 -0
- sciwrite_lint/references/workspace_db.py +1390 -0
- sciwrite_lint/report.py +163 -0
- sciwrite_lint/schemas.py +260 -0
- sciwrite_lint/scoring/__init__.py +1 -0
- sciwrite_lint/scoring/chain.py +716 -0
- sciwrite_lint/scoring/contribution.py +322 -0
- sciwrite_lint/scoring/scilint_score.py +611 -0
- sciwrite_lint/tex_parser.py +248 -0
- sciwrite_lint/usage.py +594 -0
- sciwrite_lint/vision/__init__.py +1 -0
- sciwrite_lint/vision/cache.py +140 -0
- sciwrite_lint/vision/describe.py +311 -0
- sciwrite_lint/vision/image_extraction.py +491 -0
- sciwrite_lint/vision/pipeline.py +207 -0
- sciwrite_lint/vllm/__init__.py +1 -0
- sciwrite_lint/vllm/metrics.py +157 -0
- sciwrite_lint/vllm/vllm_server.py +445 -0
- sciwrite_lint/web.py +369 -0
- sciwrite_lint-0.2.0.dist-info/METADATA +268 -0
- sciwrite_lint-0.2.0.dist-info/RECORD +76 -0
- sciwrite_lint-0.2.0.dist-info/WHEEL +5 -0
- sciwrite_lint-0.2.0.dist-info/entry_points.txt +2 -0
- sciwrite_lint-0.2.0.dist-info/licenses/LICENSE +21 -0
- sciwrite_lint-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
"""Shared helper for calling Claude CLI from Python.
|
|
2
|
+
|
|
3
|
+
All automated Claude CLI calls should go through ``run_claude()`` or
|
|
4
|
+
``run_claude_async()`` to keep model selection, tool blocking, and
|
|
5
|
+
error handling consistent.
|
|
6
|
+
|
|
7
|
+
Always uses ``--agent`` for strong identity (overrides Claude Code's
|
|
8
|
+
default "coder" persona).
|
|
9
|
+
|
|
10
|
+
For structured JSON responses, use ``run_claude_validated()`` or
|
|
11
|
+
``run_claude_async_validated()``: they parse JSON, validate against
|
|
12
|
+
a Pydantic model, and retry with the validation error as feedback
|
|
13
|
+
if the response doesn't match.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import asyncio
|
|
19
|
+
import json
|
|
20
|
+
import re
|
|
21
|
+
import subprocess
|
|
22
|
+
import tempfile
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
from typing import Any, TypeVar
|
|
25
|
+
|
|
26
|
+
from loguru import logger
|
|
27
|
+
from pydantic import BaseModel, ValidationError
|
|
28
|
+
|
|
29
|
+
_T = TypeVar("_T", bound=BaseModel)
|
|
30
|
+
|
|
31
|
+
_BLOCKED_TOOLS = "Task,WebSearch,WebFetch,Bash,Write,Edit,NotebookEdit"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _build_cmd(
|
|
35
|
+
prompt: str,
|
|
36
|
+
*,
|
|
37
|
+
model: str = "sonnet",
|
|
38
|
+
agent: str | Path | None = None,
|
|
39
|
+
system_prompt: str | None = None,
|
|
40
|
+
allowed_tools: str | None = None,
|
|
41
|
+
budget: float = 0.50,
|
|
42
|
+
) -> tuple[list[str], Path | None]:
|
|
43
|
+
"""Build the claude CLI command list.
|
|
44
|
+
|
|
45
|
+
Either ``agent`` (path to .md file) or ``system_prompt`` (text) must
|
|
46
|
+
be provided. If ``system_prompt`` is given, a temporary agent file is
|
|
47
|
+
created with the model in frontmatter.
|
|
48
|
+
|
|
49
|
+
Returns (cmd, temp_file) where temp_file is a temporary agent file
|
|
50
|
+
that the caller must clean up (or None if using an existing agent).
|
|
51
|
+
"""
|
|
52
|
+
temp_path: Path | None = None
|
|
53
|
+
|
|
54
|
+
if agent:
|
|
55
|
+
agent_path = str(agent)
|
|
56
|
+
elif system_prompt:
|
|
57
|
+
tf = tempfile.NamedTemporaryFile(
|
|
58
|
+
mode="w", suffix=".md", delete=False, encoding="utf-8"
|
|
59
|
+
)
|
|
60
|
+
tf.write(f"---\nmodel: {model}\n---\n\n{system_prompt}")
|
|
61
|
+
tf.close()
|
|
62
|
+
agent_path = tf.name
|
|
63
|
+
temp_path = Path(tf.name)
|
|
64
|
+
else:
|
|
65
|
+
raise ValueError("Either agent or system_prompt must be provided")
|
|
66
|
+
|
|
67
|
+
cmd = [
|
|
68
|
+
"claude",
|
|
69
|
+
"--agent",
|
|
70
|
+
agent_path,
|
|
71
|
+
"--model",
|
|
72
|
+
model,
|
|
73
|
+
"--print",
|
|
74
|
+
"--no-session-persistence",
|
|
75
|
+
]
|
|
76
|
+
|
|
77
|
+
# Block dangerous tools; allow specific ones if requested
|
|
78
|
+
blocked = _BLOCKED_TOOLS
|
|
79
|
+
if not allowed_tools:
|
|
80
|
+
blocked += ",Read,Glob,Grep"
|
|
81
|
+
cmd.extend(["--disallowed-tools", blocked])
|
|
82
|
+
|
|
83
|
+
if allowed_tools:
|
|
84
|
+
cmd.extend(["--tools", allowed_tools])
|
|
85
|
+
|
|
86
|
+
cmd.extend(["--max-budget-usd", str(budget)])
|
|
87
|
+
cmd.extend(["--", prompt])
|
|
88
|
+
|
|
89
|
+
return cmd, temp_path
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def run_claude(
|
|
93
|
+
prompt: str,
|
|
94
|
+
*,
|
|
95
|
+
model: str = "sonnet",
|
|
96
|
+
agent: str | Path | None = None,
|
|
97
|
+
system_prompt: str | None = None,
|
|
98
|
+
allowed_tools: str | None = None,
|
|
99
|
+
budget: float = 0.50,
|
|
100
|
+
timeout: int = 180,
|
|
101
|
+
cwd: Path | None = None,
|
|
102
|
+
) -> str | None:
|
|
103
|
+
"""Run claude CLI synchronously. Returns stdout or None on error."""
|
|
104
|
+
cmd, temp_path = _build_cmd(
|
|
105
|
+
prompt,
|
|
106
|
+
model=model,
|
|
107
|
+
agent=agent,
|
|
108
|
+
system_prompt=system_prompt,
|
|
109
|
+
allowed_tools=allowed_tools,
|
|
110
|
+
budget=budget,
|
|
111
|
+
)
|
|
112
|
+
try:
|
|
113
|
+
result = subprocess.run(
|
|
114
|
+
cmd,
|
|
115
|
+
capture_output=True,
|
|
116
|
+
text=True,
|
|
117
|
+
timeout=timeout,
|
|
118
|
+
cwd=str(cwd or Path.cwd()),
|
|
119
|
+
)
|
|
120
|
+
except subprocess.TimeoutExpired:
|
|
121
|
+
logger.error("Claude CLI timed out after {}s", timeout)
|
|
122
|
+
return None
|
|
123
|
+
except FileNotFoundError:
|
|
124
|
+
raise RuntimeError(
|
|
125
|
+
"claude CLI not found. Install: https://docs.anthropic.com/en/docs/claude-code"
|
|
126
|
+
)
|
|
127
|
+
finally:
|
|
128
|
+
if temp_path:
|
|
129
|
+
temp_path.unlink(missing_ok=True)
|
|
130
|
+
|
|
131
|
+
if result.returncode != 0:
|
|
132
|
+
logger.error(
|
|
133
|
+
"Claude CLI error (exit {}): {}", result.returncode, result.stderr[:200]
|
|
134
|
+
)
|
|
135
|
+
return None
|
|
136
|
+
|
|
137
|
+
return result.stdout
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
async def run_claude_async(
|
|
141
|
+
prompt: str,
|
|
142
|
+
*,
|
|
143
|
+
model: str = "sonnet",
|
|
144
|
+
agent: str | Path | None = None,
|
|
145
|
+
system_prompt: str | None = None,
|
|
146
|
+
allowed_tools: str | None = None,
|
|
147
|
+
budget: float = 0.50,
|
|
148
|
+
timeout: int = 180,
|
|
149
|
+
cwd: Path | None = None,
|
|
150
|
+
) -> str | None:
|
|
151
|
+
"""Run claude CLI asynchronously. Returns stdout or None on error."""
|
|
152
|
+
cmd, temp_path = _build_cmd(
|
|
153
|
+
prompt,
|
|
154
|
+
model=model,
|
|
155
|
+
agent=agent,
|
|
156
|
+
system_prompt=system_prompt,
|
|
157
|
+
allowed_tools=allowed_tools,
|
|
158
|
+
budget=budget,
|
|
159
|
+
)
|
|
160
|
+
try:
|
|
161
|
+
proc = await asyncio.create_subprocess_exec(
|
|
162
|
+
*cmd,
|
|
163
|
+
stdout=asyncio.subprocess.PIPE,
|
|
164
|
+
stderr=asyncio.subprocess.PIPE,
|
|
165
|
+
cwd=str(cwd or Path.cwd()),
|
|
166
|
+
)
|
|
167
|
+
try:
|
|
168
|
+
stdout_bytes, stderr_bytes = await asyncio.wait_for(
|
|
169
|
+
proc.communicate(),
|
|
170
|
+
timeout=timeout,
|
|
171
|
+
)
|
|
172
|
+
except TimeoutError:
|
|
173
|
+
proc.kill()
|
|
174
|
+
await proc.wait()
|
|
175
|
+
logger.error("Claude CLI timed out after {}s", timeout)
|
|
176
|
+
return None
|
|
177
|
+
except FileNotFoundError:
|
|
178
|
+
raise RuntimeError(
|
|
179
|
+
"claude CLI not found. Install: https://docs.anthropic.com/en/docs/claude-code"
|
|
180
|
+
)
|
|
181
|
+
finally:
|
|
182
|
+
if temp_path:
|
|
183
|
+
temp_path.unlink(missing_ok=True)
|
|
184
|
+
|
|
185
|
+
stdout = stdout_bytes.decode() if stdout_bytes else ""
|
|
186
|
+
stderr = stderr_bytes.decode() if stderr_bytes else ""
|
|
187
|
+
|
|
188
|
+
if proc.returncode != 0:
|
|
189
|
+
logger.error("Claude CLI error (exit {}): {}", proc.returncode, stderr[:200])
|
|
190
|
+
return None
|
|
191
|
+
|
|
192
|
+
return stdout
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
# ---------------------------------------------------------------------------
|
|
196
|
+
# JSON extraction + Pydantic validation
|
|
197
|
+
# ---------------------------------------------------------------------------
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def extract_json_from_response(text: str) -> dict[str, Any] | None:
|
|
201
|
+
"""Extract JSON from Claude CLI text response.
|
|
202
|
+
|
|
203
|
+
Handles ``<thinking>`` blocks, markdown code fences, and stray text
|
|
204
|
+
around the JSON object.
|
|
205
|
+
"""
|
|
206
|
+
text = re.sub(r"<thinking>.*?</thinking>", "", text, flags=re.DOTALL)
|
|
207
|
+
text = re.sub(r"^```(?:json)?\s*\n?", "", text.strip(), flags=re.MULTILINE)
|
|
208
|
+
text = re.sub(r"\n?```\s*$", "", text.strip(), flags=re.MULTILINE)
|
|
209
|
+
text = text.strip()
|
|
210
|
+
try:
|
|
211
|
+
return json.loads(text)
|
|
212
|
+
except json.JSONDecodeError:
|
|
213
|
+
pass
|
|
214
|
+
match = re.search(r"\{[\s\S]*\}", text)
|
|
215
|
+
if match:
|
|
216
|
+
try:
|
|
217
|
+
return json.loads(match.group())
|
|
218
|
+
except json.JSONDecodeError:
|
|
219
|
+
pass
|
|
220
|
+
return None
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def validate_response(
|
|
224
|
+
raw: str | None,
|
|
225
|
+
model: type[_T],
|
|
226
|
+
) -> _T | None:
|
|
227
|
+
"""Parse Claude CLI output and validate against a Pydantic model.
|
|
228
|
+
|
|
229
|
+
Returns a validated model instance, or None if parsing/validation fails.
|
|
230
|
+
On failure, logs the error for diagnostics.
|
|
231
|
+
"""
|
|
232
|
+
if raw is None:
|
|
233
|
+
return None
|
|
234
|
+
data = extract_json_from_response(raw)
|
|
235
|
+
if data is None:
|
|
236
|
+
logger.warning("Could not parse JSON from Claude response: {}", raw[:200])
|
|
237
|
+
return None
|
|
238
|
+
try:
|
|
239
|
+
return model.model_validate(data)
|
|
240
|
+
except ValidationError as e:
|
|
241
|
+
logger.warning("Claude response failed validation: {}", e)
|
|
242
|
+
return None
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def run_claude_validated(
|
|
246
|
+
prompt: str,
|
|
247
|
+
response_model: type[_T],
|
|
248
|
+
*,
|
|
249
|
+
retries: int = 1,
|
|
250
|
+
model: str = "sonnet",
|
|
251
|
+
agent: str | Path | None = None,
|
|
252
|
+
system_prompt: str | None = None,
|
|
253
|
+
allowed_tools: str | None = None,
|
|
254
|
+
budget: float = 0.50,
|
|
255
|
+
timeout: int = 180,
|
|
256
|
+
cwd: Path | None = None,
|
|
257
|
+
) -> _T | None:
|
|
258
|
+
"""Synchronous version of ``run_claude_async_validated``."""
|
|
259
|
+
current_prompt = prompt
|
|
260
|
+
for attempt in range(1 + retries):
|
|
261
|
+
stdout = run_claude(
|
|
262
|
+
current_prompt,
|
|
263
|
+
model=model,
|
|
264
|
+
agent=agent,
|
|
265
|
+
system_prompt=system_prompt,
|
|
266
|
+
allowed_tools=allowed_tools,
|
|
267
|
+
budget=budget,
|
|
268
|
+
timeout=timeout,
|
|
269
|
+
cwd=cwd,
|
|
270
|
+
)
|
|
271
|
+
if stdout is None:
|
|
272
|
+
return None
|
|
273
|
+
|
|
274
|
+
data = extract_json_from_response(stdout)
|
|
275
|
+
if data is None:
|
|
276
|
+
if attempt < retries:
|
|
277
|
+
current_prompt = (
|
|
278
|
+
f"{prompt}\n\n"
|
|
279
|
+
f"YOUR PREVIOUS RESPONSE COULD NOT BE PARSED AS JSON. "
|
|
280
|
+
f"Respond with ONLY a valid JSON object, no other text."
|
|
281
|
+
)
|
|
282
|
+
logger.warning(
|
|
283
|
+
"Claude response not valid JSON (attempt {}/{}), retrying",
|
|
284
|
+
attempt + 1,
|
|
285
|
+
1 + retries,
|
|
286
|
+
)
|
|
287
|
+
continue
|
|
288
|
+
return None
|
|
289
|
+
|
|
290
|
+
try:
|
|
291
|
+
return response_model.model_validate(data)
|
|
292
|
+
except ValidationError as e:
|
|
293
|
+
if attempt < retries:
|
|
294
|
+
error_msg = str(e)
|
|
295
|
+
current_prompt = (
|
|
296
|
+
f"{prompt}\n\n"
|
|
297
|
+
f"YOUR PREVIOUS RESPONSE FAILED VALIDATION:\n{error_msg}\n\n"
|
|
298
|
+
f"Fix the errors and respond with ONLY a valid JSON object."
|
|
299
|
+
)
|
|
300
|
+
logger.warning(
|
|
301
|
+
"Claude response failed validation (attempt {}/{}): {}",
|
|
302
|
+
attempt + 1,
|
|
303
|
+
1 + retries,
|
|
304
|
+
error_msg[:200],
|
|
305
|
+
)
|
|
306
|
+
else:
|
|
307
|
+
logger.warning(
|
|
308
|
+
"Claude response failed validation after {} attempts: {}",
|
|
309
|
+
1 + retries,
|
|
310
|
+
str(e)[:200],
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
return None
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
async def run_claude_async_validated(
|
|
317
|
+
prompt: str,
|
|
318
|
+
response_model: type[_T],
|
|
319
|
+
*,
|
|
320
|
+
retries: int = 1,
|
|
321
|
+
model: str = "sonnet",
|
|
322
|
+
agent: str | Path | None = None,
|
|
323
|
+
system_prompt: str | None = None,
|
|
324
|
+
allowed_tools: str | None = None,
|
|
325
|
+
budget: float = 0.50,
|
|
326
|
+
timeout: int = 180,
|
|
327
|
+
cwd: Path | None = None,
|
|
328
|
+
) -> _T | None:
|
|
329
|
+
"""Run Claude CLI and validate the response against a Pydantic model.
|
|
330
|
+
|
|
331
|
+
On validation failure, retries with the error message appended to the
|
|
332
|
+
prompt so Claude can correct its response. Cloud APIs don't have
|
|
333
|
+
constrained decoding, so validation + retry is the only way to
|
|
334
|
+
guarantee schema compliance.
|
|
335
|
+
|
|
336
|
+
Args:
|
|
337
|
+
prompt: The user prompt.
|
|
338
|
+
response_model: Pydantic model class to validate against.
|
|
339
|
+
retries: Number of retry attempts on validation failure (default: 1).
|
|
340
|
+
model: Claude model to use (default: sonnet).
|
|
341
|
+
agent: Path to agent .md file.
|
|
342
|
+
system_prompt: System prompt text (alternative to agent).
|
|
343
|
+
allowed_tools: Comma-separated tool list to allow.
|
|
344
|
+
budget: Max budget in USD.
|
|
345
|
+
timeout: CLI timeout in seconds.
|
|
346
|
+
cwd: Working directory for claude CLI.
|
|
347
|
+
|
|
348
|
+
Returns:
|
|
349
|
+
Validated model instance, or None if all attempts fail.
|
|
350
|
+
"""
|
|
351
|
+
current_prompt = prompt
|
|
352
|
+
for attempt in range(1 + retries):
|
|
353
|
+
stdout = await run_claude_async(
|
|
354
|
+
current_prompt,
|
|
355
|
+
model=model,
|
|
356
|
+
agent=agent,
|
|
357
|
+
system_prompt=system_prompt,
|
|
358
|
+
allowed_tools=allowed_tools,
|
|
359
|
+
budget=budget,
|
|
360
|
+
timeout=timeout,
|
|
361
|
+
cwd=cwd,
|
|
362
|
+
)
|
|
363
|
+
if stdout is None:
|
|
364
|
+
return None
|
|
365
|
+
|
|
366
|
+
data = extract_json_from_response(stdout)
|
|
367
|
+
if data is None:
|
|
368
|
+
if attempt < retries:
|
|
369
|
+
current_prompt = (
|
|
370
|
+
f"{prompt}\n\n"
|
|
371
|
+
f"YOUR PREVIOUS RESPONSE COULD NOT BE PARSED AS JSON. "
|
|
372
|
+
f"Respond with ONLY a valid JSON object, no other text."
|
|
373
|
+
)
|
|
374
|
+
logger.warning(
|
|
375
|
+
"Claude response not valid JSON (attempt {}/{}), retrying",
|
|
376
|
+
attempt + 1,
|
|
377
|
+
1 + retries,
|
|
378
|
+
)
|
|
379
|
+
continue
|
|
380
|
+
return None
|
|
381
|
+
|
|
382
|
+
try:
|
|
383
|
+
return response_model.model_validate(data)
|
|
384
|
+
except ValidationError as e:
|
|
385
|
+
if attempt < retries:
|
|
386
|
+
error_msg = str(e)
|
|
387
|
+
current_prompt = (
|
|
388
|
+
f"{prompt}\n\n"
|
|
389
|
+
f"YOUR PREVIOUS RESPONSE FAILED VALIDATION:\n{error_msg}\n\n"
|
|
390
|
+
f"Fix the errors and respond with ONLY a valid JSON object."
|
|
391
|
+
)
|
|
392
|
+
logger.warning(
|
|
393
|
+
"Claude response failed validation (attempt {}/{}): {}",
|
|
394
|
+
attempt + 1,
|
|
395
|
+
1 + retries,
|
|
396
|
+
error_msg[:200],
|
|
397
|
+
)
|
|
398
|
+
else:
|
|
399
|
+
logger.warning(
|
|
400
|
+
"Claude response failed validation after {} attempts: {}",
|
|
401
|
+
1 + retries,
|
|
402
|
+
str(e)[:200],
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
return None
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""CLI subcommand implementations for sciwrite-lint."""
|