nexus-dev-toolkit 3.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
nexus_cli.py ADDED
@@ -0,0 +1,292 @@
1
+ import json
2
+ import shutil
3
+ import subprocess
4
+ import sys
5
+ from pathlib import Path
6
+
7
+ import typer
8
+ from rich.console import Console
9
+ from rich.table import Table
10
+
11
+ app = typer.Typer(name="nexus", no_args_is_help=True, help="nexus-dev-toolkit — LLM-agnostic developer workflow toolkit")
12
+ skill_app = typer.Typer(name="skill", no_args_is_help=True, help="Manage skills in .claude/commands/")
13
+ rule_app = typer.Typer(name="rule", no_args_is_help=True, help="Manage rules in knowledge/rules/")
14
+ app.add_typer(skill_app, name="skill")
15
+ app.add_typer(rule_app, name="rule")
16
+
17
+ console = Console()
18
+
19
+ # ── Built-in skills shipped with the package ──────────────────────────────────
20
+
21
+ _SKILLS_SRC = Path(__file__).parent / "tools" / "epav" / "skills"
22
+
23
+ _BUILTIN_SKILLS = [
24
+ "scaffold.md",
25
+ "evaluate.md",
26
+ "plan.md",
27
+ "apply.md",
28
+ "validate.md",
29
+ "epav.md",
30
+ ]
31
+
32
+ # ── .claude/settings.json ────────────────────────────────────────────────────
33
+
34
+ _CLAUDE_SETTINGS = {
35
+ "hooks": {
36
+ "PostToolUse": [
37
+ {
38
+ "matcher": ".*",
39
+ "hooks": [
40
+ {
41
+ "type": "command",
42
+ "command": "graphify update . --force 2>/dev/null || true"
43
+ }
44
+ ]
45
+ }
46
+ ]
47
+ }
48
+ }
49
+
50
+ # ── knowledge/ scaffold ───────────────────────────────────────────────────────
51
+
52
+ _KNOWLEDGE_DIRS = [
53
+ "knowledge/rules",
54
+ "knowledge/patterns",
55
+ "knowledge/prompts/dev",
56
+ "knowledge/retros",
57
+ ]
58
+
59
+ # ── MCP config ────────────────────────────────────────────────────────────────
60
+
61
+ _MCP_BLOCK = {
62
+ "nexus": {
63
+ "command": "uvx",
64
+ "args": ["--refresh", "--from", "nexus-dev-toolkit", "nexus-mcp"],
65
+ }
66
+ }
67
+
68
+
69
+ def _init_project(project_dir: Path) -> list[str]:
70
+ """
71
+ nexus init — sets up:
72
+ .claude/commands/ ← built-in skills
73
+ .claude/settings.json ← PostToolUse graphify hook
74
+ knowledge/ ← empty scaffold
75
+ """
76
+ created = []
77
+
78
+ # .claude/commands/ — copy built-in skills
79
+ commands_dir = project_dir / ".claude" / "commands"
80
+ commands_dir.mkdir(parents=True, exist_ok=True)
81
+
82
+ for skill_name in _BUILTIN_SKILLS:
83
+ src = _SKILLS_SRC / skill_name
84
+ dest = commands_dir / skill_name
85
+ if src.exists() and not dest.exists():
86
+ shutil.copy2(src, dest)
87
+ created.append(f".claude/commands/{skill_name}")
88
+
89
+ # .claude/settings.json — PostToolUse graphify hook
90
+ settings_path = project_dir / ".claude" / "settings.json"
91
+ if not settings_path.exists():
92
+ settings_path.write_text(json.dumps(_CLAUDE_SETTINGS, indent=2))
93
+ created.append(".claude/settings.json")
94
+
95
+ # knowledge/ scaffold
96
+ for d in _KNOWLEDGE_DIRS:
97
+ target = project_dir / d
98
+ target.mkdir(parents=True, exist_ok=True)
99
+
100
+ return created
101
+
102
+
103
+ def _write_mcp_config(project_dir: Path) -> str:
104
+ mcp_path = project_dir / ".mcp.json"
105
+ existing: dict = {}
106
+ if mcp_path.exists():
107
+ try:
108
+ existing = json.loads(mcp_path.read_text())
109
+ except Exception:
110
+ pass
111
+ existing.setdefault("mcpServers", {}).update(_MCP_BLOCK)
112
+ mcp_path.write_text(json.dumps(existing, indent=2))
113
+ return ".mcp.json"
114
+
115
+
116
+ # ── Commands ──────────────────────────────────────────────────────────────────
117
+
118
+ @app.command()
119
+ def init(
120
+ project_dir: str = typer.Argument(".", help="Project directory to initialize"),
121
+ ) -> None:
122
+ """Initialize .claude/commands/, .claude/settings.json, and knowledge/ in a project."""
123
+ root = Path(project_dir).resolve()
124
+ console.print(f"\n [cyan]▶[/cyan] Initializing nexus in [bold]{root}[/bold]\n")
125
+
126
+ created = _init_project(root)
127
+ for f in created:
128
+ console.print(f" [green]✓[/green] {f}")
129
+
130
+ if not created:
131
+ console.print(" [yellow]·[/yellow] Already initialized — nothing to do")
132
+ return
133
+
134
+ mcp = _write_mcp_config(root)
135
+ console.print(f" [green]✓[/green] {mcp}")
136
+
137
+ console.print(f"\n [bold green]Done.[/bold green] Open [bold]{root}[/bold] in Claude Code and type [cyan]/scaffold[/cyan]\n")
138
+
139
+
140
+ @app.command()
141
+ def update() -> None:
142
+ """Update nexus-dev-toolkit to the latest version."""
143
+ console.print("\n [cyan]▶[/cyan] Updating nexus-dev-toolkit…\n")
144
+ if shutil.which("uv"):
145
+ subprocess.run(["uv", "tool", "upgrade", "nexus-dev-toolkit"])
146
+ else:
147
+ subprocess.run([sys.executable, "-m", "pip", "install", "--upgrade", "nexus-dev-toolkit"])
148
+ console.print("\n [green]✓[/green] Done.\n")
149
+
150
+
151
+ # ── skill subcommands ─────────────────────────────────────────────────────────
152
+
153
+ _SKILL_TEMPLATE = """\
154
+ # /{name}
155
+
156
+ **{name}** — describe what this skill does.
157
+
158
+ ## When to use
159
+
160
+ Describe the trigger or context.
161
+
162
+ ## Steps
163
+
164
+ ### 1 — First step
165
+
166
+ What to do.
167
+
168
+ ### 2 — Second step
169
+
170
+ What to do next.
171
+
172
+ ### 3 — Output
173
+
174
+ What the AI should produce when done.
175
+ """
176
+
177
+
178
+ @skill_app.command("add")
179
+ def skill_add(
180
+ name: str = typer.Argument(..., help="Skill name (e.g. 'code-review')"),
181
+ project_dir: str = typer.Option(".", "--dir", "-d"),
182
+ ) -> None:
183
+ """Create a new skill in .claude/commands/."""
184
+ root = Path(project_dir).resolve()
185
+ dest = root / ".claude" / "commands" / f"{name}.md"
186
+ dest.parent.mkdir(parents=True, exist_ok=True)
187
+
188
+ if dest.exists():
189
+ console.print(f" [yellow]·[/yellow] .claude/commands/{name}.md already exists")
190
+ return
191
+
192
+ dest.write_text(_SKILL_TEMPLATE.format(name=name), encoding="utf-8")
193
+ console.print(f" [green]✓[/green] Created .claude/commands/{name}.md")
194
+ console.print(f" [dim]Edit it and type [cyan]/{name}[/cyan] in Claude Code.[/dim]")
195
+
196
+
197
+ @skill_app.command("list")
198
+ def skill_list(
199
+ project_dir: str = typer.Option(".", "--dir", "-d"),
200
+ ) -> None:
201
+ """List all skills in .claude/commands/."""
202
+ root = Path(project_dir).resolve()
203
+ commands_dir = root / ".claude" / "commands"
204
+
205
+ if not commands_dir.exists():
206
+ console.print(" [yellow]·[/yellow] No .claude/commands/ found. Run [cyan]nexus init[/cyan] first.")
207
+ return
208
+
209
+ skills = sorted(commands_dir.glob("*.md"))
210
+ if not skills:
211
+ console.print(" [yellow]·[/yellow] No skills yet. Run [cyan]nexus skill add <name>[/cyan]")
212
+ return
213
+
214
+ table = Table(show_header=True, header_style="dim")
215
+ table.add_column("Skill", style="cyan")
216
+ table.add_column("Source")
217
+
218
+ builtins = set(_BUILTIN_SKILLS)
219
+ for s in skills:
220
+ source = "built-in" if s.name in builtins else "custom"
221
+ table.add_row(f"/{s.stem}", source)
222
+
223
+ console.print(table)
224
+
225
+
226
+ # ── rule subcommands ──────────────────────────────────────────────────────────
227
+
228
+ _RULE_TEMPLATE = """\
229
+ # {name}
230
+
231
+ _Project rule — read by AI tools via AGENTS.md._
232
+
233
+ ## Rules
234
+
235
+ - Rule one
236
+ - Rule two
237
+ - Rule three
238
+
239
+ ## Rationale
240
+
241
+ Why these rules exist.
242
+ """
243
+
244
+
245
+ @rule_app.command("add")
246
+ def rule_add(
247
+ name: str = typer.Argument(..., help="Rule name (e.g. 'api-standards')"),
248
+ project_dir: str = typer.Option(".", "--dir", "-d"),
249
+ ) -> None:
250
+ """Create a new rule in knowledge/rules/."""
251
+ root = Path(project_dir).resolve()
252
+ dest = root / "knowledge" / "rules" / f"{name}.md"
253
+ dest.parent.mkdir(parents=True, exist_ok=True)
254
+
255
+ if dest.exists():
256
+ console.print(f" [yellow]·[/yellow] knowledge/rules/{name}.md already exists")
257
+ return
258
+
259
+ dest.write_text(_RULE_TEMPLATE.format(name=name), encoding="utf-8")
260
+ console.print(f" [green]✓[/green] Created knowledge/rules/{name}.md")
261
+
262
+
263
+ @rule_app.command("list")
264
+ def rule_list(
265
+ project_dir: str = typer.Option(".", "--dir", "-d"),
266
+ ) -> None:
267
+ """List all rules in knowledge/rules/."""
268
+ root = Path(project_dir).resolve()
269
+ rules_dir = root / "knowledge" / "rules"
270
+
271
+ if not rules_dir.exists():
272
+ console.print(" [yellow]·[/yellow] No knowledge/rules/ found. Run [cyan]nexus init[/cyan] first.")
273
+ return
274
+
275
+ rules = sorted(rules_dir.glob("*.md"))
276
+ if not rules:
277
+ console.print(" [yellow]·[/yellow] No rules yet. Run [cyan]nexus rule add <name>[/cyan]")
278
+ return
279
+
280
+ table = Table(show_header=True, header_style="dim")
281
+ table.add_column("Rule", style="cyan")
282
+ for r in rules:
283
+ table.add_row(r.stem)
284
+ console.print(table)
285
+
286
+
287
+ def main() -> None:
288
+ app()
289
+
290
+
291
+ if __name__ == "__main__":
292
+ main()
@@ -0,0 +1,170 @@
1
+ Metadata-Version: 2.4
2
+ Name: nexus-dev-toolkit
3
+ Version: 3.0.0
4
+ Summary: LLM-agnostic developer workflow toolkit — Day 0 scaffold + Day 1 EPAV
5
+ Author-email: Ronald dela Cruz <rcdelacruz@users.noreply.github.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://nexus.coderstudio.co
8
+ Project-URL: Repository, https://github.com/rcdelacruz/nexus-dev-toolkit
9
+ Project-URL: Issues, https://github.com/rcdelacruz/nexus-dev-toolkit/issues
10
+ Requires-Python: >=3.10
11
+ Description-Content-Type: text/markdown
12
+ License-File: LICENSE
13
+ Requires-Dist: mcp[cli]>=1.0.0
14
+ Requires-Dist: typer>=0.12.0
15
+ Requires-Dist: rich>=13.0.0
16
+ Provides-Extra: dev
17
+ Requires-Dist: pytest>=8.0.0; extra == "dev"
18
+ Requires-Dist: pytest-asyncio>=0.23.0; extra == "dev"
19
+ Dynamic: license-file
20
+
21
+ # nexus-dev-toolkit
22
+
23
+ [![PyPI version](https://img.shields.io/pypi/v/nexus-dev-toolkit)](https://pypi.org/project/nexus-dev-toolkit/)
24
+ [![Python](https://img.shields.io/pypi/pyversions/nexus-dev-toolkit)](https://pypi.org/project/nexus-dev-toolkit/)
25
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
26
+
27
+ Developer workflow toolkit for AI-assisted development. Gives any team a structured Day 0 scaffold and repeatable Day 1 feature cycle via the EPAV methodology.
28
+
29
+ ---
30
+
31
+ ## Why
32
+
33
+ Ad-hoc AI prompting doesn't scale. Every dev prompts differently, context drifts, and nobody knows what the AI was told last sprint.
34
+
35
+ `nexus-dev-toolkit` gives your team a single workflow:
36
+
37
+ - **Day 0** — scaffold the project once, production-grade, zero credentials needed
38
+ - **Day 1** — every feature follows the same four steps: evaluate → plan → apply → validate
39
+
40
+ Every skill, every rule, every pattern lives in the project repo — versioned, shared, and enforced.
41
+
42
+ ---
43
+
44
+ ## The Workflow
45
+
46
+ ### Day 0 — `/scaffold` (once per project)
47
+
48
+ ```
49
+ nexus init <project-dir>
50
+ ```
51
+
52
+ Sets up `.claude/commands/`, `.claude/settings.json`, and `knowledge/`. Then in Claude Code:
53
+
54
+ ```
55
+ /scaffold
56
+ ```
57
+
58
+ EVALUATE → PLAN → APPLY → VALIDATE. Produces a production-grade project: correct stack from your arch doc, mock auth, mock data, design system, AGENTS.md — all from your architecture document. Runs with `npm install && npm run dev` (or equivalent) from commit one.
59
+
60
+ ### Day 1 — EPAV (every feature, every sprint)
61
+
62
+ ```
63
+ /evaluate → /plan → /apply → /validate
64
+ ```
65
+
66
+ Each step is a built-in skill in `.claude/commands/`. Every task starts with a row from the dev tasks CSV. Every task ends with acceptance criteria verified.
67
+
68
+ ---
69
+
70
+ ## Install
71
+
72
+ **Requirements:** Python 3.10+, `uv` (recommended) or `pip`
73
+
74
+ ```bash
75
+ # Recommended
76
+ uv tool install nexus-dev-toolkit
77
+
78
+ # Or via pip
79
+ pip install nexus-dev-toolkit
80
+ ```
81
+
82
+ ---
83
+
84
+ ## Quick Start
85
+
86
+ ```bash
87
+ cd my-project
88
+ nexus init .
89
+ ```
90
+
91
+ Then open the project in Claude Code and type `/scaffold`.
92
+
93
+ ---
94
+
95
+ ## Commands
96
+
97
+ ```bash
98
+ nexus init . # set up .claude/commands/ + knowledge/ + .mcp.json
99
+ nexus skill add code-review # create a custom skill in .claude/commands/
100
+ nexus skill list # list all skills
101
+ nexus rule add api-standards # create a rule in knowledge/rules/
102
+ nexus rule list # list all rules
103
+ nexus update # update to latest version
104
+ ```
105
+
106
+ ---
107
+
108
+ ## What `nexus init` Creates
109
+
110
+ ```
111
+ .claude/
112
+ ├── commands/
113
+ │ ├── scaffold.md ← /scaffold — Day 0 one-time setup
114
+ │ ├── evaluate.md ← /evaluate — orient on a task
115
+ │ ├── plan.md ← /plan — blueprint, no code
116
+ │ ├── apply.md ← /apply — implement the plan
117
+ │ ├── validate.md ← /validate — verify acceptance criteria
118
+ │ └── epav.md ← /epav — full cycle guide
119
+ └── settings.json ← PostToolUse hook: graphify auto-updates after every file edit
120
+ knowledge/
121
+ ├── rules/ ← coding standards, arch decisions
122
+ ├── patterns/ ← reusable implementation patterns
123
+ ├── prompts/dev/ ← task prompt templates
124
+ └── retros/ ← retrospective notes
125
+ .mcp.json ← MCP server config
126
+ ```
127
+
128
+ ---
129
+
130
+ ## MCP Server
131
+
132
+ ```json
133
+ {
134
+ "mcpServers": {
135
+ "nexus": {
136
+ "command": "uvx",
137
+ "args": ["--refresh", "--from", "nexus-dev-toolkit", "nexus-mcp"]
138
+ }
139
+ }
140
+ }
141
+ ```
142
+
143
+ `nexus init` writes `.mcp.json` automatically.
144
+
145
+ ### MCP Tools
146
+
147
+ | Tool | Purpose |
148
+ |---|---|
149
+ | `ingest_architecture_doc` | Load arch doc → `knowledge/rules/arch-summary.md` |
150
+ | `load_task` | Load a CSV task row into context |
151
+ | `generate_project_rules` | Generate `AGENTS.md` from arch doc |
152
+ | `resolve_package_versions` | Resolve exact package versions via real package manager |
153
+
154
+ ---
155
+
156
+ ## Custom Skills
157
+
158
+ ```bash
159
+ nexus skill add my-code-review
160
+ # Edit .claude/commands/my-code-review.md
161
+ # Type /my-code-review in Claude Code
162
+ ```
163
+
164
+ Custom skills live alongside built-in skills in `.claude/commands/` — versioned in your repo, shared across the team.
165
+
166
+ ---
167
+
168
+ ## License
169
+
170
+ [MIT](LICENSE)
@@ -0,0 +1,21 @@
1
+ nexus_cli.py,sha256=Z8bwSC5T9-_uaB273g24-TZjit_lxyv46T9-6zB-wnE,9118
2
+ nexus_server.py,sha256=XSo_2Azq28sqL8bKL4ahx7hsgTuABgwmiZBHIs_EQtM,278
3
+ nexus_dev_toolkit-3.0.0.dist-info/licenses/LICENSE,sha256=IebhcEWRJRzVU5aZPAAN1-xa36NeQtJ3lnA3KF-dKnA,1073
4
+ tools/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
+ tools/epav/__init__.py,sha256=U5_ZOSDCyS7l688craVbj13_aD86pYOHooLo-YGoeew,559
6
+ tools/epav/arch_ingest.py,sha256=EqoEqcfLg76_ISU7Jlpdk9xpl5YfEofXOn7NrNpESXM,4810
7
+ tools/epav/package_resolver.py,sha256=DMpXR-wXJbWc5eTbCUyUD-ujRvn8ZJKRoJUoQRgBX_Y,9508
8
+ tools/epav/project_rules.py,sha256=glz6hoEkOXWtPcrotphcT5g0wjXATcyS_VYPgCo0Qas,6958
9
+ tools/epav/task_loader.py,sha256=t7WeYL3LGF2t8mijj_mpl0iiglWK6T4cRFjbWv39Wm4,5243
10
+ tools/epav/skills/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
+ tools/epav/skills/apply.md,sha256=mQEAQwT0Bqkf9CVIAyLvI7nctGmbI6zn0szqQYOzvy8,1115
12
+ tools/epav/skills/epav.md,sha256=Z-Uy3Ej-V-8OWtaWFKygLI1wO-EGQlWZOB8p-jz9t_Y,1317
13
+ tools/epav/skills/evaluate.md,sha256=KMQbKQ1lVJDtl9w12Al4v-gC7LlnQPNkJ8spyvidP1c,1400
14
+ tools/epav/skills/plan.md,sha256=bj2IGu8FyfaJp-WojzsM3aQuRauiE8cGPbZkfX1EWLU,1046
15
+ tools/epav/skills/scaffold.md,sha256=xAhxWfhYKIdvLrhEl4dU8BYp9P6q9q0g0ifmM7YXJuo,10952
16
+ tools/epav/skills/validate.md,sha256=qLYuQgGwKT1bWFOlXSmsAounvkfPrfVOrVo5Pk0fvRg,1411
17
+ nexus_dev_toolkit-3.0.0.dist-info/METADATA,sha256=uf5PnsuSij31wLmdqvb0LfM1lJhaptHENx8PvG9aNhE,4943
18
+ nexus_dev_toolkit-3.0.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
19
+ nexus_dev_toolkit-3.0.0.dist-info/entry_points.txt,sha256=X6brRRXNnOreQFKMXeF0mbTVmpzWHX44VGOrVeGL6-Q,71
20
+ nexus_dev_toolkit-3.0.0.dist-info/top_level.txt,sha256=_3H722hZfNwMcAzexQoFrJ-I-tceGD42Zxe1re8MMSc,29
21
+ nexus_dev_toolkit-3.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ nexus = nexus_cli:main
3
+ nexus-mcp = nexus_server:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ronald dela Cruz
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,3 @@
1
+ nexus_cli
2
+ nexus_server
3
+ tools
nexus_server.py ADDED
@@ -0,0 +1,16 @@
1
+ import logging
2
+ from mcp.server.fastmcp import FastMCP
3
+ from tools.epav import register_epav_tools
4
+
5
+ logging.basicConfig(level=logging.WARNING)
6
+
7
+ mcp = FastMCP("nexus-dev-toolkit")
8
+ register_epav_tools(mcp)
9
+
10
+
11
+ def main() -> None:
12
+ mcp.run()
13
+
14
+
15
+ if __name__ == "__main__":
16
+ main()
tools/__init__.py ADDED
File without changes
tools/epav/__init__.py ADDED
@@ -0,0 +1,14 @@
1
+ from mcp.server.fastmcp import FastMCP
2
+
3
+ from tools.epav.arch_ingest import register_arch_ingest_tool
4
+ from tools.epav.task_loader import register_task_loader_tool
5
+ from tools.epav.project_rules import register_project_rules_tool
6
+ from tools.epav.package_resolver import register_package_resolver_tool
7
+
8
+
9
+ def register_epav_tools(mcp: FastMCP) -> None:
10
+ """Register Day 0 + Day 1 EPAV tools onto the MCP server."""
11
+ register_arch_ingest_tool(mcp)
12
+ register_task_loader_tool(mcp)
13
+ register_project_rules_tool(mcp)
14
+ register_package_resolver_tool(mcp)
@@ -0,0 +1,124 @@
1
+ import json
2
+ import logging
3
+ from pathlib import Path
4
+
5
+ from mcp.server.fastmcp import FastMCP
6
+
7
+ logger = logging.getLogger(__name__)
8
+
9
+ _ARCH_SECTIONS = [
10
+ "stack", "tech stack", "technology stack",
11
+ "data model", "schema", "database",
12
+ "auth", "authentication", "authorization",
13
+ "error", "error handling", "error format",
14
+ "security", "cors", "headers",
15
+ "adr", "architecture decision",
16
+ "api", "conventions", "standards",
17
+ "middleware", "infrastructure",
18
+ ]
19
+
20
+
21
+ def _extract_sections(text: str) -> dict:
22
+ """Pull labelled sections from markdown text by heading keywords."""
23
+ sections: dict[str, list[str]] = {}
24
+ current_key = "preamble"
25
+ current_lines: list[str] = []
26
+
27
+ for line in text.splitlines():
28
+ stripped = line.lstrip("#").strip().lower()
29
+ matched = next((k for k in _ARCH_SECTIONS if k in stripped), None)
30
+ if line.startswith("#") and matched:
31
+ if current_lines:
32
+ sections[current_key] = current_lines
33
+ current_key = stripped
34
+ current_lines = [line]
35
+ else:
36
+ current_lines.append(line)
37
+
38
+ if current_lines:
39
+ sections[current_key] = current_lines
40
+
41
+ return {k: "\n".join(v).strip() for k, v in sections.items() if v}
42
+
43
+
44
+ def register_arch_ingest_tool(mcp: FastMCP) -> None:
45
+
46
+ @mcp.tool()
47
+ async def ingest_architecture_doc(
48
+ doc_path: str = "",
49
+ save_summary: bool = True,
50
+ ) -> str:
51
+ """
52
+ Ingest an architecture document and extract key decisions for the EPAV workflow.
53
+
54
+ Reads a markdown architecture doc (or scans docs/arch-docs/ if no path given),
55
+ extracts stack decisions, data model, auth strategy, error format, security rules,
56
+ and ADR list. Optionally writes a summary to knowledge/rules/arch-summary.md.
57
+
58
+ Args:
59
+ doc_path: Path to the architecture doc (.md) or a directory containing
60
+ arch docs. If empty, looks for docs/arch-docs/ in the project root.
61
+ save_summary: If True, writes extracted summary to
62
+ knowledge/rules/arch-summary.md (creates dirs if needed).
63
+
64
+ Returns:
65
+ JSON with extracted architecture sections and file paths written.
66
+ """
67
+ try:
68
+ # Resolve the source path
69
+ source = Path(doc_path) if doc_path else Path("docs/arch-docs")
70
+
71
+ if not source.exists():
72
+ return json.dumps({
73
+ "error": f"Path not found: {source}. "
74
+ "Provide doc_path or create docs/arch-docs/ with your architecture doc."
75
+ })
76
+
77
+ # Collect markdown files
78
+ if source.is_file():
79
+ md_files = [source]
80
+ else:
81
+ md_files = sorted(source.rglob("*.md")) + sorted(source.rglob("*.txt"))
82
+
83
+ if not md_files:
84
+ return json.dumps({"error": f"No markdown files found in {source}"})
85
+
86
+ # Extract sections from all files
87
+ all_sections: dict[str, str] = {}
88
+ files_read = []
89
+ for f in md_files:
90
+ try:
91
+ text = f.read_text(encoding="utf-8")
92
+ sections = _extract_sections(text)
93
+ all_sections.update(sections)
94
+ files_read.append(str(f))
95
+ except Exception as e:
96
+ logger.warning("Could not read %s: %s", f, e)
97
+
98
+ if not all_sections:
99
+ return json.dumps({
100
+ "error": "No recognisable architecture sections found. "
101
+ "Check that headings use standard terms (stack, auth, data model, etc.)."
102
+ })
103
+
104
+ files_written = []
105
+ if save_summary:
106
+ summary_path = Path("knowledge/rules/arch-summary.md")
107
+ summary_path.parent.mkdir(parents=True, exist_ok=True)
108
+ lines = ["# Architecture Summary\n", "_Auto-generated by ingest_architecture_doc_\n"]
109
+ for section, content in all_sections.items():
110
+ lines.append(f"\n## {section.title()}\n\n{content}\n")
111
+ summary_path.write_text("\n".join(lines), encoding="utf-8")
112
+ files_written.append(str(summary_path))
113
+
114
+ return json.dumps({
115
+ "files_read": files_read,
116
+ "files_written": files_written,
117
+ "sections_extracted": list(all_sections.keys()),
118
+ "summary": {k: v[:300] + "…" if len(v) > 300 else v
119
+ for k, v in all_sections.items()},
120
+ }, indent=2)
121
+
122
+ except Exception as e:
123
+ logger.exception("Unexpected error in ingest_architecture_doc")
124
+ return json.dumps({"error": f"Unexpected error: {e}"})