deepmodel-dmx 0.1.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.
- deepmodel_dmx-0.1.0.dist-info/METADATA +219 -0
- deepmodel_dmx-0.1.0.dist-info/RECORD +40 -0
- deepmodel_dmx-0.1.0.dist-info/WHEEL +4 -0
- deepmodel_dmx-0.1.0.dist-info/entry_points.txt +2 -0
- deepmodel_dmx-0.1.0.dist-info/licenses/LICENSE +38 -0
- dmx/__init__.py +24 -0
- dmx/_workflow_version.py +10 -0
- dmx/catalog.py +260 -0
- dmx/cli.py +196 -0
- dmx/exceptions.py +55 -0
- dmx/http_auth.py +50 -0
- dmx/ide/__init__.py +12 -0
- dmx/ide/detect.py +86 -0
- dmx/ide/emitters.py +264 -0
- dmx/rules/system-prompt.md +116 -0
- dmx/server.py +244 -0
- dmx/skills/specialist/dmx-docs.md +132 -0
- dmx/skills/specialist/dmx-review.md +101 -0
- dmx/skills/specialist/dmx-secure.md +112 -0
- dmx/skills/specialist/dmx-test.md +102 -0
- dmx/skills/utility/dmx-commit.md +174 -0
- dmx/skills/utility/dmx-create-branch.md +216 -0
- dmx/skills/utility/dmx-draft-pr-description.md +176 -0
- dmx/skills/utility/dmx-status.md +114 -0
- dmx/skills/utility/dmx-sync-branch.md +96 -0
- dmx/skills/utility/dmx-update-memory.md +114 -0
- dmx/skills/workflow/0-init/dmx-init.md +346 -0
- dmx/skills/workflow/1-triage/dmx-create-ticket.md +268 -0
- dmx/skills/workflow/1-triage/dmx-derive-ticket.md +267 -0
- dmx/skills/workflow/1-triage/dmx-hotfix.md +122 -0
- dmx/skills/workflow/2-plan/dmx-plan.md +132 -0
- dmx/skills/workflow/3-build/dmx-implement-next-phase.md +88 -0
- dmx/skills/workflow/3-build/dmx-implement-next-task.md +84 -0
- dmx/skills/workflow/4-validate/dmx-validate.md +173 -0
- dmx/skills/workflow/5-ship/dmx-close-ticket.md +208 -0
- dmx/skills/workflow/5-ship/dmx-create-pr.md +153 -0
- dmx/skills/workflow/6-release/dmx-create-release.md +122 -0
- dmx/skills/workflow/6-release/dmx-draft-release-note.md +216 -0
- dmx/skills/workflow/6-release/dmx-release-merge.md +144 -0
- dmx/tools.py +244 -0
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: deepmodel-dmx
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: AI SDLC MCP server — skills and rules for any AI IDE.
|
|
5
|
+
License: AGPL-3.0
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Keywords: ai,claude,cursor,mcp,sdlc
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: License :: OSI Approved :: GNU Affero General Public License v3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
+
Requires-Python: >=3.11
|
|
16
|
+
Requires-Dist: click>=8.1
|
|
17
|
+
Requires-Dist: fastmcp>=2.0
|
|
18
|
+
Requires-Dist: python-frontmatter>=1.1
|
|
19
|
+
Requires-Dist: pyyaml>=6.0
|
|
20
|
+
Requires-Dist: starlette>=0.40
|
|
21
|
+
Provides-Extra: dev
|
|
22
|
+
Requires-Dist: httpx2>=2.2.0; extra == 'dev'
|
|
23
|
+
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
24
|
+
Requires-Dist: pre-commit>=3; extra == 'dev'
|
|
25
|
+
Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
|
|
26
|
+
Requires-Dist: pytest-cov>=5; extra == 'dev'
|
|
27
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
28
|
+
Requires-Dist: ruff>=0.4; extra == 'dev'
|
|
29
|
+
Requires-Dist: syrupy>=4; extra == 'dev'
|
|
30
|
+
Requires-Dist: types-pyyaml>=6.0; extra == 'dev'
|
|
31
|
+
Provides-Extra: watch
|
|
32
|
+
Requires-Dist: watchfiles>=0.21; extra == 'watch'
|
|
33
|
+
Description-Content-Type: text/markdown
|
|
34
|
+
|
|
35
|
+
# dmx
|
|
36
|
+
|
|
37
|
+
[](https://pypi.org/project/deepmodel-dmx/)
|
|
38
|
+
[](https://github.com/deepmodel-ai/dmx/actions/workflows/test.yml)
|
|
39
|
+
[](LICENSE)
|
|
40
|
+
|
|
41
|
+
The official implementation of the [AI SDLC](https://github.com/deepmodel-ai/ai-sdlc) — delivered as an MCP server for any AI IDE.
|
|
42
|
+
|
|
43
|
+
`dmx` turns the AI SDLC framework into slash commands and always-apply engineering rules. Connect it to Cursor, Claude Code, GitHub Copilot, Antigravity, or any MCP-compatible IDE and get a structured, phase-gated AI engineering workflow out of the box.
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## The workflow
|
|
48
|
+
|
|
49
|
+
The [AI SDLC](https://github.com/deepmodel-ai/ai-sdlc) structures development around five phases with explicit developer control points. AI executes within each phase and stops. The developer drives every transition forward.
|
|
50
|
+
|
|
51
|
+

|
|
52
|
+
|
|
53
|
+
| Phase | What happens | dmx command |
|
|
54
|
+
|---|---|---|
|
|
55
|
+
| **Specify** | AI drafts a structured spec, surfaces ambiguity, asks Q&A. Developer answers. | `/dmx/create-ticket` |
|
|
56
|
+
| **Plan** | AI generates a phased task list from the answered spec. Developer reviews. | `/dmx/plan` |
|
|
57
|
+
| **Build** | AI implements one phase, stops, reports. Developer reviews and commits. Repeats. | `/dmx/implement-next-phase` |
|
|
58
|
+
| **Validate** | Automated quality gate checks spec, security, coverage. AI drafts the PR body. | `/dmx/validate` · `/dmx/create-pr` |
|
|
59
|
+
| **Ship** | Developer merges. AI tags the release and publishes. Developer confirms. | `/dmx/create-release` |
|
|
60
|
+
|
|
61
|
+
> The model does not merge. It does not advance the workflow. It does not decide when the work is done.
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## Quick start
|
|
66
|
+
|
|
67
|
+
Add to your IDE's MCP config and restart:
|
|
68
|
+
|
|
69
|
+
**Cursor** — `~/.cursor/mcp.json`
|
|
70
|
+
|
|
71
|
+
```json
|
|
72
|
+
{
|
|
73
|
+
"mcpServers": {
|
|
74
|
+
"dmx": {
|
|
75
|
+
"command": "uvx",
|
|
76
|
+
"args": ["--from", "deepmodel-dmx", "dmx", "serve"]
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
**Claude Code** — `~/.claude/claude_desktop_config.json`
|
|
83
|
+
|
|
84
|
+
```json
|
|
85
|
+
{
|
|
86
|
+
"mcpServers": {
|
|
87
|
+
"dmx": {
|
|
88
|
+
"command": "uvx",
|
|
89
|
+
"args": ["--from", "deepmodel-dmx", "dmx", "serve"]
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
**Antigravity** — `~/.gemini/config/mcp_config.json`
|
|
96
|
+
|
|
97
|
+
```json
|
|
98
|
+
{
|
|
99
|
+
"mcpServers": {
|
|
100
|
+
"dmx": {
|
|
101
|
+
"command": "uvx",
|
|
102
|
+
"args": ["--from", "deepmodel-dmx", "dmx", "serve"]
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
**GitHub Copilot** — `.vscode/settings.json` (workspace) or user settings
|
|
109
|
+
|
|
110
|
+
```json
|
|
111
|
+
{
|
|
112
|
+
"mcp": {
|
|
113
|
+
"servers": {
|
|
114
|
+
"dmx": {
|
|
115
|
+
"command": "uvx",
|
|
116
|
+
"args": ["--from", "deepmodel-dmx", "dmx", "serve"]
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Then run `/dmx-init` in your IDE to write always-apply rules and scaffold the `.dmx/` memory bank.
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## `/dmx-init` walkthrough
|
|
128
|
+
|
|
129
|
+
`/dmx-init` is the one-time project setup skill. It:
|
|
130
|
+
|
|
131
|
+
1. Detects your workflow mode (feature branches vs. trunk) and ticketing system
|
|
132
|
+
2. Auto-detects your GitHub remote and tech stack
|
|
133
|
+
3. Writes `.dmx/config.md` with project context
|
|
134
|
+
4. Scaffolds the `.dmx/` memory bank (architecture, decisions, style guide, etc.)
|
|
135
|
+
5. Calls `detect_invoking_ide` and `setup_ide_rules` to write IDE-specific rule files
|
|
136
|
+
6. Writes the always-apply persona rule so every future chat starts with the right context
|
|
137
|
+
|
|
138
|
+
Safe to re-run — updates config without overwriting memory bank files that already have content.
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
## Skill catalog
|
|
143
|
+
|
|
144
|
+
All `/dmx-*` commands are MCP prompts served live — no file writes, no installation beyond the MCP config.
|
|
145
|
+
|
|
146
|
+
### Workflow
|
|
147
|
+
|
|
148
|
+
| Skill | What it does |
|
|
149
|
+
|---|---|
|
|
150
|
+
| `/dmx/init` | One-time project setup: rules, memory bank, IDE config |
|
|
151
|
+
| `/dmx/create-ticket` | Idea → ticket → branch → spec in one command |
|
|
152
|
+
| `/dmx/derive-ticket` | Uncommitted changes → ticket → branch → derived spec |
|
|
153
|
+
| `/dmx/plan` | Answered spec → phased `tasks.md` |
|
|
154
|
+
| `/dmx/implement-next-phase` | Execute the next phase in `tasks.md`, stop |
|
|
155
|
+
| `/dmx/implement-next-task` | Execute the next single task, stop |
|
|
156
|
+
| `/dmx/validate` | Pre-PR quality gate: ticket, code, security |
|
|
157
|
+
| `/dmx/create-branch` | Create a properly named branch, scaffold spec |
|
|
158
|
+
| `/dmx/commit` | Conventional commit from staged diff |
|
|
159
|
+
| `/dmx/create-pr` | Open PR with correct title + description |
|
|
160
|
+
| `/dmx/draft-pr-description` | Generate PR body without opening the PR |
|
|
161
|
+
| `/dmx/close-ticket` | Post-merge: close ticket, delete branch, archive |
|
|
162
|
+
|
|
163
|
+
### Release
|
|
164
|
+
|
|
165
|
+
| Skill | What it does |
|
|
166
|
+
|---|---|
|
|
167
|
+
| `/dmx/hotfix` | Create hotfix branch from master |
|
|
168
|
+
| `/dmx/draft-release-note` | Generate release notes from merged PRs |
|
|
169
|
+
| `/dmx/release-merge` | Open staging → master release gate PR |
|
|
170
|
+
| `/dmx/create-release` | Tag and publish GitHub release |
|
|
171
|
+
|
|
172
|
+
### Utilities
|
|
173
|
+
|
|
174
|
+
| Skill | What it does |
|
|
175
|
+
|---|---|
|
|
176
|
+
| `/dmx/status` | Snapshot of in-progress tickets and open PRs |
|
|
177
|
+
| `/dmx/sync-branch` | Rebase/merge base branch onto current branch |
|
|
178
|
+
| `/dmx/update-memory` | Sync ticket learnings into memory bank |
|
|
179
|
+
| `/dmx/review` | Code review: clarity, correctness, maintainability |
|
|
180
|
+
| `/dmx/test` | Write tests that enable change |
|
|
181
|
+
| `/dmx/docs` | Write clear, human-first documentation |
|
|
182
|
+
| `/dmx/secure` | Security analysis — thinks like an attacker |
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
## CLI
|
|
187
|
+
|
|
188
|
+
```
|
|
189
|
+
dmx serve # stdio transport (default, for IDE MCP config)
|
|
190
|
+
dmx serve --http # SSE transport on :8080 (team server / Docker)
|
|
191
|
+
dmx serve --http --port 9000
|
|
192
|
+
dmx serve --watch # hot-reload on file change (requires watchfiles extra)
|
|
193
|
+
dmx list-skills # print all loaded skills
|
|
194
|
+
dmx version
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
**Environment variables**
|
|
198
|
+
|
|
199
|
+
| Variable | Purpose | Default |
|
|
200
|
+
|---|---|---|
|
|
201
|
+
| `PORT` | HTTP port | `8080` |
|
|
202
|
+
| `DMX_SKILLS_DIR` | Override bundled skills directory | bundled |
|
|
203
|
+
| `DMX_RULES_DIR` | Override bundled rules directory | bundled |
|
|
204
|
+
| `DMX_IDE` | Force IDE detection (`cursor`, `claude`, `copilot`, `antigravity`, `agents`) | auto-detect |
|
|
205
|
+
| `MCP_API_KEY` | Bearer token for HTTP auth | — |
|
|
206
|
+
| `REQUIRE_API_KEY` | Enable HTTP bearer auth (`true`/`false`) | `false` |
|
|
207
|
+
|
|
208
|
+
---
|
|
209
|
+
|
|
210
|
+
## Requirements
|
|
211
|
+
|
|
212
|
+
- Python 3.11+
|
|
213
|
+
- `uv` (recommended) or any package manager that supports `uvx`
|
|
214
|
+
|
|
215
|
+
---
|
|
216
|
+
|
|
217
|
+
## License
|
|
218
|
+
|
|
219
|
+
[AGPL-3.0](LICENSE) — free for open-source use. Commercial licensing available from [Deepmodel](https://deepmodel.ai).
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
dmx/__init__.py,sha256=FKvXiNPIb80EWvfb4tDXc7EF0Miz4_oSDT8ir09hNTY,589
|
|
2
|
+
dmx/_workflow_version.py,sha256=malbsQczStdMzjvVKYkOKic9SYuM6M_CUtzcrA3NFOQ,390
|
|
3
|
+
dmx/catalog.py,sha256=j_LCH5joJjmt-qSE9d1c9Y7GW-JfuUJCZ1cNmoYIKx4,8832
|
|
4
|
+
dmx/cli.py,sha256=QcLTVOByBkBmCPL_oiM0_JJVunsMoPafJUYMoM2xNec,5891
|
|
5
|
+
dmx/exceptions.py,sha256=J6V73mXVLC9_OIogZBhL4DfPGPGSXInHBbPAsspABBI,1293
|
|
6
|
+
dmx/http_auth.py,sha256=dxZhNGC4i76O_iA7w1Ru6lGvCCyxJsn9HD-cAmsjENo,1644
|
|
7
|
+
dmx/server.py,sha256=9_Mfj7qVa2vNnHy6kWru6HB1m7XgzMdt65m4Bxh_g9w,8752
|
|
8
|
+
dmx/tools.py,sha256=w3oR4oQdnkUfqN4N4cGSiuiGpQS5Om5QHaUS0Jo78Nc,9061
|
|
9
|
+
dmx/ide/__init__.py,sha256=XIKMwOowWRpgAnrilOyI48s8dyIVgA5Z6-fADvM9fuw,291
|
|
10
|
+
dmx/ide/detect.py,sha256=IjkXUBPQHlMwUi2FJtDZUQJTopRUCAgcNln-megwW78,3376
|
|
11
|
+
dmx/ide/emitters.py,sha256=t66e9aj5pc6zIHGuVXt2plPnsShVtWe8OvKG9wls3Og,8508
|
|
12
|
+
dmx/rules/system-prompt.md,sha256=MSojHRNpPcdqau3LKrA6BFEvXRk2PqCx-heogzZNaLg,5819
|
|
13
|
+
dmx/skills/specialist/dmx-docs.md,sha256=TcbH6uDIVlEM_LImYmI_PZAJuQGL4Z-XNBxCMFRy_Tc,4935
|
|
14
|
+
dmx/skills/specialist/dmx-review.md,sha256=6gSg9YFIinbCHzPsOQS3cezLPTwhV93MJhxvSgf1-5U,3496
|
|
15
|
+
dmx/skills/specialist/dmx-secure.md,sha256=UQpp-ZZ2ogBOsnSRD9X9cXoBKI9spQueLotlxyAYrgY,4055
|
|
16
|
+
dmx/skills/specialist/dmx-test.md,sha256=jZF52vzMHLgHkI3onJSiTrOmD005rkJTxnc5MZ5kbco,4104
|
|
17
|
+
dmx/skills/utility/dmx-commit.md,sha256=9_RntB1eXTyiHFM5HjNAKKY5z9rlQ3NhQo7yfpB1vqc,6067
|
|
18
|
+
dmx/skills/utility/dmx-create-branch.md,sha256=g9umv8jXxL5gF6tOpMtWSJAeK1uzns_SHbb6bPp8m1I,6926
|
|
19
|
+
dmx/skills/utility/dmx-draft-pr-description.md,sha256=1_AoJv1TVxdQlwh56lVSuyy_LDfZFv5-F_1A4B_xfwM,6714
|
|
20
|
+
dmx/skills/utility/dmx-status.md,sha256=U64WfRb1bigzMVroPx4FoKYEro8Q2G7vLdjbxZdvjB4,3315
|
|
21
|
+
dmx/skills/utility/dmx-sync-branch.md,sha256=C0_nIIdKloDMeqQZf01fr75ZZXqlFd63s8xcUN90dZE,2582
|
|
22
|
+
dmx/skills/utility/dmx-update-memory.md,sha256=DirSebAuRO3q7kUGrwOig6XlBv3oW6gK7TCxjBUfQcc,4089
|
|
23
|
+
dmx/skills/workflow/0-init/dmx-init.md,sha256=PMeHwI2z7y67i_8PnsOXbQwK_3A-oj0bu_e-EnTynWc,11207
|
|
24
|
+
dmx/skills/workflow/1-triage/dmx-create-ticket.md,sha256=z6h-haRgG0Bd0YmpThORQhUgMLlCjYadRazJef2epSk,9118
|
|
25
|
+
dmx/skills/workflow/1-triage/dmx-derive-ticket.md,sha256=DjD9SDqtW0TFwoTTW-YIgh7V9BzqgCkOqOGZoJ-NVGs,8383
|
|
26
|
+
dmx/skills/workflow/1-triage/dmx-hotfix.md,sha256=e0DHSiwhVBwd71O4Rzg6eRhnXA9xNZUAkw7JFz19DYI,4184
|
|
27
|
+
dmx/skills/workflow/2-plan/dmx-plan.md,sha256=95Tr_iv2TTgJ4CdWcYbHsf2Z1-STeWXowMCi7Qvfafw,4875
|
|
28
|
+
dmx/skills/workflow/3-build/dmx-implement-next-phase.md,sha256=Dl0uiMrCvbk-uH6u1KH6SOQS3zeKZfrzLZJcF-hohtQ,3585
|
|
29
|
+
dmx/skills/workflow/3-build/dmx-implement-next-task.md,sha256=eqo8cV8gLSAPyulURBBM7erAHH3_gane2Tf3vJ4GTkE,3125
|
|
30
|
+
dmx/skills/workflow/4-validate/dmx-validate.md,sha256=4yeTbO2SRAD5Q4koHC0d8JlnVmkh5Rhe1vTPvkVIAFc,6088
|
|
31
|
+
dmx/skills/workflow/5-ship/dmx-close-ticket.md,sha256=E5vKt6Bun4hopg1dnGi1o7oO0iasuD7bY22XAUryEsc,6536
|
|
32
|
+
dmx/skills/workflow/5-ship/dmx-create-pr.md,sha256=ClWDcgq7rVNLX_DqH0t2h4SqUXCgMxbdZcmx0ESJ9fM,4885
|
|
33
|
+
dmx/skills/workflow/6-release/dmx-create-release.md,sha256=ufLqxoKmWObeic73ufzpeRfToTDkhKB6LxzJpLe7_kI,4021
|
|
34
|
+
dmx/skills/workflow/6-release/dmx-draft-release-note.md,sha256=QGiEEaerOnG9bTpmPWr2QY29gdUqPfJMjr4AdlSDUtY,6102
|
|
35
|
+
dmx/skills/workflow/6-release/dmx-release-merge.md,sha256=qGhrkBsL0j8Ig_qKoomWlMLwRB563Rur15Q7p2ZI4aM,5152
|
|
36
|
+
deepmodel_dmx-0.1.0.dist-info/METADATA,sha256=Updtkyo7OfAsdgrZvxPnvQ6-wULNr5uLIJ8yjWzVMhk,7594
|
|
37
|
+
deepmodel_dmx-0.1.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
38
|
+
deepmodel_dmx-0.1.0.dist-info/entry_points.txt,sha256=zC3IDbfwwSPWurk6bKz1oF8sAvGZ0U8JohQ8NZIDYqs,37
|
|
39
|
+
deepmodel_dmx-0.1.0.dist-info/licenses/LICENSE,sha256=g_rT-ZAvrkFf4v_I3nAwt_RygoWN4M2nZ08l7405lvQ,1809
|
|
40
|
+
deepmodel_dmx-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
GNU AFFERO GENERAL PUBLIC LICENSE
|
|
2
|
+
Version 3, 19 November 2007
|
|
3
|
+
|
|
4
|
+
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
|
5
|
+
Everyone is permitted to copy and distribute verbatim copies
|
|
6
|
+
of this license document, but changing it is not allowed.
|
|
7
|
+
|
|
8
|
+
Preamble
|
|
9
|
+
|
|
10
|
+
The GNU Affero General Public License is a free, copyleft license for
|
|
11
|
+
software and other kinds of works, specifically designed to ensure
|
|
12
|
+
cooperation with the community in the case of network server software.
|
|
13
|
+
|
|
14
|
+
The licenses for most software and other practical works are designed
|
|
15
|
+
to take away your freedom to share and change the works. By contrast,
|
|
16
|
+
our General Public Licenses are intended to guarantee your freedom to
|
|
17
|
+
share and change all versions of a program--to make sure it remains free
|
|
18
|
+
software for all its users.
|
|
19
|
+
|
|
20
|
+
When we speak of free software, we are referring to freedom, not
|
|
21
|
+
price. Our General Public Licenses are designed to make sure that you
|
|
22
|
+
have the freedom to distribute copies of free software (and charge for
|
|
23
|
+
them if you wish), that you receive source code or can modify it, that
|
|
24
|
+
you know you can do these rights, and that you have the freedom to use,
|
|
25
|
+
share, and change the software.
|
|
26
|
+
|
|
27
|
+
Specifically, the GNU Affero General Public License is designed to
|
|
28
|
+
ensure that anyone running a modified version of the software over a
|
|
29
|
+
network can receive the source code for that version.
|
|
30
|
+
|
|
31
|
+
For the full license text, see: https://www.gnu.org/licenses/agpl-3.0.txt
|
|
32
|
+
|
|
33
|
+
Copyright (C) 2024 Deepmodel Inc.
|
|
34
|
+
All Rights Reserved under AGPL-3.0.
|
|
35
|
+
|
|
36
|
+
Additional permissions: Deepmodel Inc. retains the right to release
|
|
37
|
+
the software under different license terms (dual licensing). See
|
|
38
|
+
CLA.md for contributor terms.
|
dmx/__init__.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""dmx — AI SDLC MCP server."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dmx.catalog import RuleDefinition, SkillArgument, SkillDefinition
|
|
6
|
+
from dmx.exceptions import DmxError, EmitterError, IdeDetectionError, RuleLoadError, SkillLoadError
|
|
7
|
+
from dmx.ide.emitters import IdeRuleFile
|
|
8
|
+
from dmx.server import create_app
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
# Exceptions
|
|
12
|
+
"DmxError",
|
|
13
|
+
"EmitterError",
|
|
14
|
+
"IdeDetectionError",
|
|
15
|
+
"RuleLoadError",
|
|
16
|
+
"SkillLoadError",
|
|
17
|
+
# Data classes
|
|
18
|
+
"IdeRuleFile",
|
|
19
|
+
"RuleDefinition",
|
|
20
|
+
"SkillArgument",
|
|
21
|
+
"SkillDefinition",
|
|
22
|
+
# Factory
|
|
23
|
+
"create_app",
|
|
24
|
+
]
|
dmx/_workflow_version.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""Workflow version — independent of the package version.
|
|
2
|
+
|
|
3
|
+
Bump this constant when skills or rules change in a way that requires
|
|
4
|
+
developers to re-run ``/dmx/init`` to get current behaviour.
|
|
5
|
+
|
|
6
|
+
Do NOT bump for: bug fixes, new IDE emitters, CLI changes, dependency updates.
|
|
7
|
+
DO bump for: new or removed skills, system-prompt rewrites, rule restructuring.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
WORKFLOW_VERSION = "workflow-v1"
|
dmx/catalog.py
ADDED
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
"""Skill and rule catalog: load, parse, and template substitution."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import re
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from pathlib import Path # noqa: TCH003 — used at runtime in rglob/stat calls
|
|
9
|
+
from typing import Any, cast
|
|
10
|
+
|
|
11
|
+
import frontmatter
|
|
12
|
+
|
|
13
|
+
from dmx.exceptions import RuleLoadError, SkillLoadError
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"ARG_NAME_RE",
|
|
17
|
+
"SLUG_RE",
|
|
18
|
+
"SkillArgument",
|
|
19
|
+
"SkillDefinition",
|
|
20
|
+
"RuleDefinition",
|
|
21
|
+
"load_skills",
|
|
22
|
+
"load_rules",
|
|
23
|
+
"substitute_args",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
logger = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
SLUG_RE = re.compile(r"^[a-z0-9_-]+$")
|
|
29
|
+
# Argument names must be valid Python identifiers (lowercase, underscores only).
|
|
30
|
+
# Hyphens are intentionally excluded: argument names are template placeholders
|
|
31
|
+
# and Python parameter names — hyphens are not valid in either context.
|
|
32
|
+
ARG_NAME_RE = re.compile(r"^[a-z][a-z0-9_]*$")
|
|
33
|
+
MAX_SKILL_BYTES = 256 * 1024 # 256 KiB
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass(frozen=True)
|
|
37
|
+
class SkillArgument:
|
|
38
|
+
"""A single argument accepted by a skill.
|
|
39
|
+
|
|
40
|
+
Attributes:
|
|
41
|
+
name: Argument name used in ``{{name}}`` template placeholders.
|
|
42
|
+
description: Human-readable description shown in IDE command palette.
|
|
43
|
+
required: Whether the argument must be provided at invocation time.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
name: str
|
|
47
|
+
description: str = ""
|
|
48
|
+
required: bool = False
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass(frozen=True)
|
|
52
|
+
class SkillDefinition:
|
|
53
|
+
"""A fully parsed skill loaded from a Markdown file.
|
|
54
|
+
|
|
55
|
+
Attributes:
|
|
56
|
+
name: Slug from frontmatter ``name`` field; falls back to filename stem.
|
|
57
|
+
title: Display name shown in IDE command palette.
|
|
58
|
+
description: Short description shown in IDE command palette.
|
|
59
|
+
arguments: Tuple of accepted arguments.
|
|
60
|
+
body: Raw Markdown body with ``{{arg}}`` placeholders.
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
name: str
|
|
64
|
+
title: str
|
|
65
|
+
description: str
|
|
66
|
+
arguments: tuple[SkillArgument, ...] = field(default_factory=tuple)
|
|
67
|
+
body: str = ""
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@dataclass(frozen=True)
|
|
71
|
+
class RuleDefinition:
|
|
72
|
+
"""A fully parsed rule loaded from a Markdown file.
|
|
73
|
+
|
|
74
|
+
Attributes:
|
|
75
|
+
name: Slug from frontmatter ``name`` field; falls back to filename stem.
|
|
76
|
+
title: Display name.
|
|
77
|
+
description: Short description shown in IDE rule picker.
|
|
78
|
+
always_apply: Whether the rule is always injected into context.
|
|
79
|
+
globs: Optional file-pattern filter (Cursor ``.mdc`` / Claude paths).
|
|
80
|
+
ides: Target IDEs; empty tuple means all targets.
|
|
81
|
+
body: Raw Markdown rule body.
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
name: str
|
|
85
|
+
title: str
|
|
86
|
+
description: str = ""
|
|
87
|
+
always_apply: bool = True
|
|
88
|
+
globs: str | None = None
|
|
89
|
+
ides: tuple[str, ...] = field(default_factory=tuple)
|
|
90
|
+
body: str = ""
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def load_skills(directory: Path) -> tuple[SkillDefinition, ...]:
|
|
94
|
+
"""Load all skill files from *directory* recursively.
|
|
95
|
+
|
|
96
|
+
Files that cannot be parsed are skipped with a warning; the rest are
|
|
97
|
+
returned. Files larger than ``MAX_SKILL_BYTES`` are also skipped.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
directory: Root directory to scan with ``rglob("*.md")``.
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
Tuple of parsed :class:`SkillDefinition` objects, sorted by name.
|
|
104
|
+
"""
|
|
105
|
+
skills: list[SkillDefinition] = []
|
|
106
|
+
for path in directory.rglob("*.md"):
|
|
107
|
+
try:
|
|
108
|
+
skill = _parse_skill(path)
|
|
109
|
+
except SkillLoadError as exc:
|
|
110
|
+
logger.warning("skipping skill — %s", exc)
|
|
111
|
+
continue
|
|
112
|
+
if skill is not None:
|
|
113
|
+
skills.append(skill)
|
|
114
|
+
return tuple(sorted(skills, key=lambda s: s.name))
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def load_rules(directory: Path) -> tuple[RuleDefinition, ...]:
|
|
118
|
+
"""Load all rule files from *directory* recursively.
|
|
119
|
+
|
|
120
|
+
Files that cannot be parsed are skipped with a warning.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
directory: Root directory to scan with ``rglob("*.md")``.
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
Tuple of parsed :class:`RuleDefinition` objects, sorted by name.
|
|
127
|
+
"""
|
|
128
|
+
rules: list[RuleDefinition] = []
|
|
129
|
+
for path in directory.rglob("*.md"):
|
|
130
|
+
try:
|
|
131
|
+
rule = _parse_rule(path)
|
|
132
|
+
except RuleLoadError as exc:
|
|
133
|
+
logger.warning("skipping rule — %s", exc)
|
|
134
|
+
continue
|
|
135
|
+
if rule is not None:
|
|
136
|
+
rules.append(rule)
|
|
137
|
+
return tuple(sorted(rules, key=lambda r: r.name))
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def substitute_args(template: str, args: dict[str, str | None]) -> str:
|
|
141
|
+
"""Substitute ``{{name}}`` placeholders in *template* with *args* values.
|
|
142
|
+
|
|
143
|
+
Unknown placeholders and those whose value is ``None`` resolve to an
|
|
144
|
+
empty string.
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
template: Markdown template containing ``{{name}}`` placeholders.
|
|
148
|
+
args: Mapping of argument name to value.
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
Template with all placeholders resolved.
|
|
152
|
+
"""
|
|
153
|
+
|
|
154
|
+
def _replace(match: re.Match[str]) -> str:
|
|
155
|
+
key = match.group(1).strip()
|
|
156
|
+
value = args.get(key)
|
|
157
|
+
return value if value is not None else ""
|
|
158
|
+
|
|
159
|
+
return re.sub(r"\{\{([^}]+)\}\}", _replace, template)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
# ---------------------------------------------------------------------------
|
|
163
|
+
# Private helpers
|
|
164
|
+
# ---------------------------------------------------------------------------
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _valid_arg_name(skill_name: str, arg_name: object) -> bool:
|
|
168
|
+
"""Return True if *arg_name* is a valid skill argument name.
|
|
169
|
+
|
|
170
|
+
Argument names must be lowercase Python identifiers (letters, digits,
|
|
171
|
+
underscores; must start with a letter). Hyphens are rejected: they are
|
|
172
|
+
not valid Python identifiers and cannot be used as template placeholders
|
|
173
|
+
or function parameter names without conversion gymnastics.
|
|
174
|
+
"""
|
|
175
|
+
if not isinstance(arg_name, str) or not ARG_NAME_RE.match(arg_name):
|
|
176
|
+
logger.warning(
|
|
177
|
+
"skill %r: argument name %r is invalid (must match %s) — skipping argument",
|
|
178
|
+
skill_name,
|
|
179
|
+
arg_name,
|
|
180
|
+
ARG_NAME_RE.pattern,
|
|
181
|
+
)
|
|
182
|
+
return False
|
|
183
|
+
return True
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _parse_skill(path: Path) -> SkillDefinition | None:
|
|
187
|
+
"""Parse a single skill file; return None and log a warning on failure."""
|
|
188
|
+
try:
|
|
189
|
+
if path.stat().st_size > MAX_SKILL_BYTES:
|
|
190
|
+
logger.warning("skill file exceeds size limit, skipping: %s", path)
|
|
191
|
+
return None
|
|
192
|
+
|
|
193
|
+
post = frontmatter.load(str(path))
|
|
194
|
+
name = str(post.metadata.get("name", path.stem))
|
|
195
|
+
|
|
196
|
+
if not SLUG_RE.match(name):
|
|
197
|
+
logger.warning("invalid skill name %r, skipping: %s", name, path)
|
|
198
|
+
return None
|
|
199
|
+
|
|
200
|
+
raw_args_value = post.metadata.get("arguments", [])
|
|
201
|
+
if not isinstance(raw_args_value, list):
|
|
202
|
+
logger.warning(
|
|
203
|
+
"skill %r: 'arguments' must be a list, got %s — ignoring arguments",
|
|
204
|
+
name,
|
|
205
|
+
type(raw_args_value).__name__,
|
|
206
|
+
)
|
|
207
|
+
raw_args_value = []
|
|
208
|
+
raw_args: list[dict[str, Any]] = cast("list[dict[str, Any]]", raw_args_value)
|
|
209
|
+
arguments = tuple(
|
|
210
|
+
SkillArgument(
|
|
211
|
+
name=str(arg["name"]),
|
|
212
|
+
description=str(arg.get("description", "")),
|
|
213
|
+
required=bool(arg.get("required", False)),
|
|
214
|
+
)
|
|
215
|
+
for arg in raw_args
|
|
216
|
+
if isinstance(arg, dict) and "name" in arg and _valid_arg_name(name, arg["name"])
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
return SkillDefinition(
|
|
220
|
+
name=name,
|
|
221
|
+
title=str(post.metadata.get("title", name)),
|
|
222
|
+
description=str(post.metadata.get("description", "")),
|
|
223
|
+
arguments=arguments,
|
|
224
|
+
body=post.content,
|
|
225
|
+
)
|
|
226
|
+
except Exception as exc: # noqa: BLE001
|
|
227
|
+
raise SkillLoadError(f"failed to load skill at {path}: {exc}", path=str(path)) from exc
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def _parse_rule(path: Path) -> RuleDefinition | None:
|
|
231
|
+
"""Parse a single rule file; return None and log a warning on failure."""
|
|
232
|
+
try:
|
|
233
|
+
post = frontmatter.load(str(path))
|
|
234
|
+
name = str(post.metadata.get("name", path.stem))
|
|
235
|
+
|
|
236
|
+
if not SLUG_RE.match(name):
|
|
237
|
+
logger.warning("invalid rule name %r, skipping: %s", name, path)
|
|
238
|
+
return None
|
|
239
|
+
|
|
240
|
+
raw_ides_value = post.metadata.get("ides", [])
|
|
241
|
+
# Normalize: YAML scalar string → list; e.g. `ides: cursor` → ["cursor"]
|
|
242
|
+
if isinstance(raw_ides_value, str):
|
|
243
|
+
raw_ides_value = [raw_ides_value]
|
|
244
|
+
raw_ides: list[str] = cast("list[str]", raw_ides_value or [])
|
|
245
|
+
ides = tuple(raw_ides)
|
|
246
|
+
|
|
247
|
+
globs_raw = post.metadata.get("globs")
|
|
248
|
+
globs: str | None = str(globs_raw) if globs_raw is not None else None
|
|
249
|
+
|
|
250
|
+
return RuleDefinition(
|
|
251
|
+
name=name,
|
|
252
|
+
title=str(post.metadata.get("title", name)),
|
|
253
|
+
description=str(post.metadata.get("description", "")),
|
|
254
|
+
always_apply=bool(post.metadata.get("alwaysApply", True)),
|
|
255
|
+
globs=globs,
|
|
256
|
+
ides=ides,
|
|
257
|
+
body=post.content,
|
|
258
|
+
)
|
|
259
|
+
except Exception as exc: # noqa: BLE001
|
|
260
|
+
raise RuleLoadError(f"failed to load rule at {path}: {exc}", path=str(path)) from exc
|