codetour-cli 0.2.0__tar.gz → 0.3.0__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 (57) hide show
  1. codetour_cli-0.3.0/PKG-INFO +138 -0
  2. codetour_cli-0.3.0/README.md +99 -0
  3. {codetour_cli-0.2.0 → codetour_cli-0.3.0}/pyproject.toml +10 -1
  4. codetour_cli-0.3.0/src/codetour_cli/mcp_server.py +279 -0
  5. codetour_cli-0.3.0/src/codetour_cli.egg-info/PKG-INFO +138 -0
  6. {codetour_cli-0.2.0 → codetour_cli-0.3.0}/src/codetour_cli.egg-info/SOURCES.txt +2 -0
  7. {codetour_cli-0.2.0 → codetour_cli-0.3.0}/src/codetour_cli.egg-info/entry_points.txt +1 -0
  8. {codetour_cli-0.2.0 → codetour_cli-0.3.0}/src/codetour_cli.egg-info/requires.txt +3 -0
  9. codetour_cli-0.3.0/tests/test_mcp_server.py +253 -0
  10. codetour_cli-0.2.0/PKG-INFO +0 -47
  11. codetour_cli-0.2.0/README.md +0 -10
  12. codetour_cli-0.2.0/src/codetour_cli.egg-info/PKG-INFO +0 -47
  13. {codetour_cli-0.2.0 → codetour_cli-0.3.0}/LICENSE +0 -0
  14. {codetour_cli-0.2.0 → codetour_cli-0.3.0}/setup.cfg +0 -0
  15. {codetour_cli-0.2.0 → codetour_cli-0.3.0}/src/codetour_cli/__init__.py +0 -0
  16. {codetour_cli-0.2.0 → codetour_cli-0.3.0}/src/codetour_cli/cli.py +0 -0
  17. {codetour_cli-0.2.0 → codetour_cli-0.3.0}/src/codetour_cli/config.py +0 -0
  18. {codetour_cli-0.2.0 → codetour_cli-0.3.0}/src/codetour_cli/description_validator.py +0 -0
  19. {codetour_cli-0.2.0 → codetour_cli-0.3.0}/src/codetour_cli/detection.py +0 -0
  20. {codetour_cli-0.2.0 → codetour_cli-0.3.0}/src/codetour_cli/discovery.py +0 -0
  21. {codetour_cli-0.2.0 → codetour_cli-0.3.0}/src/codetour_cli/lint.py +0 -0
  22. {codetour_cli-0.2.0 → codetour_cli-0.3.0}/src/codetour_cli/logging/__init__.py +0 -0
  23. {codetour_cli-0.2.0 → codetour_cli-0.3.0}/src/codetour_cli/logging/setup.py +0 -0
  24. {codetour_cli-0.2.0 → codetour_cli-0.3.0}/src/codetour_cli/migration/__init__.py +0 -0
  25. {codetour_cli-0.2.0 → codetour_cli-0.3.0}/src/codetour_cli/migration/direct.py +0 -0
  26. {codetour_cli-0.2.0 → codetour_cli-0.3.0}/src/codetour_cli/migration/git_diff.py +0 -0
  27. {codetour_cli-0.2.0 → codetour_cli-0.3.0}/src/codetour_cli/migration/stepwise.py +0 -0
  28. {codetour_cli-0.2.0 → codetour_cli-0.3.0}/src/codetour_cli/review_parser.py +0 -0
  29. {codetour_cli-0.2.0 → codetour_cli-0.3.0}/src/codetour_cli/schema_models.py +0 -0
  30. {codetour_cli-0.2.0 → codetour_cli-0.3.0}/src/codetour_cli/tour/__init__.py +0 -0
  31. {codetour_cli-0.2.0 → codetour_cli-0.3.0}/src/codetour_cli/tour/schema.py +0 -0
  32. {codetour_cli-0.2.0 → codetour_cli-0.3.0}/src/codetour_cli/tour/updater.py +0 -0
  33. {codetour_cli-0.2.0 → codetour_cli-0.3.0}/src/codetour_cli.egg-info/dependency_links.txt +0 -0
  34. {codetour_cli-0.2.0 → codetour_cli-0.3.0}/src/codetour_cli.egg-info/top_level.txt +0 -0
  35. {codetour_cli-0.2.0 → codetour_cli-0.3.0}/tests/test_advanced_scenarios.py +0 -0
  36. {codetour_cli-0.2.0 → codetour_cli-0.3.0}/tests/test_apply_review_surgical_integration.py +0 -0
  37. {codetour_cli-0.2.0 → codetour_cli-0.3.0}/tests/test_description_validation.py +0 -0
  38. {codetour_cli-0.2.0 → codetour_cli-0.3.0}/tests/test_detection.py +0 -0
  39. {codetour_cli-0.2.0 → codetour_cli-0.3.0}/tests/test_direct_migration.py +0 -0
  40. {codetour_cli-0.2.0 → codetour_cli-0.3.0}/tests/test_fuzzy_repo_calibration.py +0 -0
  41. {codetour_cli-0.2.0 → codetour_cli-0.3.0}/tests/test_git_diff_parser.py +0 -0
  42. {codetour_cli-0.2.0 → codetour_cli-0.3.0}/tests/test_harness.py +0 -0
  43. {codetour_cli-0.2.0 → codetour_cli-0.3.0}/tests/test_integration.py +0 -0
  44. {codetour_cli-0.2.0 → codetour_cli-0.3.0}/tests/test_lint.py +0 -0
  45. {codetour_cli-0.2.0 → codetour_cli-0.3.0}/tests/test_lint_cli.py +0 -0
  46. {codetour_cli-0.2.0 → codetour_cli-0.3.0}/tests/test_logging_setup.py +0 -0
  47. {codetour_cli-0.2.0 → codetour_cli-0.3.0}/tests/test_pattern_validation.py +0 -0
  48. {codetour_cli-0.2.0 → codetour_cli-0.3.0}/tests/test_post_migration_lint.py +0 -0
  49. {codetour_cli-0.2.0 → codetour_cli-0.3.0}/tests/test_review_roundtrip.py +0 -0
  50. {codetour_cli-0.2.0 → codetour_cli-0.3.0}/tests/test_review_workflow_options.py +0 -0
  51. {codetour_cli-0.2.0 → codetour_cli-0.3.0}/tests/test_scenarios.py +0 -0
  52. {codetour_cli-0.2.0 → codetour_cli-0.3.0}/tests/test_schema_validation.py +0 -0
  53. {codetour_cli-0.2.0 → codetour_cli-0.3.0}/tests/test_stepwise_migration.py +0 -0
  54. {codetour_cli-0.2.0 → codetour_cli-0.3.0}/tests/test_synthetic_repo_migration_engine.py +0 -0
  55. {codetour_cli-0.2.0 → codetour_cli-0.3.0}/tests/test_templates.py +0 -0
  56. {codetour_cli-0.2.0 → codetour_cli-0.3.0}/tests/test_tour_schema.py +0 -0
  57. {codetour_cli-0.2.0 → codetour_cli-0.3.0}/tests/test_tour_updater.py +0 -0
@@ -0,0 +1,138 @@
1
+ Metadata-Version: 2.4
2
+ Name: codetour-cli
3
+ Version: 0.3.0
4
+ Summary: Algorithmic maintenance of CodeTour files — an unofficial companion CLI for the CodeTour VS Code extension
5
+ Author: Jérémie Lumbroso
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/jlumbroso/codetour-cli
8
+ Project-URL: Repository, https://github.com/jlumbroso/codetour-cli
9
+ Keywords: codetour,code-tour,vscode,git,cli
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Topic :: Software Development :: Version Control :: Git
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Requires-Python: >=3.10
19
+ Description-Content-Type: text/markdown
20
+ License-File: LICENSE
21
+ Requires-Dist: gitpython
22
+ Requires-Dist: pydantic>=2.0
23
+ Requires-Dist: structlog
24
+ Requires-Dist: rich
25
+ Requires-Dist: python-dotenv
26
+ Requires-Dist: pyyaml
27
+ Requires-Dist: click>=8.0
28
+ Requires-Dist: jinja2>=3.0
29
+ Requires-Dist: json-source-edit<2,>=1.0
30
+ Provides-Extra: mcp
31
+ Requires-Dist: mcp>=1.2; extra == "mcp"
32
+ Provides-Extra: dev
33
+ Requires-Dist: pytest; extra == "dev"
34
+ Requires-Dist: pytest-cov; extra == "dev"
35
+ Requires-Dist: black; extra == "dev"
36
+ Requires-Dist: ruff; extra == "dev"
37
+ Requires-Dist: mypy; extra == "dev"
38
+ Dynamic: license-file
39
+
40
+ # CodeTour CLI
41
+
42
+ [![Tests](https://github.com/jlumbroso/codetour-cli/actions/workflows/test.yml/badge.svg)](https://github.com/jlumbroso/codetour-cli/actions/workflows/test.yml)
43
+ [![Coverage ≥60%](https://img.shields.io/badge/coverage-%E2%89%A560%25-yellowgreen)](https://github.com/jlumbroso/codetour-cli/actions/workflows/test.yml)
44
+ [![PyPI](https://img.shields.io/pypi/v/codetour-cli)](https://pypi.org/project/codetour-cli/)
45
+ [![Python versions](https://img.shields.io/pypi/pyversions/codetour-cli)](https://pypi.org/project/codetour-cli/)
46
+ [![Downloads](https://static.pepy.tech/badge/codetour-cli)](https://pepy.tech/projects/codetour-cli)
47
+ [![License](https://img.shields.io/github/license/jlumbroso/codetour-cli)](https://github.com/jlumbroso/codetour-cli/blob/main/LICENSE)
48
+
49
+ **Algorithmic maintenance of [CodeTour](https://github.com/microsoft/codetour) files: keep `.tour` walkthroughs accurate as the code they narrate evolves.**
50
+
51
+ > **Unofficial companion tool.** `codetour-cli` is an independent,
52
+ > unofficial companion to [CodeTour](https://github.com/microsoft/codetour),
53
+ > the VS Code extension by Jonathan Carter (Microsoft). It is not
54
+ > affiliated with, endorsed by, or maintained by Microsoft or the CodeTour
55
+ > project. It exists to *serve* CodeTour and its users — keeping `.tour`
56
+ > files accurate as the code they walk through evolves.
57
+
58
+ ---
59
+
60
+ ## The problem
61
+
62
+ A CodeTour step pinned to `src/auth.py:42` is correct the day it's written. Twenty commits later, line 42 is something else entirely — and every tour in the repository is silently lying. Tours are the best onboarding artifact a codebase can have, *if* someone keeps them true. Nobody keeps them true by hand.
63
+
64
+ `codetour-cli` tracks each step across the commits between a tour's pinned `ref` and `HEAD` (git hunk mapping plus content heuristics), applies high-confidence line updates automatically with **byte-preserving surgical edits** (your diff shows the step change, not a rewritten file — via [`json-source-edit`](https://pypi.org/project/json-source-edit/)), and routes everything it isn't sure about to a review report designed to be worked by humans *and* AI agents alike.
65
+
66
+ ## Install
67
+
68
+ ```bash
69
+ pip install codetour-cli # Python ≥ 3.10
70
+ ```
71
+
72
+ ## Quickstart
73
+
74
+ ```bash
75
+ codetour-cli status # which tours lag HEAD?
76
+ codetour-cli migrate --dry-run # preview: what would move, what needs review
77
+ codetour-cli migrate # apply confident updates; write review report
78
+ codetour-cli lint # ground-truth check tours against the workspace
79
+ ```
80
+
81
+ ## The maintenance loop
82
+
83
+ ```
84
+ author tour ──► lint ──► commit, pin `ref`
85
+ ▲ │
86
+ │ ...code evolves...
87
+ │ │
88
+ │ status (tour lags HEAD?)
89
+ │ │
90
+ apply-review ◄── review report ◄── migrate [--dry-run]
91
+ (checked steps (auto-applies confident updates;
92
+ written back) low-confidence → the report)
93
+ ```
94
+
95
+ Steps the migration can't confidently place land in `MIGRATION-REVIEW-{tour}-{commit}.md`: each entry carries the step's original description (its *intent*), old → new location, why confidence dropped, and the actual code now at the proposed location. Check the boxes you approve — or correct the locations inline — then:
96
+
97
+ ```bash
98
+ codetour-cli apply-review MIGRATION-REVIEW-mytour-abc12345.md
99
+ ```
100
+
101
+ **The report is deliberately dual-audience**: an AI agent can read it, judge each proposed location against the step's stated intent, mark the checkboxes, and apply — the same workflow, no human bottleneck for the easy calls. (A companion [Claude skill](https://github.com/jlumbroso/codetour-skill/releases) teaches agents both tour authoring and this maintenance loop.)
102
+
103
+ ## Commands
104
+
105
+ | Command | Purpose |
106
+ |---|---|
107
+ | `init` | Set up `.codetour-cli.yml` configuration |
108
+ | `status` | Health of every tour: current vs. lagging HEAD |
109
+ | `check [tour]` | Validate tour structure without migrating |
110
+ | `lint [paths]` | Step-level checks against the workspace: missing files, out-of-range lines, non-matching or ambiguous patterns, broken `nextTour` links (`--format json`, `--strict`) |
111
+ | `migrate [tour]` | Track steps to HEAD; apply confident updates; report the rest |
112
+ | `apply-review <report>` | Write checked corrections back to the tour file |
113
+ | `undo [tour]` | Restore from the `.tour.backup` files migrate creates |
114
+
115
+ **Flags worth knowing** (`migrate`): `--threshold X` — the auto-apply confidence bar (note: confidence takes 5 exact values, not a smooth dial; see `docs/adr/0016-*.md`); `--clean-reviews` — drop stale review reports from earlier target commits; `--context-lines N` — code context in reports (default 7, config `review.context_lines`). (`apply-review`): `--auto-approve-above X` — also apply *unchecked* steps at or above that confidence; steps marked for deletion are **never** auto-approved.
116
+
117
+ ## MCP server
118
+
119
+ For agents on MCP-capable surfaces, the same operations are exposed as structured tools:
120
+
121
+ ```bash
122
+ pip install "codetour-cli[mcp]"
123
+ codetour-mcp # stdio MCP server
124
+ ```
125
+
126
+ Five tools — `tour_status`, `lint_tours`, `migrate_tour` (defaults to dry-run; a model-facing tool must not mutate by default), `read_review_report`, `apply_review` — each a thin wrapper calling the exact functions the CLI verbs call, with identical semantics (including: deletion-marked steps are never auto-approved). Register it e.g. in `.mcp.json`:
127
+
128
+ ```json
129
+ { "mcpServers": { "codetour": { "command": "codetour-mcp" } } }
130
+ ```
131
+
132
+ ## Design record
133
+
134
+ This tool is developed with the [ADRs4AI](https://github.com/jlumbroso) methodology — every architectural decision, including the ones that were later reversed, lives in `docs/adr/` as a first-class deliberation record: the byte-preserving editing contract, the confidence quantization study, the review-workflow design, and the retirement of ideas that eight months of shipped reality outvoted.
135
+
136
+ ## License
137
+
138
+ [MIT](LICENSE) — like CodeTour itself.
@@ -0,0 +1,99 @@
1
+ # CodeTour CLI
2
+
3
+ [![Tests](https://github.com/jlumbroso/codetour-cli/actions/workflows/test.yml/badge.svg)](https://github.com/jlumbroso/codetour-cli/actions/workflows/test.yml)
4
+ [![Coverage ≥60%](https://img.shields.io/badge/coverage-%E2%89%A560%25-yellowgreen)](https://github.com/jlumbroso/codetour-cli/actions/workflows/test.yml)
5
+ [![PyPI](https://img.shields.io/pypi/v/codetour-cli)](https://pypi.org/project/codetour-cli/)
6
+ [![Python versions](https://img.shields.io/pypi/pyversions/codetour-cli)](https://pypi.org/project/codetour-cli/)
7
+ [![Downloads](https://static.pepy.tech/badge/codetour-cli)](https://pepy.tech/projects/codetour-cli)
8
+ [![License](https://img.shields.io/github/license/jlumbroso/codetour-cli)](https://github.com/jlumbroso/codetour-cli/blob/main/LICENSE)
9
+
10
+ **Algorithmic maintenance of [CodeTour](https://github.com/microsoft/codetour) files: keep `.tour` walkthroughs accurate as the code they narrate evolves.**
11
+
12
+ > **Unofficial companion tool.** `codetour-cli` is an independent,
13
+ > unofficial companion to [CodeTour](https://github.com/microsoft/codetour),
14
+ > the VS Code extension by Jonathan Carter (Microsoft). It is not
15
+ > affiliated with, endorsed by, or maintained by Microsoft or the CodeTour
16
+ > project. It exists to *serve* CodeTour and its users — keeping `.tour`
17
+ > files accurate as the code they walk through evolves.
18
+
19
+ ---
20
+
21
+ ## The problem
22
+
23
+ A CodeTour step pinned to `src/auth.py:42` is correct the day it's written. Twenty commits later, line 42 is something else entirely — and every tour in the repository is silently lying. Tours are the best onboarding artifact a codebase can have, *if* someone keeps them true. Nobody keeps them true by hand.
24
+
25
+ `codetour-cli` tracks each step across the commits between a tour's pinned `ref` and `HEAD` (git hunk mapping plus content heuristics), applies high-confidence line updates automatically with **byte-preserving surgical edits** (your diff shows the step change, not a rewritten file — via [`json-source-edit`](https://pypi.org/project/json-source-edit/)), and routes everything it isn't sure about to a review report designed to be worked by humans *and* AI agents alike.
26
+
27
+ ## Install
28
+
29
+ ```bash
30
+ pip install codetour-cli # Python ≥ 3.10
31
+ ```
32
+
33
+ ## Quickstart
34
+
35
+ ```bash
36
+ codetour-cli status # which tours lag HEAD?
37
+ codetour-cli migrate --dry-run # preview: what would move, what needs review
38
+ codetour-cli migrate # apply confident updates; write review report
39
+ codetour-cli lint # ground-truth check tours against the workspace
40
+ ```
41
+
42
+ ## The maintenance loop
43
+
44
+ ```
45
+ author tour ──► lint ──► commit, pin `ref`
46
+ ▲ │
47
+ │ ...code evolves...
48
+ │ │
49
+ │ status (tour lags HEAD?)
50
+ │ │
51
+ apply-review ◄── review report ◄── migrate [--dry-run]
52
+ (checked steps (auto-applies confident updates;
53
+ written back) low-confidence → the report)
54
+ ```
55
+
56
+ Steps the migration can't confidently place land in `MIGRATION-REVIEW-{tour}-{commit}.md`: each entry carries the step's original description (its *intent*), old → new location, why confidence dropped, and the actual code now at the proposed location. Check the boxes you approve — or correct the locations inline — then:
57
+
58
+ ```bash
59
+ codetour-cli apply-review MIGRATION-REVIEW-mytour-abc12345.md
60
+ ```
61
+
62
+ **The report is deliberately dual-audience**: an AI agent can read it, judge each proposed location against the step's stated intent, mark the checkboxes, and apply — the same workflow, no human bottleneck for the easy calls. (A companion [Claude skill](https://github.com/jlumbroso/codetour-skill/releases) teaches agents both tour authoring and this maintenance loop.)
63
+
64
+ ## Commands
65
+
66
+ | Command | Purpose |
67
+ |---|---|
68
+ | `init` | Set up `.codetour-cli.yml` configuration |
69
+ | `status` | Health of every tour: current vs. lagging HEAD |
70
+ | `check [tour]` | Validate tour structure without migrating |
71
+ | `lint [paths]` | Step-level checks against the workspace: missing files, out-of-range lines, non-matching or ambiguous patterns, broken `nextTour` links (`--format json`, `--strict`) |
72
+ | `migrate [tour]` | Track steps to HEAD; apply confident updates; report the rest |
73
+ | `apply-review <report>` | Write checked corrections back to the tour file |
74
+ | `undo [tour]` | Restore from the `.tour.backup` files migrate creates |
75
+
76
+ **Flags worth knowing** (`migrate`): `--threshold X` — the auto-apply confidence bar (note: confidence takes 5 exact values, not a smooth dial; see `docs/adr/0016-*.md`); `--clean-reviews` — drop stale review reports from earlier target commits; `--context-lines N` — code context in reports (default 7, config `review.context_lines`). (`apply-review`): `--auto-approve-above X` — also apply *unchecked* steps at or above that confidence; steps marked for deletion are **never** auto-approved.
77
+
78
+ ## MCP server
79
+
80
+ For agents on MCP-capable surfaces, the same operations are exposed as structured tools:
81
+
82
+ ```bash
83
+ pip install "codetour-cli[mcp]"
84
+ codetour-mcp # stdio MCP server
85
+ ```
86
+
87
+ Five tools — `tour_status`, `lint_tours`, `migrate_tour` (defaults to dry-run; a model-facing tool must not mutate by default), `read_review_report`, `apply_review` — each a thin wrapper calling the exact functions the CLI verbs call, with identical semantics (including: deletion-marked steps are never auto-approved). Register it e.g. in `.mcp.json`:
88
+
89
+ ```json
90
+ { "mcpServers": { "codetour": { "command": "codetour-mcp" } } }
91
+ ```
92
+
93
+ ## Design record
94
+
95
+ This tool is developed with the [ADRs4AI](https://github.com/jlumbroso) methodology — every architectural decision, including the ones that were later reversed, lives in `docs/adr/` as a first-class deliberation record: the byte-preserving editing contract, the confidence quantization study, the review-workflow design, and the retirement of ideas that eight months of shipped reality outvoted.
96
+
97
+ ## License
98
+
99
+ [MIT](LICENSE) — like CodeTour itself.
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "codetour-cli"
7
- version = "0.2.0"
7
+ version = "0.3.0"
8
8
  description = "Algorithmic maintenance of CodeTour files — an unofficial companion CLI for the CodeTour VS Code extension"
9
9
  authors = [{name = "Jérémie Lumbroso"}]
10
10
  readme = "README.md"
@@ -41,6 +41,11 @@ dependencies = [
41
41
  ]
42
42
 
43
43
  [project.optional-dependencies]
44
+ # MCP server (ADR-0017): thin wrapper over the same machinery the CLI
45
+ # verbs call, exposed as `codetour-mcp` (stdio).
46
+ mcp = [
47
+ "mcp>=1.2",
48
+ ]
44
49
  dev = [
45
50
  "pytest",
46
51
  "pytest-cov",
@@ -51,6 +56,10 @@ dev = [
51
56
 
52
57
  [project.scripts]
53
58
  codetour-cli = "codetour_cli.cli:main"
59
+ # Installed unconditionally; the mcp SDK lives in the [mcp] extra, and the
60
+ # launcher answers a bare install with an install hint, not a traceback
61
+ # (the json-source-edit playground-launcher pattern).
62
+ codetour-mcp = "codetour_cli.mcp_server:main"
54
63
 
55
64
  [project.urls]
56
65
  Homepage = "https://github.com/jlumbroso/codetour-cli"
@@ -0,0 +1,279 @@
1
+ """MCP server for codetour-cli — structured tour operations for AI agents.
2
+
3
+ ADR-0017 (Accepted 2026-08-30): a THIN WRAPPER over the exact machinery the
4
+ CLI verbs call — `discovery.discover_repository`, `lint.lint_collection`,
5
+ `cli.migrate_command`, `review_parser.parse_review_report`,
6
+ `cli.apply_review_command`. Any behavioral drift between a CLI verb and its
7
+ MCP tool is a bug by definition; nothing here reimplements an operation.
8
+
9
+ Entry-point seam (same pattern as json-source-edit's playground launcher):
10
+ setuptools installs console scripts unconditionally, so a bare
11
+ `pip install codetour-cli` puts `codetour-mcp` on PATH while the `mcp` SDK
12
+ lives in the `[mcp]` extra. `main()` is deliberately import-light — nothing
13
+ above it may import outside the standard library and this package — so the
14
+ first thing a stranger types answers with an install hint, not a traceback.
15
+
16
+ Two deliberate, documented parameter-default choices (not behavior drift —
17
+ per-operation behavior is identical to the CLI at equal flag values):
18
+ - `migrate_tour` defaults to `dry_run=True`: a model-facing tool must not
19
+ mutate files by default; pass dry_run=False for the CLI's default-live
20
+ behavior.
21
+ - `apply_review` exposes the CLI's `--yes` as `approve_warned_descriptions`
22
+ and feeds declined prompts a "n" stream, because an MCP server has no
23
+ interactive stdin — skip-on-warning is exactly what pressing enter at the
24
+ CLI's [y/N] prompt does.
25
+ """
26
+
27
+ from __future__ import annotations
28
+
29
+ import io
30
+ import sys
31
+ from contextlib import redirect_stdout
32
+ from dataclasses import asdict
33
+ from pathlib import Path
34
+ from types import SimpleNamespace
35
+ from typing import Any, Optional
36
+
37
+ _INSTALL_HINT = (
38
+ "codetour-mcp is the optional MCP server, and its dependency is not "
39
+ "installed (missing: {missing}).\n"
40
+ "Install it with:\n\n"
41
+ ' pip install "codetour-cli[mcp]"\n'
42
+ )
43
+
44
+
45
+ # ── tool implementations (plain functions; registered onto FastMCP in main) ──
46
+ # Kept SDK-free so tests can exercise them without the [mcp] extra installed.
47
+
48
+
49
+ def tour_status_impl() -> dict:
50
+ """Structured health of every tour in the repository."""
51
+ from codetour_cli.discovery import discover_repository, is_tour_up_to_date
52
+
53
+ info = discover_repository()
54
+ tours = []
55
+ for t in info.tours:
56
+ tours.append({
57
+ "file": str(t.path),
58
+ "title": getattr(t.tour, "title", None),
59
+ "steps": len(getattr(t.tour, "steps", []) or []),
60
+ "source_commit": t.source_commit,
61
+ "source_confidence": t.source_confidence,
62
+ "source_method": t.source_method,
63
+ "commits_behind": t.commits_behind,
64
+ "up_to_date": is_tour_up_to_date(t),
65
+ })
66
+ return {
67
+ "repo_path": str(info.repo_path),
68
+ "tours_dir": str(info.tours_dir),
69
+ "tours": tours,
70
+ }
71
+
72
+
73
+ def lint_tours_impl(paths: Optional[list[str]] = None, strict: bool = False) -> dict:
74
+ """Ground-truth lint findings, structured (same checks as `codetour-cli lint`)."""
75
+ from codetour_cli.discovery import find_repo_root, find_tours_directory, scan_tours
76
+ from codetour_cli.lint import has_errors, lint_collection
77
+
78
+ repo_root = find_repo_root()
79
+ if paths:
80
+ tour_paths = []
81
+ for raw in paths:
82
+ p = Path(raw)
83
+ if not p.is_absolute():
84
+ p = repo_root / p
85
+ if p.is_dir():
86
+ tour_paths.extend(scan_tours(p))
87
+ else:
88
+ tour_paths.append(p)
89
+ else:
90
+ tours_dir = find_tours_directory(repo_root)
91
+ tour_paths = scan_tours(tours_dir) if tours_dir else []
92
+
93
+ findings = lint_collection(tour_paths, repo_root)
94
+ errors = sum(1 for f in findings if f.severity == "error")
95
+ return {
96
+ "tours_checked": len(tour_paths),
97
+ "errors": errors,
98
+ "warnings": len(findings) - errors,
99
+ "clean": not has_errors(findings, strict=strict),
100
+ "findings": [asdict(f) if hasattr(f, "__dataclass_fields__") else vars(f)
101
+ for f in findings],
102
+ }
103
+
104
+
105
+ def _run_command(command_fn, args: SimpleNamespace, stdin_text: str = "") -> dict:
106
+ """Run a CLI command function verbatim, capturing stdout; feed a scripted
107
+ stdin so interactive prompts resolve to their conservative default."""
108
+ from codetour_cli.config import load_config
109
+
110
+ config = load_config()
111
+ buf = io.StringIO()
112
+ old_stdin = sys.stdin
113
+ sys.stdin = io.StringIO(stdin_text)
114
+ try:
115
+ with redirect_stdout(buf):
116
+ exit_code = command_fn(args, config)
117
+ finally:
118
+ sys.stdin = old_stdin
119
+ return {"exit_code": exit_code, "output": buf.getvalue()}
120
+
121
+
122
+ def migrate_tour_impl(
123
+ tour_file: Optional[str] = None,
124
+ dry_run: bool = True,
125
+ threshold: Optional[float] = None,
126
+ to_commit: str = "HEAD",
127
+ from_commit: Optional[str] = None,
128
+ clean_reviews: bool = False,
129
+ context_lines: Optional[int] = None,
130
+ ) -> dict:
131
+ """Migrate a tour (or all out-of-date tours) via the CLI's own command path."""
132
+ from codetour_cli.cli import migrate_command
133
+
134
+ args = SimpleNamespace(
135
+ tour_file=tour_file,
136
+ from_commit=from_commit,
137
+ to_commit=to_commit,
138
+ threshold=threshold,
139
+ no_backup=False,
140
+ interactive=False,
141
+ dry_run=dry_run,
142
+ no_guidance=False,
143
+ no_lint=False,
144
+ clean_reviews=clean_reviews,
145
+ context_lines=context_lines,
146
+ )
147
+ result = _run_command(migrate_command, args)
148
+ result["reports"] = [
149
+ line.split("Review report", 1)[1].split(":", 1)[1].strip()
150
+ for line in result["output"].splitlines()
151
+ if "Review report" in line and ":" in line.split("Review report", 1)[1]
152
+ ]
153
+ result["dry_run"] = dry_run
154
+ return result
155
+
156
+
157
+ def read_review_report_impl(report_file: str) -> dict:
158
+ """Parse a MIGRATION-REVIEW-*.md report into structured step reviews."""
159
+ from codetour_cli.review_parser import parse_review_report
160
+
161
+ review = parse_review_report(Path(report_file))
162
+ return {
163
+ "tour_name": review.tour_name,
164
+ "source_commit": review.source_commit,
165
+ "target_commit": review.target_commit,
166
+ "mode": review.mode,
167
+ "step_reviews": [asdict(r) for r in review.step_reviews],
168
+ }
169
+
170
+
171
+ def apply_review_impl(
172
+ report_file: str,
173
+ auto_approve_above: Optional[float] = None,
174
+ approve_warned_descriptions: bool = False,
175
+ ) -> dict:
176
+ """Apply a review report via the CLI's own command path."""
177
+ from codetour_cli.cli import apply_review_command
178
+
179
+ args = SimpleNamespace(
180
+ report_file=report_file,
181
+ auto_approve=approve_warned_descriptions,
182
+ auto_approve_above=auto_approve_above,
183
+ )
184
+ # "n\n" stream: any description-warning prompt resolves to skip — the
185
+ # CLI's own conservative default at its [y/N] prompt.
186
+ return _run_command(apply_review_command, args, stdin_text="n\n" * 1000)
187
+
188
+
189
+ # ── server assembly ──────────────────────────────────────────────────────────
190
+
191
+
192
+ def build_server() -> Any:
193
+ """Construct the MCP server (requires the [mcp] extra).
194
+
195
+ SDK v2 renamed FastMCP → MCPServer (same tool/run surface, verified
196
+ empirically 2026-08-30 — the stdio smoke test caught the v1 prior
197
+ before it shipped); import v2 first, fall back to v1 for pinned
198
+ environments."""
199
+ try:
200
+ from mcp.server.mcpserver import MCPServer as _Server # mcp >= 2
201
+ except ImportError:
202
+ from mcp.server.fastmcp import FastMCP as _Server # mcp 1.x
203
+
204
+ server = _Server("codetour-cli")
205
+
206
+ server.tool(
207
+ name="tour_status",
208
+ description=(
209
+ "Structured health of every CodeTour in the repository: per tour, "
210
+ "its pinned source commit, detection confidence/method, how many "
211
+ "commits behind HEAD it is, and whether it is up to date."
212
+ ),
213
+ )(tour_status_impl)
214
+
215
+ server.tool(
216
+ name="lint_tours",
217
+ description=(
218
+ "Lint tours against the actual workspace (missing files, "
219
+ "out-of-range lines, non-matching or ambiguous patterns, broken "
220
+ "nextTour links, duplicate isPrimary). Structured findings; "
221
+ "same checks as `codetour-cli lint`."
222
+ ),
223
+ )(lint_tours_impl)
224
+
225
+ server.tool(
226
+ name="migrate_tour",
227
+ description=(
228
+ "Migrate a tour (or all out-of-date tours) toward HEAD via the "
229
+ "CLI's own pipeline. DEFAULTS TO dry_run=true — pass "
230
+ "dry_run=false to actually write. Confident step updates are "
231
+ "applied (byte-preserving edits); low-confidence steps land in a "
232
+ "review report (paths returned in `reports`). Note: `threshold` "
233
+ "confidence only takes 5 exact values — values in (0.5, 0.95) "
234
+ "behave like the default; see that repo's ADR-0016 before tuning."
235
+ ),
236
+ )(migrate_tour_impl)
237
+
238
+ server.tool(
239
+ name="read_review_report",
240
+ description=(
241
+ "Parse a MIGRATION-REVIEW-*.md report into structured step "
242
+ "reviews: per step, its checkbox state, tentative file/line, "
243
+ "confidence, and any edited description. Read this, judge each "
244
+ "proposed location against the step's original intent, then use "
245
+ "apply_review."
246
+ ),
247
+ )(read_review_report_impl)
248
+
249
+ server.tool(
250
+ name="apply_review",
251
+ description=(
252
+ "Apply corrections from a review report to the tour file "
253
+ "(byte-preserving edits, backup created). Applies checkbox-"
254
+ "checked steps; `auto_approve_above` (0.0-1.0) additionally "
255
+ "applies UNCHECKED steps at/above that confidence — but steps "
256
+ "marked for deletion are NEVER auto-approved (destructive stays "
257
+ "behind an explicit checkbox). Confidence takes 5 exact values "
258
+ "(ADR-0016). `approve_warned_descriptions` maps to the CLI's "
259
+ "--yes for description-edit warnings; when false, warned edits "
260
+ "are skipped (the CLI prompt's own default)."
261
+ ),
262
+ )(apply_review_impl)
263
+
264
+ return server
265
+
266
+
267
+ def main() -> None:
268
+ try:
269
+ import mcp # noqa: F401
270
+ except ImportError as exc:
271
+ missing = getattr(exc, "name", None) or str(exc)
272
+ print(_INSTALL_HINT.format(missing=missing), file=sys.stderr)
273
+ raise SystemExit(1)
274
+
275
+ build_server().run()
276
+
277
+
278
+ if __name__ == "__main__":
279
+ main()
@@ -0,0 +1,138 @@
1
+ Metadata-Version: 2.4
2
+ Name: codetour-cli
3
+ Version: 0.3.0
4
+ Summary: Algorithmic maintenance of CodeTour files — an unofficial companion CLI for the CodeTour VS Code extension
5
+ Author: Jérémie Lumbroso
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/jlumbroso/codetour-cli
8
+ Project-URL: Repository, https://github.com/jlumbroso/codetour-cli
9
+ Keywords: codetour,code-tour,vscode,git,cli
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Topic :: Software Development :: Version Control :: Git
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Requires-Python: >=3.10
19
+ Description-Content-Type: text/markdown
20
+ License-File: LICENSE
21
+ Requires-Dist: gitpython
22
+ Requires-Dist: pydantic>=2.0
23
+ Requires-Dist: structlog
24
+ Requires-Dist: rich
25
+ Requires-Dist: python-dotenv
26
+ Requires-Dist: pyyaml
27
+ Requires-Dist: click>=8.0
28
+ Requires-Dist: jinja2>=3.0
29
+ Requires-Dist: json-source-edit<2,>=1.0
30
+ Provides-Extra: mcp
31
+ Requires-Dist: mcp>=1.2; extra == "mcp"
32
+ Provides-Extra: dev
33
+ Requires-Dist: pytest; extra == "dev"
34
+ Requires-Dist: pytest-cov; extra == "dev"
35
+ Requires-Dist: black; extra == "dev"
36
+ Requires-Dist: ruff; extra == "dev"
37
+ Requires-Dist: mypy; extra == "dev"
38
+ Dynamic: license-file
39
+
40
+ # CodeTour CLI
41
+
42
+ [![Tests](https://github.com/jlumbroso/codetour-cli/actions/workflows/test.yml/badge.svg)](https://github.com/jlumbroso/codetour-cli/actions/workflows/test.yml)
43
+ [![Coverage ≥60%](https://img.shields.io/badge/coverage-%E2%89%A560%25-yellowgreen)](https://github.com/jlumbroso/codetour-cli/actions/workflows/test.yml)
44
+ [![PyPI](https://img.shields.io/pypi/v/codetour-cli)](https://pypi.org/project/codetour-cli/)
45
+ [![Python versions](https://img.shields.io/pypi/pyversions/codetour-cli)](https://pypi.org/project/codetour-cli/)
46
+ [![Downloads](https://static.pepy.tech/badge/codetour-cli)](https://pepy.tech/projects/codetour-cli)
47
+ [![License](https://img.shields.io/github/license/jlumbroso/codetour-cli)](https://github.com/jlumbroso/codetour-cli/blob/main/LICENSE)
48
+
49
+ **Algorithmic maintenance of [CodeTour](https://github.com/microsoft/codetour) files: keep `.tour` walkthroughs accurate as the code they narrate evolves.**
50
+
51
+ > **Unofficial companion tool.** `codetour-cli` is an independent,
52
+ > unofficial companion to [CodeTour](https://github.com/microsoft/codetour),
53
+ > the VS Code extension by Jonathan Carter (Microsoft). It is not
54
+ > affiliated with, endorsed by, or maintained by Microsoft or the CodeTour
55
+ > project. It exists to *serve* CodeTour and its users — keeping `.tour`
56
+ > files accurate as the code they walk through evolves.
57
+
58
+ ---
59
+
60
+ ## The problem
61
+
62
+ A CodeTour step pinned to `src/auth.py:42` is correct the day it's written. Twenty commits later, line 42 is something else entirely — and every tour in the repository is silently lying. Tours are the best onboarding artifact a codebase can have, *if* someone keeps them true. Nobody keeps them true by hand.
63
+
64
+ `codetour-cli` tracks each step across the commits between a tour's pinned `ref` and `HEAD` (git hunk mapping plus content heuristics), applies high-confidence line updates automatically with **byte-preserving surgical edits** (your diff shows the step change, not a rewritten file — via [`json-source-edit`](https://pypi.org/project/json-source-edit/)), and routes everything it isn't sure about to a review report designed to be worked by humans *and* AI agents alike.
65
+
66
+ ## Install
67
+
68
+ ```bash
69
+ pip install codetour-cli # Python ≥ 3.10
70
+ ```
71
+
72
+ ## Quickstart
73
+
74
+ ```bash
75
+ codetour-cli status # which tours lag HEAD?
76
+ codetour-cli migrate --dry-run # preview: what would move, what needs review
77
+ codetour-cli migrate # apply confident updates; write review report
78
+ codetour-cli lint # ground-truth check tours against the workspace
79
+ ```
80
+
81
+ ## The maintenance loop
82
+
83
+ ```
84
+ author tour ──► lint ──► commit, pin `ref`
85
+ ▲ │
86
+ │ ...code evolves...
87
+ │ │
88
+ │ status (tour lags HEAD?)
89
+ │ │
90
+ apply-review ◄── review report ◄── migrate [--dry-run]
91
+ (checked steps (auto-applies confident updates;
92
+ written back) low-confidence → the report)
93
+ ```
94
+
95
+ Steps the migration can't confidently place land in `MIGRATION-REVIEW-{tour}-{commit}.md`: each entry carries the step's original description (its *intent*), old → new location, why confidence dropped, and the actual code now at the proposed location. Check the boxes you approve — or correct the locations inline — then:
96
+
97
+ ```bash
98
+ codetour-cli apply-review MIGRATION-REVIEW-mytour-abc12345.md
99
+ ```
100
+
101
+ **The report is deliberately dual-audience**: an AI agent can read it, judge each proposed location against the step's stated intent, mark the checkboxes, and apply — the same workflow, no human bottleneck for the easy calls. (A companion [Claude skill](https://github.com/jlumbroso/codetour-skill/releases) teaches agents both tour authoring and this maintenance loop.)
102
+
103
+ ## Commands
104
+
105
+ | Command | Purpose |
106
+ |---|---|
107
+ | `init` | Set up `.codetour-cli.yml` configuration |
108
+ | `status` | Health of every tour: current vs. lagging HEAD |
109
+ | `check [tour]` | Validate tour structure without migrating |
110
+ | `lint [paths]` | Step-level checks against the workspace: missing files, out-of-range lines, non-matching or ambiguous patterns, broken `nextTour` links (`--format json`, `--strict`) |
111
+ | `migrate [tour]` | Track steps to HEAD; apply confident updates; report the rest |
112
+ | `apply-review <report>` | Write checked corrections back to the tour file |
113
+ | `undo [tour]` | Restore from the `.tour.backup` files migrate creates |
114
+
115
+ **Flags worth knowing** (`migrate`): `--threshold X` — the auto-apply confidence bar (note: confidence takes 5 exact values, not a smooth dial; see `docs/adr/0016-*.md`); `--clean-reviews` — drop stale review reports from earlier target commits; `--context-lines N` — code context in reports (default 7, config `review.context_lines`). (`apply-review`): `--auto-approve-above X` — also apply *unchecked* steps at or above that confidence; steps marked for deletion are **never** auto-approved.
116
+
117
+ ## MCP server
118
+
119
+ For agents on MCP-capable surfaces, the same operations are exposed as structured tools:
120
+
121
+ ```bash
122
+ pip install "codetour-cli[mcp]"
123
+ codetour-mcp # stdio MCP server
124
+ ```
125
+
126
+ Five tools — `tour_status`, `lint_tours`, `migrate_tour` (defaults to dry-run; a model-facing tool must not mutate by default), `read_review_report`, `apply_review` — each a thin wrapper calling the exact functions the CLI verbs call, with identical semantics (including: deletion-marked steps are never auto-approved). Register it e.g. in `.mcp.json`:
127
+
128
+ ```json
129
+ { "mcpServers": { "codetour": { "command": "codetour-mcp" } } }
130
+ ```
131
+
132
+ ## Design record
133
+
134
+ This tool is developed with the [ADRs4AI](https://github.com/jlumbroso) methodology — every architectural decision, including the ones that were later reversed, lives in `docs/adr/` as a first-class deliberation record: the byte-preserving editing contract, the confidence quantization study, the review-workflow design, and the retirement of ideas that eight months of shipped reality outvoted.
135
+
136
+ ## License
137
+
138
+ [MIT](LICENSE) — like CodeTour itself.
@@ -8,6 +8,7 @@ src/codetour_cli/description_validator.py
8
8
  src/codetour_cli/detection.py
9
9
  src/codetour_cli/discovery.py
10
10
  src/codetour_cli/lint.py
11
+ src/codetour_cli/mcp_server.py
11
12
  src/codetour_cli/review_parser.py
12
13
  src/codetour_cli/schema_models.py
13
14
  src/codetour_cli.egg-info/PKG-INFO
@@ -37,6 +38,7 @@ tests/test_integration.py
37
38
  tests/test_lint.py
38
39
  tests/test_lint_cli.py
39
40
  tests/test_logging_setup.py
41
+ tests/test_mcp_server.py
40
42
  tests/test_pattern_validation.py
41
43
  tests/test_post_migration_lint.py
42
44
  tests/test_review_roundtrip.py
@@ -1,2 +1,3 @@
1
1
  [console_scripts]
2
2
  codetour-cli = codetour_cli.cli:main
3
+ codetour-mcp = codetour_cli.mcp_server:main
@@ -14,3 +14,6 @@ pytest-cov
14
14
  black
15
15
  ruff
16
16
  mypy
17
+
18
+ [mcp]
19
+ mcp>=1.2
@@ -0,0 +1,253 @@
1
+ """Tests for the MCP server (ADR-0017, v0.3.0).
2
+
3
+ The tool implementations are deliberately SDK-free plain functions, so
4
+ everything except the stdio smoke test runs without the [mcp] extra --
5
+ and everything runs against REAL machinery (real git repos, real tour
6
+ files, the real command functions), never mocks: the wrapper-not-fork
7
+ constraint means the thing worth testing is that the wrappers drive the
8
+ same behavior the CLI does.
9
+ """
10
+
11
+ import json
12
+ import subprocess
13
+ import sys
14
+
15
+ import pytest
16
+
17
+ from codetour_cli.mcp_server import (
18
+ apply_review_impl,
19
+ lint_tours_impl,
20
+ migrate_tour_impl,
21
+ read_review_report_impl,
22
+ tour_status_impl,
23
+ )
24
+
25
+
26
+ def _init_git_repo(path):
27
+ subprocess.run(["git", "init", "-q"], cwd=path, check=True)
28
+ subprocess.run(["git", "config", "user.email", "test@example.com"], cwd=path, check=True)
29
+ subprocess.run(["git", "config", "user.name", "Test"], cwd=path, check=True)
30
+
31
+
32
+ def _review_report(tour_name, entries):
33
+ """Same shape the apply-review integration tests use, plus a `checked`
34
+ knob so auto-approve-above has genuinely unchecked steps to promote."""
35
+ steps_md = []
36
+ for e in entries:
37
+ checked = "[x] Reviewed" if e.get("checked", True) else "[ ] Not reviewed"
38
+ block = f"""### Step {e['step_number']}: placeholder → placeholder ({e.get('confidence', 80)}% confidence)
39
+
40
+ ---
41
+
42
+ #### Review Status
43
+
44
+ - **Status**: {checked}
45
+ - **Tentative file**: `{e['tentative_file']}`
46
+ - **Tentative line**: `{e['tentative_line']}`
47
+ - **Confidence**: {e.get('confidence', 80)}%
48
+ - **Modified Description**: [ ]
49
+
50
+ ---
51
+ """
52
+ steps_md.append(block)
53
+
54
+ return f"""---
55
+ schema_version: '1.0'
56
+ ---
57
+
58
+ # Migration Review Report
59
+
60
+ - **Generated**: 2026-08-30 00:00:00
61
+ - **Source commit**: abc12345
62
+ - **Target commit**: def67890
63
+ - **Mode**: live
64
+ - **Tour**: {tour_name}
65
+ - **Total steps**: {len(entries)}
66
+
67
+ ## Steps Needing Review
68
+
69
+ """ + "\n".join(steps_md)
70
+
71
+
72
+ TOUR_JSON = """{
73
+ "$schema": "https://aka.ms/codetour-schema",
74
+ "title": "Rust Basics",
75
+ "steps": [
76
+ {
77
+ "file": "src/main.rs",
78
+ "line": 3,
79
+ "description": "Entry point"
80
+ },
81
+ {
82
+ "file": "src/lib.rs",
83
+ "line": 2,
84
+ "description": "Library definition"
85
+ }
86
+ ]
87
+ }
88
+ """
89
+
90
+
91
+ @pytest.fixture
92
+ def repo_with_tour(tmp_path, monkeypatch):
93
+ _init_git_repo(tmp_path)
94
+ (tmp_path / "src").mkdir()
95
+ (tmp_path / "src" / "main.rs").write_text("fn a() {}\nfn b() {}\nfn main() {}\n", encoding="utf-8")
96
+ (tmp_path / "src" / "lib.rs").write_text("mod a;\npub fn lib() {}\n", encoding="utf-8")
97
+ tours_dir = tmp_path / ".tours"
98
+ tours_dir.mkdir()
99
+ tour_path = tours_dir / "rust-basics.tour"
100
+ tour_path.write_text(TOUR_JSON, encoding="utf-8")
101
+ subprocess.run(["git", "add", "."], cwd=tmp_path, check=True)
102
+ subprocess.run(["git", "commit", "-q", "-m", "initial"], cwd=tmp_path, check=True)
103
+ monkeypatch.chdir(tmp_path)
104
+ return tmp_path, tour_path
105
+
106
+
107
+ class TestTourStatus:
108
+ def test_structured_status_for_real_repo(self, repo_with_tour):
109
+ tmp_path, _ = repo_with_tour
110
+ result = tour_status_impl()
111
+ assert result["repo_path"] == str(tmp_path)
112
+ assert len(result["tours"]) == 1
113
+ t = result["tours"][0]
114
+ assert t["title"] == "Rust Basics"
115
+ assert t["steps"] == 2
116
+ assert isinstance(t["commits_behind"], int)
117
+ assert isinstance(t["up_to_date"], bool)
118
+ assert t["source_commit"]
119
+
120
+
121
+ class TestLintTours:
122
+ def test_reports_real_problems(self, repo_with_tour):
123
+ tmp_path, tour_path = repo_with_tour
124
+ # A real defect a user would ship: a step pointing at a missing file.
125
+ broken = TOUR_JSON.replace("src/lib.rs", "src/gone.rs")
126
+ tour_path.write_text(broken, encoding="utf-8")
127
+ result = lint_tours_impl()
128
+ assert result["tours_checked"] == 1
129
+ assert result["errors"] >= 1
130
+ assert result["clean"] is False
131
+ assert any("gone.rs" in str(f.get("message", "")) for f in result["findings"])
132
+
133
+ def test_clean_tour_is_clean(self, repo_with_tour):
134
+ result = lint_tours_impl()
135
+ assert result["errors"] == 0
136
+ assert result["clean"] is True
137
+
138
+
139
+ class TestReadReviewReport:
140
+ def test_parses_structured(self, repo_with_tour, tmp_path):
141
+ report = tmp_path / "MIGRATION-REVIEW-rust-basics-def67890.md"
142
+ report.write_text(
143
+ _review_report("rust-basics.tour", [
144
+ {"step_number": 1, "tentative_file": "src/main.rs",
145
+ "tentative_line": "2", "confidence": 50, "checked": False},
146
+ ]),
147
+ encoding="utf-8",
148
+ )
149
+ result = read_review_report_impl(str(report))
150
+ assert result["tour_name"] == "rust-basics.tour"
151
+ assert result["target_commit"] == "def67890"
152
+ (r,) = result["step_reviews"]
153
+ assert r["step_number"] == 1
154
+ assert r["status_checked"] is False
155
+ assert r["confidence"] == 50
156
+
157
+
158
+ class TestApplyReview:
159
+ def test_applies_checked_step_to_real_tour(self, repo_with_tour, tmp_path):
160
+ _, tour_path = repo_with_tour
161
+ report = tmp_path / "MIGRATION-REVIEW-rust-basics-def67890.md"
162
+ report.write_text(
163
+ _review_report("rust-basics.tour", [
164
+ {"step_number": 2, "tentative_file": "src/lib.rs",
165
+ "tentative_line": "1", "checked": True},
166
+ ]),
167
+ encoding="utf-8",
168
+ )
169
+ result = apply_review_impl(str(report))
170
+ assert result["exit_code"] == 0
171
+ data = json.loads(tour_path.read_text(encoding="utf-8"))
172
+ assert data["steps"][1]["line"] == 1
173
+ assert data["steps"][0]["line"] == 3 # untouched neighbor
174
+
175
+ def test_auto_approve_above_promotes_unchecked_high_confidence(self, repo_with_tour, tmp_path):
176
+ _, tour_path = repo_with_tour
177
+ report = tmp_path / "MIGRATION-REVIEW-rust-basics-def67890.md"
178
+ report.write_text(
179
+ _review_report("rust-basics.tour", [
180
+ {"step_number": 1, "tentative_file": "src/main.rs",
181
+ "tentative_line": "1", "confidence": 90, "checked": False},
182
+ {"step_number": 2, "tentative_file": "src/lib.rs",
183
+ "tentative_line": "1", "confidence": 50, "checked": False},
184
+ ]),
185
+ encoding="utf-8",
186
+ )
187
+ result = apply_review_impl(str(report), auto_approve_above=0.8)
188
+ assert result["exit_code"] == 0
189
+ data = json.loads(tour_path.read_text(encoding="utf-8"))
190
+ assert data["steps"][0]["line"] == 1 # 90% ≥ 80%: promoted, applied
191
+ assert data["steps"][1]["line"] == 2 # 50% < 80%: untouched
192
+
193
+ def test_without_flag_unchecked_steps_do_nothing(self, repo_with_tour, tmp_path):
194
+ _, tour_path = repo_with_tour
195
+ before = tour_path.read_bytes()
196
+ report = tmp_path / "MIGRATION-REVIEW-rust-basics-def67890.md"
197
+ report.write_text(
198
+ _review_report("rust-basics.tour", [
199
+ {"step_number": 1, "tentative_file": "src/main.rs",
200
+ "tentative_line": "1", "confidence": 90, "checked": False},
201
+ ]),
202
+ encoding="utf-8",
203
+ )
204
+ result = apply_review_impl(str(report))
205
+ assert result["exit_code"] == 0
206
+ assert tour_path.read_bytes() == before
207
+
208
+
209
+ class TestMigrateTour:
210
+ def test_dry_run_is_the_default_and_mutates_nothing(self, repo_with_tour):
211
+ _, tour_path = repo_with_tour
212
+ before = tour_path.read_bytes()
213
+ result = migrate_tour_impl()
214
+ assert result["dry_run"] is True
215
+ assert result["exit_code"] in (0, 1)
216
+ assert tour_path.read_bytes() == before
217
+ assert result["output"] # the CLI's own narration, captured
218
+
219
+
220
+ class TestStdioServer:
221
+ def test_smoke_initialize_and_tools_list(self):
222
+ # A real (minimal) MCP client over stdio: the v2 SDK enforces the
223
+ # session lifecycle, so batch-piping all messages races the
224
+ # initialize handshake and tools/list gets dropped — write, read
225
+ # the response, then proceed, like an actual client.
226
+ pytest.importorskip("mcp")
227
+ proc = subprocess.Popen(
228
+ [sys.executable, "-m", "codetour_cli.mcp_server"],
229
+ stdin=subprocess.PIPE, stdout=subprocess.PIPE,
230
+ stderr=subprocess.PIPE, text=True,
231
+ )
232
+ try:
233
+ def send(msg):
234
+ proc.stdin.write(json.dumps(msg) + "\n")
235
+ proc.stdin.flush()
236
+
237
+ send({"jsonrpc": "2.0", "id": 1, "method": "initialize",
238
+ "params": {"protocolVersion": "2025-06-18", "capabilities": {},
239
+ "clientInfo": {"name": "smoke", "version": "0"}}})
240
+ init_resp = json.loads(proc.stdout.readline())
241
+ assert init_resp["id"] == 1
242
+ assert init_resp["result"]["serverInfo"]["name"] == "codetour-cli"
243
+
244
+ send({"jsonrpc": "2.0", "method": "notifications/initialized"})
245
+ send({"jsonrpc": "2.0", "id": 2, "method": "tools/list"})
246
+ tools_resp = json.loads(proc.stdout.readline())
247
+ assert tools_resp["id"] == 2
248
+ names = {t["name"] for t in tools_resp["result"]["tools"]}
249
+ assert names == {"tour_status", "lint_tours", "migrate_tour",
250
+ "read_review_report", "apply_review"}
251
+ finally:
252
+ proc.kill()
253
+ proc.wait(timeout=10)
@@ -1,47 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: codetour-cli
3
- Version: 0.2.0
4
- Summary: Algorithmic maintenance of CodeTour files — an unofficial companion CLI for the CodeTour VS Code extension
5
- Author: Jérémie Lumbroso
6
- License: MIT
7
- Project-URL: Homepage, https://github.com/jlumbroso/codetour-cli
8
- Project-URL: Repository, https://github.com/jlumbroso/codetour-cli
9
- Keywords: codetour,code-tour,vscode,git,cli
10
- Classifier: Development Status :: 3 - Alpha
11
- Classifier: Intended Audience :: Developers
12
- Classifier: License :: OSI Approved :: MIT License
13
- Classifier: Topic :: Software Development :: Version Control :: Git
14
- Classifier: Programming Language :: Python :: 3.10
15
- Classifier: Programming Language :: Python :: 3.11
16
- Classifier: Programming Language :: Python :: 3.12
17
- Classifier: Programming Language :: Python :: 3.13
18
- Requires-Python: >=3.10
19
- Description-Content-Type: text/markdown
20
- License-File: LICENSE
21
- Requires-Dist: gitpython
22
- Requires-Dist: pydantic>=2.0
23
- Requires-Dist: structlog
24
- Requires-Dist: rich
25
- Requires-Dist: python-dotenv
26
- Requires-Dist: pyyaml
27
- Requires-Dist: click>=8.0
28
- Requires-Dist: jinja2>=3.0
29
- Requires-Dist: json-source-edit<2,>=1.0
30
- Provides-Extra: dev
31
- Requires-Dist: pytest; extra == "dev"
32
- Requires-Dist: pytest-cov; extra == "dev"
33
- Requires-Dist: black; extra == "dev"
34
- Requires-Dist: ruff; extra == "dev"
35
- Requires-Dist: mypy; extra == "dev"
36
- Dynamic: license-file
37
-
38
- # CodeTour CLI
39
-
40
- A CLI to reliably migrate CodeTours in the background, and prevent drift.
41
-
42
- > **Unofficial companion tool.** `codetour-cli` is an independent,
43
- > unofficial companion to [CodeTour](https://github.com/microsoft/codetour),
44
- > the VS Code extension by Jonathan Carter (Microsoft). It is not
45
- > affiliated with, endorsed by, or maintained by Microsoft or the CodeTour
46
- > project. It exists to *serve* CodeTour and its users — keeping `.tour`
47
- > files accurate as the code they walk through evolves.
@@ -1,10 +0,0 @@
1
- # CodeTour CLI
2
-
3
- A CLI to reliably migrate CodeTours in the background, and prevent drift.
4
-
5
- > **Unofficial companion tool.** `codetour-cli` is an independent,
6
- > unofficial companion to [CodeTour](https://github.com/microsoft/codetour),
7
- > the VS Code extension by Jonathan Carter (Microsoft). It is not
8
- > affiliated with, endorsed by, or maintained by Microsoft or the CodeTour
9
- > project. It exists to *serve* CodeTour and its users — keeping `.tour`
10
- > files accurate as the code they walk through evolves.
@@ -1,47 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: codetour-cli
3
- Version: 0.2.0
4
- Summary: Algorithmic maintenance of CodeTour files — an unofficial companion CLI for the CodeTour VS Code extension
5
- Author: Jérémie Lumbroso
6
- License: MIT
7
- Project-URL: Homepage, https://github.com/jlumbroso/codetour-cli
8
- Project-URL: Repository, https://github.com/jlumbroso/codetour-cli
9
- Keywords: codetour,code-tour,vscode,git,cli
10
- Classifier: Development Status :: 3 - Alpha
11
- Classifier: Intended Audience :: Developers
12
- Classifier: License :: OSI Approved :: MIT License
13
- Classifier: Topic :: Software Development :: Version Control :: Git
14
- Classifier: Programming Language :: Python :: 3.10
15
- Classifier: Programming Language :: Python :: 3.11
16
- Classifier: Programming Language :: Python :: 3.12
17
- Classifier: Programming Language :: Python :: 3.13
18
- Requires-Python: >=3.10
19
- Description-Content-Type: text/markdown
20
- License-File: LICENSE
21
- Requires-Dist: gitpython
22
- Requires-Dist: pydantic>=2.0
23
- Requires-Dist: structlog
24
- Requires-Dist: rich
25
- Requires-Dist: python-dotenv
26
- Requires-Dist: pyyaml
27
- Requires-Dist: click>=8.0
28
- Requires-Dist: jinja2>=3.0
29
- Requires-Dist: json-source-edit<2,>=1.0
30
- Provides-Extra: dev
31
- Requires-Dist: pytest; extra == "dev"
32
- Requires-Dist: pytest-cov; extra == "dev"
33
- Requires-Dist: black; extra == "dev"
34
- Requires-Dist: ruff; extra == "dev"
35
- Requires-Dist: mypy; extra == "dev"
36
- Dynamic: license-file
37
-
38
- # CodeTour CLI
39
-
40
- A CLI to reliably migrate CodeTours in the background, and prevent drift.
41
-
42
- > **Unofficial companion tool.** `codetour-cli` is an independent,
43
- > unofficial companion to [CodeTour](https://github.com/microsoft/codetour),
44
- > the VS Code extension by Jonathan Carter (Microsoft). It is not
45
- > affiliated with, endorsed by, or maintained by Microsoft or the CodeTour
46
- > project. It exists to *serve* CodeTour and its users — keeping `.tour`
47
- > files accurate as the code they walk through evolves.
File without changes
File without changes