patchwork-conventions 0.1.1__tar.gz → 0.1.2__tar.gz

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 (30) hide show
  1. {patchwork_conventions-0.1.1 → patchwork_conventions-0.1.2}/PKG-INFO +30 -4
  2. {patchwork_conventions-0.1.1 → patchwork_conventions-0.1.2}/README.md +29 -3
  3. {patchwork_conventions-0.1.1 → patchwork_conventions-0.1.2}/pyproject.toml +1 -1
  4. {patchwork_conventions-0.1.1 → patchwork_conventions-0.1.2}/src/patchwork/cli.py +6 -6
  5. {patchwork_conventions-0.1.1 → patchwork_conventions-0.1.2}/src/patchwork/mcp/server.py +27 -2
  6. {patchwork_conventions-0.1.1 → patchwork_conventions-0.1.2}/src/patchwork/miners/config_detector.py +28 -7
  7. {patchwork_conventions-0.1.1 → patchwork_conventions-0.1.2}/src/patchwork/miners/git_patterns.py +2 -2
  8. {patchwork_conventions-0.1.1 → patchwork_conventions-0.1.2}/src/patchwork_conventions.egg-info/PKG-INFO +30 -4
  9. {patchwork_conventions-0.1.1 → patchwork_conventions-0.1.2}/LICENSE +0 -0
  10. {patchwork_conventions-0.1.1 → patchwork_conventions-0.1.2}/setup.cfg +0 -0
  11. {patchwork_conventions-0.1.1 → patchwork_conventions-0.1.2}/src/patchwork/__init__.py +0 -0
  12. {patchwork_conventions-0.1.1 → patchwork_conventions-0.1.2}/src/patchwork/mcp/__init__.py +0 -0
  13. {patchwork_conventions-0.1.1 → patchwork_conventions-0.1.2}/src/patchwork/miners/__init__.py +0 -0
  14. {patchwork_conventions-0.1.1 → patchwork_conventions-0.1.2}/src/patchwork/miners/api_patterns.py +0 -0
  15. {patchwork_conventions-0.1.1 → patchwork_conventions-0.1.2}/src/patchwork/miners/ast_base.py +0 -0
  16. {patchwork_conventions-0.1.1 → patchwork_conventions-0.1.2}/src/patchwork/miners/error_handling.py +0 -0
  17. {patchwork_conventions-0.1.1 → patchwork_conventions-0.1.2}/src/patchwork/miners/imports.py +0 -0
  18. {patchwork_conventions-0.1.1 → patchwork_conventions-0.1.2}/src/patchwork/miners/naming.py +0 -0
  19. {patchwork_conventions-0.1.1 → patchwork_conventions-0.1.2}/src/patchwork/miners/structure.py +0 -0
  20. {patchwork_conventions-0.1.1 → patchwork_conventions-0.1.2}/src/patchwork/miners/testing.py +0 -0
  21. {patchwork_conventions-0.1.1 → patchwork_conventions-0.1.2}/src/patchwork/output/__init__.py +0 -0
  22. {patchwork_conventions-0.1.1 → patchwork_conventions-0.1.2}/src/patchwork/output/report.py +0 -0
  23. {patchwork_conventions-0.1.1 → patchwork_conventions-0.1.2}/src/patchwork/scanner.py +0 -0
  24. {patchwork_conventions-0.1.1 → patchwork_conventions-0.1.2}/src/patchwork_conventions.egg-info/SOURCES.txt +0 -0
  25. {patchwork_conventions-0.1.1 → patchwork_conventions-0.1.2}/src/patchwork_conventions.egg-info/dependency_links.txt +0 -0
  26. {patchwork_conventions-0.1.1 → patchwork_conventions-0.1.2}/src/patchwork_conventions.egg-info/entry_points.txt +0 -0
  27. {patchwork_conventions-0.1.1 → patchwork_conventions-0.1.2}/src/patchwork_conventions.egg-info/requires.txt +0 -0
  28. {patchwork_conventions-0.1.1 → patchwork_conventions-0.1.2}/src/patchwork_conventions.egg-info/top_level.txt +0 -0
  29. {patchwork_conventions-0.1.1 → patchwork_conventions-0.1.2}/tests/test_naming.py +0 -0
  30. {patchwork_conventions-0.1.1 → patchwork_conventions-0.1.2}/tests/test_scanner.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: patchwork-conventions
3
- Version: 0.1.1
3
+ Version: 0.1.2
4
4
  Summary: Mine your codebase. Generate CONVENTIONS.md. Stop AI agents from making up your style.
5
5
  Author: patchwork contributors
6
6
  License: MIT
@@ -228,20 +228,46 @@ patchwork scan --claude-md
228
228
 
229
229
  ### Option 3: MCP server
230
230
 
231
- Add to `~/.claude/settings.json`:
231
+ **Claude Code** — add to `~/.claude.json` (or run `claude mcp add` interactively):
232
232
 
233
233
  ```json
234
234
  {
235
235
  "mcpServers": {
236
236
  "patchwork": {
237
237
  "command": "patchwork",
238
- "args": ["serve", "--stdio", "/path/to/your/project"]
238
+ "args": ["serve", "/path/to/your/project", "--stdio"]
239
239
  }
240
240
  }
241
241
  }
242
242
  ```
243
243
 
244
- Then Claude Code can use 8 on-demand tools:
244
+ **Claude Desktop** add to `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or `%APPDATA%\Claude\claude_desktop_config.json` (Windows):
245
+
246
+ ```json
247
+ {
248
+ "mcpServers": {
249
+ "patchwork": {
250
+ "command": "patchwork",
251
+ "args": ["serve", "/path/to/your/project", "--stdio"]
252
+ }
253
+ }
254
+ }
255
+ ```
256
+
257
+ **Cursor** — add to `.cursor/mcp.json` in your project root:
258
+
259
+ ```json
260
+ {
261
+ "mcpServers": {
262
+ "patchwork": {
263
+ "command": "patchwork",
264
+ "args": ["serve", ".", "--stdio"]
265
+ }
266
+ }
267
+ }
268
+ ```
269
+
270
+ Then your AI agent can use 8 on-demand tools:
245
271
 
246
272
  | Tool | When to use |
247
273
  |---|---|
@@ -183,20 +183,46 @@ patchwork scan --claude-md
183
183
 
184
184
  ### Option 3: MCP server
185
185
 
186
- Add to `~/.claude/settings.json`:
186
+ **Claude Code** — add to `~/.claude.json` (or run `claude mcp add` interactively):
187
187
 
188
188
  ```json
189
189
  {
190
190
  "mcpServers": {
191
191
  "patchwork": {
192
192
  "command": "patchwork",
193
- "args": ["serve", "--stdio", "/path/to/your/project"]
193
+ "args": ["serve", "/path/to/your/project", "--stdio"]
194
194
  }
195
195
  }
196
196
  }
197
197
  ```
198
198
 
199
- Then Claude Code can use 8 on-demand tools:
199
+ **Claude Desktop** add to `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or `%APPDATA%\Claude\claude_desktop_config.json` (Windows):
200
+
201
+ ```json
202
+ {
203
+ "mcpServers": {
204
+ "patchwork": {
205
+ "command": "patchwork",
206
+ "args": ["serve", "/path/to/your/project", "--stdio"]
207
+ }
208
+ }
209
+ }
210
+ ```
211
+
212
+ **Cursor** — add to `.cursor/mcp.json` in your project root:
213
+
214
+ ```json
215
+ {
216
+ "mcpServers": {
217
+ "patchwork": {
218
+ "command": "patchwork",
219
+ "args": ["serve", ".", "--stdio"]
220
+ }
221
+ }
222
+ }
223
+ ```
224
+
225
+ Then your AI agent can use 8 on-demand tools:
200
226
 
201
227
  | Tool | When to use |
202
228
  |---|---|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "patchwork-conventions"
7
- version = "0.1.1"
7
+ version = "0.1.2"
8
8
  description = "Mine your codebase. Generate CONVENTIONS.md. Stop AI agents from making up your style."
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
@@ -111,7 +111,7 @@ def scan(
111
111
 
112
112
  if claude_md and out_path.exists():
113
113
  # Append patchwork section to existing CLAUDE.md
114
- existing = out_path.read_text()
114
+ existing = out_path.read_text(encoding="utf-8")
115
115
  marker = "<!-- patchwork:start -->"
116
116
  end_marker = "<!-- patchwork:end -->"
117
117
  if marker in existing:
@@ -126,7 +126,7 @@ def scan(
126
126
  else:
127
127
  text = existing.rstrip() + f"\n\n{marker}\n{text}\n{end_marker}\n"
128
128
 
129
- out_path.write_text(text)
129
+ out_path.write_text(text, encoding="utf-8")
130
130
 
131
131
  if not quiet:
132
132
  _print_summary(report, out_path)
@@ -145,7 +145,7 @@ def update(path: str, output: str | None) -> None:
145
145
  # Load any existing manual annotations
146
146
  manual_sections: dict[str, str] = {}
147
147
  if out_path.exists():
148
- manual_sections = _extract_manual_sections(out_path.read_text())
148
+ manual_sections = _extract_manual_sections(out_path.read_text(encoding="utf-8"))
149
149
 
150
150
  opts = ScanOptions(root=root)
151
151
  with Progress(SpinnerColumn(), TextColumn("{task.description}"), transient=True) as p:
@@ -158,7 +158,7 @@ def update(path: str, output: str | None) -> None:
158
158
  for heading, content in manual_sections.items():
159
159
  text += f"\n\n## {heading} (manual)\n\n{content}"
160
160
 
161
- out_path.write_text(text)
161
+ out_path.write_text(text, encoding="utf-8")
162
162
  console.print(f"[green]✓[/green] Updated [bold]{out_path}[/bold]")
163
163
  if manual_sections:
164
164
  console.print(f" Preserved {len(manual_sections)} manual section(s)")
@@ -182,7 +182,7 @@ def diff(path: str) -> None:
182
182
  console.print("[yellow]No existing CONVENTIONS.md — run `patchwork scan` first[/yellow]")
183
183
  sys.exit(1)
184
184
 
185
- old_text = out_path.read_text()
185
+ old_text = out_path.read_text(encoding="utf-8")
186
186
  diffs = list(difflib.unified_diff(
187
187
  old_text.splitlines(),
188
188
  new_text.splitlines(),
@@ -241,7 +241,7 @@ def watch(path: str, interval: float) -> None:
241
241
  if changed:
242
242
  last_mtime = current
243
243
  report = do_scan(opts)
244
- out_path.write_text(report.to_markdown())
244
+ out_path.write_text(report.to_markdown(), encoding="utf-8")
245
245
  console.print(f"[green]↺[/green] CONVENTIONS.md updated")
246
246
  except KeyboardInterrupt:
247
247
  console.print("\n[dim]Stopped[/dim]")
@@ -189,9 +189,34 @@ async def run_server(root: Path, port: int = 3742, stdio: bool = True) -> None:
189
189
  ),
190
190
  ]
191
191
 
192
+ def _resolve_scan_root(raw_path: str | None) -> Path:
193
+ """Resolve and validate the scan path.
194
+
195
+ Rejects paths that escape the filesystem root or point to sensitive
196
+ system directories, and normalises symlinks so containment checks
197
+ are reliable. The MCP server is a read-only tool, but we still
198
+ validate inputs at the boundary to follow least-privilege.
199
+ """
200
+ candidate = Path(raw_path).resolve() if raw_path else root.resolve()
201
+
202
+ # Reject obviously dangerous system paths
203
+ _BLOCKED = {"/etc", "/proc", "/sys", "/dev", "/private/etc"}
204
+ for blocked in _BLOCKED:
205
+ if str(candidate).startswith(blocked):
206
+ raise ValueError(f"Scanning system path '{candidate}' is not allowed.")
207
+
208
+ # Must be an existing directory (not a file or a socket etc.)
209
+ if not candidate.is_dir():
210
+ raise ValueError(f"Path '{candidate}' is not a directory.")
211
+
212
+ return candidate
213
+
192
214
  @server.call_tool()
193
215
  async def call_tool(name: str, arguments: dict) -> list[types.TextContent]:
194
- scan_root = Path(arguments.get("path", str(root))).resolve()
216
+ try:
217
+ scan_root = _resolve_scan_root(arguments.get("path"))
218
+ except ValueError as exc:
219
+ return [types.TextContent(type="text", text=f"Error: {exc}")]
195
220
 
196
221
  if arguments.get("refresh"):
197
222
  _invalidate(scan_root)
@@ -391,7 +416,7 @@ async def run_server(root: Path, port: int = 3742, stdio: bool = True) -> None:
391
416
  Route("/sse", endpoint=handle_sse),
392
417
  Mount("/messages", app=sse.handle_post_message),
393
418
  ])
394
- uvicorn.run(app, host="0.0.0.0", port=port)
419
+ uvicorn.run(app, host="127.0.0.1", port=port)
395
420
 
396
421
 
397
422
  def _check_convention(name: str, kind: str, lang: str, report: ConventionReport) -> str:
@@ -17,6 +17,19 @@ except ImportError:
17
17
  except ImportError:
18
18
  tomllib = None # type: ignore
19
19
 
20
+ # Hard limit on config file size to prevent DoS from pathologically large files
21
+ _MAX_CONFIG_BYTES = 1 * 1024 * 1024 # 1 MB
22
+
23
+
24
+ def _safe_read(path: Path) -> str | None:
25
+ """Read a config file, returning None if it exceeds the size limit."""
26
+ try:
27
+ if path.stat().st_size > _MAX_CONFIG_BYTES:
28
+ return None
29
+ return path.read_text(encoding="utf-8", errors="replace")
30
+ except OSError:
31
+ return None
32
+
20
33
 
21
34
  @dataclass
22
35
  class ProjectConfig:
@@ -129,9 +142,12 @@ class ConfigDetector:
129
142
  return cfg
130
143
 
131
144
  def _read_package_json(self, path: Path, cfg: ProjectConfig) -> None:
145
+ text = _safe_read(path)
146
+ if text is None:
147
+ return
132
148
  try:
133
- data = json.loads(path.read_text())
134
- except (json.JSONDecodeError, OSError):
149
+ data = json.loads(text)
150
+ except json.JSONDecodeError:
135
151
  return
136
152
 
137
153
  cfg.language = "javascript/typescript"
@@ -179,8 +195,11 @@ class ConfigDetector:
179
195
  cfg.language = "python"
180
196
  if tomllib is None:
181
197
  return
198
+ text = _safe_read(path)
199
+ if text is None:
200
+ return
182
201
  try:
183
- data = tomllib.loads(path.read_text())
202
+ data = tomllib.loads(text)
184
203
  except Exception:
185
204
  return
186
205
 
@@ -225,9 +244,8 @@ class ConfigDetector:
225
244
  def _read_go_mod(self, path: Path, cfg: ProjectConfig) -> None:
226
245
  cfg.language = "go"
227
246
  cfg.package_manager = "go"
228
- try:
229
- text = path.read_text()
230
- except OSError:
247
+ text = _safe_read(path)
248
+ if text is None:
231
249
  return
232
250
  m = re.search(r'^module\s+(\S+)', text, re.MULTILINE)
233
251
  if m:
@@ -253,8 +271,11 @@ class ConfigDetector:
253
271
  cfg.package_manager = "cargo"
254
272
  if tomllib is None:
255
273
  return
274
+ text = _safe_read(path)
275
+ if text is None:
276
+ return
256
277
  try:
257
- data = tomllib.loads(path.read_text())
278
+ data = tomllib.loads(text)
258
279
  except Exception:
259
280
  return
260
281
  pkg = data.get("package", {})
@@ -9,7 +9,7 @@ GitPatternMiner — Mines git history for workflow conventions:
9
9
  from __future__ import annotations
10
10
 
11
11
  import re
12
- import subprocess
12
+ import subprocess # nosec B404 — used only for 'git' with a hardcoded arg list, no shell=True
13
13
  from collections import Counter, defaultdict
14
14
  from dataclasses import dataclass, field
15
15
  from pathlib import Path
@@ -40,7 +40,7 @@ _BRANCH_FIX = re.compile(r'^(fix|hotfix|bugfix)/[\w/-]+$')
40
40
 
41
41
  def _run_git(args: list[str], cwd: Path, max_bytes: int = 500_000) -> str:
42
42
  try:
43
- result = subprocess.run(
43
+ result = subprocess.run( # nosec B603 — list args, no shell=True, cwd is a validated Path
44
44
  ["git"] + args,
45
45
  cwd=str(cwd),
46
46
  capture_output=True,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: patchwork-conventions
3
- Version: 0.1.1
3
+ Version: 0.1.2
4
4
  Summary: Mine your codebase. Generate CONVENTIONS.md. Stop AI agents from making up your style.
5
5
  Author: patchwork contributors
6
6
  License: MIT
@@ -228,20 +228,46 @@ patchwork scan --claude-md
228
228
 
229
229
  ### Option 3: MCP server
230
230
 
231
- Add to `~/.claude/settings.json`:
231
+ **Claude Code** — add to `~/.claude.json` (or run `claude mcp add` interactively):
232
232
 
233
233
  ```json
234
234
  {
235
235
  "mcpServers": {
236
236
  "patchwork": {
237
237
  "command": "patchwork",
238
- "args": ["serve", "--stdio", "/path/to/your/project"]
238
+ "args": ["serve", "/path/to/your/project", "--stdio"]
239
239
  }
240
240
  }
241
241
  }
242
242
  ```
243
243
 
244
- Then Claude Code can use 8 on-demand tools:
244
+ **Claude Desktop** add to `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or `%APPDATA%\Claude\claude_desktop_config.json` (Windows):
245
+
246
+ ```json
247
+ {
248
+ "mcpServers": {
249
+ "patchwork": {
250
+ "command": "patchwork",
251
+ "args": ["serve", "/path/to/your/project", "--stdio"]
252
+ }
253
+ }
254
+ }
255
+ ```
256
+
257
+ **Cursor** — add to `.cursor/mcp.json` in your project root:
258
+
259
+ ```json
260
+ {
261
+ "mcpServers": {
262
+ "patchwork": {
263
+ "command": "patchwork",
264
+ "args": ["serve", ".", "--stdio"]
265
+ }
266
+ }
267
+ }
268
+ ```
269
+
270
+ Then your AI agent can use 8 on-demand tools:
245
271
 
246
272
  | Tool | When to use |
247
273
  |---|---|