Files
claude-smart-usage/scan.py
Claude d30c26aa8e Redesign around web-based Claude Code workflow
Instead of a CLI the user runs separately, this is now two files you
drop into any repo:

- CLAUDE.md: read automatically by Claude Code, teaches it to scan
  first, commit often, and leave handoff notes when usage runs low
- scan.py: standalone script Claude runs itself to find errors via
  linters/tests/type-checkers, using zero AI tokens

No workflow change for the user — they type in Claude Code as before.

https://claude.ai/code/session_01Fzv8baXnEVVhnrffAb3Ucc
2026-03-29 23:01:09 +00:00

197 lines
5.9 KiB
Python

#!/usr/bin/env python3
"""Fast codebase scanner — finds errors using linters, tests, and type checkers.
Claude Code runs this script itself via bash. It uses zero AI tokens because
it's just running your project's existing tools and collecting the output.
Usage (run by Claude Code, not by you):
python scan.py [project_root]
Outputs a structured report to stdout that Claude can read and act on.
Auto-detects project type: Python, Node/TypeScript, Rust, Go.
"""
import subprocess
import shutil
import sys
from pathlib import Path
def run(cmd, cwd, timeout=120):
"""Run a command, return (exit_code, output)."""
try:
r = subprocess.run(cmd, cwd=cwd, capture_output=True, text=True, timeout=timeout)
out = r.stdout
if r.stderr:
out += "\n" + r.stderr
return r.returncode, out.strip()
except subprocess.TimeoutExpired:
return -1, f"TIMEOUT after {timeout}s: {' '.join(cmd)}"
except FileNotFoundError:
return -1, ""
def extract_files(output, root):
"""Pull file paths from linter output like 'path/file.py:10: error'."""
files = set()
for line in output.splitlines():
if ":" in line:
candidate = line.split(":")[0].strip().lstrip("./")
if candidate and (root / candidate).exists():
files.add(candidate)
return sorted(files)
def scan_python(root):
"""Run Python checks: pytest, ruff, mypy, syntax."""
sections = []
if not any((root / f).exists() for f in ["pyproject.toml", "setup.py", "requirements.txt"]):
return sections
# pytest
for cmd in [["python", "-m", "pytest", "--tb=short", "-q"], ["pytest", "--tb=short", "-q"]]:
code, out = run(cmd, root)
if code == -1 and not out:
continue
if code != 0 and out:
sections.append(("Test Failures", out))
break
# ruff
if shutil.which("ruff"):
code, out = run(["ruff", "check", "."], root)
if code != 0 and out:
sections.append(("Lint Errors (ruff)", out))
# mypy
if shutil.which("mypy"):
code, out = run(["mypy", ".", "--no-error-summary"], root, timeout=180)
if code != 0 and out:
sections.append(("Type Errors (mypy)", out))
# Syntax errors
syntax_errors = []
for py_file in list(root.rglob("*.py"))[:500]:
rel = py_file.relative_to(root)
if any(p in {".venv", "venv", "node_modules", "__pycache__", ".git", ".tox"} for p in rel.parts):
continue
code, out = run(["python", "-c", f"import ast; ast.parse(open('{py_file}').read())"], root)
if code != 0:
syntax_errors.append(f"{rel}: SyntaxError - {out.splitlines()[-1] if out else 'unknown'}")
if syntax_errors:
sections.append(("Syntax Errors", "\n".join(syntax_errors)))
return sections
def scan_node(root):
"""Run Node/TypeScript checks: test, eslint, tsc."""
sections = []
if not (root / "package.json").exists():
return sections
pkg = "yarn" if (root / "yarn.lock").exists() else "npm"
code, out = run([pkg, "test", "--", "--reporter=min"], root, timeout=180)
if code != 0 and out:
sections.append(("Test Failures", out))
if shutil.which("npx"):
code, out = run(["npx", "eslint", ".", "--format=compact", "--no-warn-ignored"], root, timeout=120)
if code != 0 and out:
sections.append(("Lint Errors (eslint)", out))
if (root / "tsconfig.json").exists():
code, out = run(["npx", "tsc", "--noEmit"], root, timeout=180)
if code != 0 and out:
sections.append(("TypeScript Errors", out))
return sections
def scan_rust(root):
"""Run Rust checks: cargo check, cargo test --no-run."""
sections = []
if not (root / "Cargo.toml").exists():
return sections
code, out = run(["cargo", "check", "--message-format=short"], root, timeout=300)
if code != 0 and out:
sections.append(("Build Errors (cargo)", out))
code, out = run(["cargo", "test", "--no-run"], root, timeout=300)
if code != 0 and out:
sections.append(("Test Compilation Errors", out))
return sections
def scan_go(root):
"""Run Go checks: build, vet, test."""
sections = []
if not (root / "go.mod").exists():
return sections
code, out = run(["go", "build", "./..."], root, timeout=180)
if code != 0 and out:
sections.append(("Build Errors (go)", out))
code, out = run(["go", "vet", "./..."], root, timeout=120)
if code != 0 and out:
sections.append(("Vet Warnings (go)", out))
code, out = run(["go", "test", "-short", "-count=1", "./..."], root, timeout=180)
if code != 0 and out:
sections.append(("Test Failures (go)", out))
return sections
def main():
root = Path(sys.argv[1] if len(sys.argv) > 1 else ".").resolve()
if not root.is_dir():
print(f"Error: {root} is not a directory", file=sys.stderr)
sys.exit(1)
all_sections = []
all_sections.extend(scan_python(root))
all_sections.extend(scan_node(root))
all_sections.extend(scan_rust(root))
all_sections.extend(scan_go(root))
if not all_sections:
print("SCAN COMPLETE: No errors found.")
sys.exit(0)
# Collect all files with issues
all_files = set()
for title, output in all_sections:
all_files.update(extract_files(output, root))
# Print structured report
print(f"SCAN COMPLETE: Found issues in {len(all_sections)} categories across {len(all_files)} files.\n")
for title, output in all_sections:
print(f"=== {title} ===")
lines = output.splitlines()
if len(lines) > 80:
print("(truncated to last 80 lines)")
print("\n".join(lines[-80:]))
else:
print(output)
print()
if all_files:
print("=== Files with Issues ===")
for f in sorted(all_files):
print(f" {f}")
if __name__ == "__main__":
main()