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 +292 -0
- nexus_dev_toolkit-3.0.0.dist-info/METADATA +170 -0
- nexus_dev_toolkit-3.0.0.dist-info/RECORD +21 -0
- nexus_dev_toolkit-3.0.0.dist-info/WHEEL +5 -0
- nexus_dev_toolkit-3.0.0.dist-info/entry_points.txt +3 -0
- nexus_dev_toolkit-3.0.0.dist-info/licenses/LICENSE +21 -0
- nexus_dev_toolkit-3.0.0.dist-info/top_level.txt +3 -0
- nexus_server.py +16 -0
- tools/__init__.py +0 -0
- tools/epav/__init__.py +14 -0
- tools/epav/arch_ingest.py +124 -0
- tools/epav/package_resolver.py +271 -0
- tools/epav/project_rules.py +208 -0
- tools/epav/skills/__init__.py +0 -0
- tools/epav/skills/apply.md +41 -0
- tools/epav/skills/epav.md +46 -0
- tools/epav/skills/evaluate.md +42 -0
- tools/epav/skills/plan.md +46 -0
- tools/epav/skills/scaffold.md +249 -0
- tools/epav/skills/validate.md +57 -0
- tools/epav/task_loader.py +140 -0
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
|
+
[](https://pypi.org/project/nexus-dev-toolkit/)
|
|
24
|
+
[](https://pypi.org/project/nexus-dev-toolkit/)
|
|
25
|
+
[](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,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.
|
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}"})
|