patchwork-conventions 0.1.0__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.0 → patchwork_conventions-0.1.2}/PKG-INFO +30 -4
  2. {patchwork_conventions-0.1.0 → patchwork_conventions-0.1.2}/README.md +29 -3
  3. {patchwork_conventions-0.1.0 → patchwork_conventions-0.1.2}/pyproject.toml +1 -1
  4. {patchwork_conventions-0.1.0 → patchwork_conventions-0.1.2}/src/patchwork/cli.py +6 -6
  5. {patchwork_conventions-0.1.0 → patchwork_conventions-0.1.2}/src/patchwork/mcp/server.py +27 -2
  6. {patchwork_conventions-0.1.0 → patchwork_conventions-0.1.2}/src/patchwork/miners/api_patterns.py +41 -35
  7. {patchwork_conventions-0.1.0 → patchwork_conventions-0.1.2}/src/patchwork/miners/config_detector.py +28 -7
  8. {patchwork_conventions-0.1.0 → patchwork_conventions-0.1.2}/src/patchwork/miners/git_patterns.py +2 -2
  9. {patchwork_conventions-0.1.0 → patchwork_conventions-0.1.2}/src/patchwork/miners/imports.py +7 -4
  10. {patchwork_conventions-0.1.0 → patchwork_conventions-0.1.2}/src/patchwork/output/report.py +1 -1
  11. {patchwork_conventions-0.1.0 → patchwork_conventions-0.1.2}/src/patchwork_conventions.egg-info/PKG-INFO +30 -4
  12. {patchwork_conventions-0.1.0 → patchwork_conventions-0.1.2}/LICENSE +0 -0
  13. {patchwork_conventions-0.1.0 → patchwork_conventions-0.1.2}/setup.cfg +0 -0
  14. {patchwork_conventions-0.1.0 → patchwork_conventions-0.1.2}/src/patchwork/__init__.py +0 -0
  15. {patchwork_conventions-0.1.0 → patchwork_conventions-0.1.2}/src/patchwork/mcp/__init__.py +0 -0
  16. {patchwork_conventions-0.1.0 → patchwork_conventions-0.1.2}/src/patchwork/miners/__init__.py +0 -0
  17. {patchwork_conventions-0.1.0 → patchwork_conventions-0.1.2}/src/patchwork/miners/ast_base.py +0 -0
  18. {patchwork_conventions-0.1.0 → patchwork_conventions-0.1.2}/src/patchwork/miners/error_handling.py +0 -0
  19. {patchwork_conventions-0.1.0 → patchwork_conventions-0.1.2}/src/patchwork/miners/naming.py +0 -0
  20. {patchwork_conventions-0.1.0 → patchwork_conventions-0.1.2}/src/patchwork/miners/structure.py +0 -0
  21. {patchwork_conventions-0.1.0 → patchwork_conventions-0.1.2}/src/patchwork/miners/testing.py +0 -0
  22. {patchwork_conventions-0.1.0 → patchwork_conventions-0.1.2}/src/patchwork/output/__init__.py +0 -0
  23. {patchwork_conventions-0.1.0 → patchwork_conventions-0.1.2}/src/patchwork/scanner.py +0 -0
  24. {patchwork_conventions-0.1.0 → patchwork_conventions-0.1.2}/src/patchwork_conventions.egg-info/SOURCES.txt +0 -0
  25. {patchwork_conventions-0.1.0 → patchwork_conventions-0.1.2}/src/patchwork_conventions.egg-info/dependency_links.txt +0 -0
  26. {patchwork_conventions-0.1.0 → patchwork_conventions-0.1.2}/src/patchwork_conventions.egg-info/entry_points.txt +0 -0
  27. {patchwork_conventions-0.1.0 → patchwork_conventions-0.1.2}/src/patchwork_conventions.egg-info/requires.txt +0 -0
  28. {patchwork_conventions-0.1.0 → patchwork_conventions-0.1.2}/src/patchwork_conventions.egg-info/top_level.txt +0 -0
  29. {patchwork_conventions-0.1.0 → patchwork_conventions-0.1.2}/tests/test_naming.py +0 -0
  30. {patchwork_conventions-0.1.0 → 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.0
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.0"
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:
@@ -108,12 +108,14 @@ def _detect_apis(paths: list[Path], lang: str) -> APIResult:
108
108
  colon_routes = 0
109
109
  brace_routes = 0
110
110
  angle_routes = 0
111
- orm_counts: Counter[str] = Counter()
112
- fw_counts: Counter[str] = Counter()
111
+ # Track per-file hits (not per-match) to avoid false positives from
112
+ # code that merely mentions framework names in strings, comments, or docs.
113
+ orm_files: Counter[str] = Counter()
114
+ fw_files: Counter[str] = Counter()
113
115
  async_counts: Counter[str] = Counter()
114
116
  http_client_counts: Counter[str] = Counter()
115
- has_graphql = False
116
- has_grpc = False
117
+ gql_files = 0
118
+ grpc_files = 0
117
119
 
118
120
  for path in paths[:200]:
119
121
  try:
@@ -129,32 +131,40 @@ def _detect_apis(paths: list[Path], lang: str) -> APIResult:
129
131
  brace_routes += len(_ROUTE_BRACE.findall(text))
130
132
  angle_routes += len(_ROUTE_ANGLE.findall(text))
131
133
 
132
- for orm, patterns in _ORM_SIGNALS.items():
133
- for pat in patterns:
134
- if re.search(pat, text):
135
- orm_counts[orm] += 1
136
- break
134
+ # Skip files that are themselves pattern-definition files (e.g. this miner)
135
+ # to avoid self-matching on our own regex strings.
136
+ if "_ORM_SIGNALS" in text or "_FRAMEWORK_SIGNALS" in text:
137
+ continue
138
+
139
+ # Count each ORM/framework once per file (not per pattern match)
140
+ for orm_name, patterns in _ORM_SIGNALS.items():
141
+ if any(re.search(pat, text) for pat in patterns):
142
+ orm_files[orm_name] += 1
137
143
 
138
144
  for fw, patterns in _FRAMEWORK_SIGNALS.items():
139
- for pat in patterns:
140
- if re.search(pat, text):
141
- fw_counts[fw] += 1
142
- break
145
+ if any(re.search(pat, text) for pat in patterns):
146
+ fw_files[fw] += 1
143
147
 
144
148
  for style, patterns in _ASYNC_SIGNALS.get(lang, {}).items():
145
- for pat in patterns:
146
- if re.search(pat, text):
147
- async_counts[style] += 1
148
- break
149
+ if any(re.search(pat, text) for pat in patterns):
150
+ async_counts[style] += 1
149
151
 
150
152
  for client in _HTTP_CLIENTS.get(lang, []):
151
- if client in text:
153
+ # Match actual import/usage, not just the string appearing in comments or dicts
154
+ if re.search(rf'(?:import\s+{re.escape(client)}|from\s+{re.escape(client)}\s|{re.escape(client)}\.)', text):
152
155
  http_client_counts[client] += 1
153
156
 
154
- if "graphql" in text.lower() or "GraphQL" in text or "gql`" in text:
155
- has_graphql = True
156
- if "proto" in text.lower() or "grpc" in text.lower() or "protobuf" in text.lower():
157
- has_grpc = True
157
+ if re.search(r'\bgraphql\b|gql`', text, re.IGNORECASE):
158
+ gql_files += 1
159
+ if re.search(r'\bgrpc\b|\bprotobuf\b|\.proto["\']', text, re.IGNORECASE):
160
+ grpc_files += 1
161
+
162
+ # Require signal in ≥3 files to filter out false positives.
163
+ # Single or double-file mentions are often: config files, the tool's own
164
+ # pattern dictionaries, test fixtures, or README-like docstrings.
165
+ min_files = 3
166
+ orm_files = Counter({k: v for k, v in orm_files.items() if v >= min_files})
167
+ fw_files = Counter({k: v for k, v in fw_files.items() if v >= min_files})
158
168
 
159
169
  # Response shape
160
170
  response_shape = None
@@ -169,27 +179,23 @@ def _detect_apis(paths: list[Path], lang: str) -> APIResult:
169
179
  route_total = colon_routes + brace_routes + angle_routes
170
180
  route_style = None
171
181
  if route_total > 0:
172
- if colon_routes == max(colon_routes, brace_routes, angle_routes):
182
+ best = max(colon_routes, brace_routes, angle_routes)
183
+ if colon_routes == best:
173
184
  route_style = ":id (Express style)"
174
- elif brace_routes == max(colon_routes, brace_routes, angle_routes):
185
+ elif brace_routes == best:
175
186
  route_style = "{id} (FastAPI style)"
176
187
  else:
177
188
  route_style = "<id> (Flask style)"
178
189
 
179
- async_pattern = async_counts.most_common(1)[0][0] if async_counts else None
180
- orm = orm_counts.most_common(1)[0][0] if orm_counts else None
181
- http_client = http_client_counts.most_common(1)[0][0] if http_client_counts else None
182
- api_frameworks = [fw for fw, _ in fw_counts.most_common(3)]
183
-
184
190
  return APIResult(
185
191
  response_shape=response_shape,
186
192
  route_param_style=route_style,
187
- async_pattern=async_pattern,
188
- orm=orm,
189
- has_graphql=has_graphql,
190
- has_grpc=has_grpc,
191
- api_frameworks=api_frameworks,
192
- http_client=http_client,
193
+ async_pattern=async_counts.most_common(1)[0][0] if async_counts else None,
194
+ orm=orm_files.most_common(1)[0][0] if orm_files else None,
195
+ has_graphql=gql_files >= 3,
196
+ has_grpc=grpc_files >= 3,
197
+ api_frameworks=[fw for fw, _ in fw_files.most_common(3)],
198
+ http_client=http_client_counts.most_common(1)[0][0] if http_client_counts else None,
193
199
  )
194
200
 
195
201
 
@@ -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,
@@ -29,7 +29,7 @@ class ImportResult:
29
29
 
30
30
  _PY_RELATIVE = re.compile(r'^\s*from\s+\.', re.MULTILINE)
31
31
  _PY_ABSOLUTE = re.compile(r'^\s*(?:import|from)\s+(?!\.)', re.MULTILINE)
32
- _PY_IMPORT = re.compile(r'^\s*(?:from\s+([\w.]+)\s+import|import\s+([\w.,\s]+))', re.MULTILINE)
32
+ _PY_IMPORT = re.compile(r'^\s*(?:from\s+([\w.]+)\s+import|import\s+([\w]+(?:\s*,\s*[\w]+)*))', re.MULTILINE)
33
33
 
34
34
  _JS_RELATIVE = re.compile(r"""(?:import|require)\s*\(?['"](\./|\.\./)""", re.MULTILINE)
35
35
  _JS_ABSOLUTE_ALIAS = re.compile(r"""(?:import|require)\s*\(?['"](@\w+/|~/)""", re.MULTILINE)
@@ -60,9 +60,12 @@ def _detect_py_imports(paths: list[Path]) -> ImportResult:
60
60
  relative_count += len(_PY_RELATIVE.findall(text))
61
61
  absolute_count += len(_PY_ABSOLUTE.findall(text))
62
62
  for m in _PY_IMPORT.finditer(text):
63
- mod = (m.group(1) or m.group(2) or "").strip().split(".")[0]
64
- if mod:
65
- all_modules.append(mod)
63
+ raw = (m.group(1) or m.group(2) or "").strip()
64
+ # group(1) is a dotted module path; group(2) may be 'os, sys, re'
65
+ for part in raw.split(","):
66
+ mod = part.strip().split(".")[0]
67
+ if mod and mod.isidentifier():
68
+ all_modules.append(mod)
66
69
  # Detect src/ or similar path aliases in pyproject/setup.cfg
67
70
  if "@" in text or "from src." in text:
68
71
  aliases.add("src/")
@@ -50,7 +50,7 @@ class ConventionReport:
50
50
 
51
51
  lines += [
52
52
  f"# {filename}",
53
- f"> Auto-generated by [patchwork](https://github.com/yourusername/patchwork) on {ts} ",
53
+ f"> Auto-generated by [patchwork](https://github.com/SaiNarayana-B/patchwork) on {ts} ",
54
54
  f"> Scanned {self.file_count} files in {self.elapsed:.1f}s",
55
55
  f"> **Do not edit manually** — run `patchwork update` to refresh",
56
56
  "",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: patchwork-conventions
3
- Version: 0.1.0
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
  |---|---|