openprompt-lang 0.3.0 → 0.4.0
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.
- package/README.md +483 -32
- package/bin/cli.js +232 -41
- package/bin/create.js +135 -0
- package/bin/lint.js +20 -21
- package/docs/COMMANDS.md +200 -127
- package/docs/COMMITS/INDEX.md +1 -0
- package/docs/PROMPT_AI_CONTEXT.md +99 -0
- package/docs/langs/dotnet.md +36 -0
- package/docs/langs/java-spring.md +45 -0
- package/docs/langs/python-fastapi.md +35 -0
- package/docs/langs/unity.md +30 -0
- package/docs/langs/vue-nuxt.md +36 -0
- package/package.json +31 -3
- package/scaffolds/.cursorrules +6 -0
- package/scaffolds/AGENTS.md +27 -0
- package/scaffolds/Dockerfile +11 -0
- package/scaffolds/capacitor.config.ts +17 -0
- package/scaffolds/netlify.toml +8 -0
- package/scaffolds/prompt-lang.json +15 -0
- package/scaffolds/railway.json +12 -0
- package/scaffolds/tailwind.config.js +8 -0
- package/scaffolds/tauri.conf.json +26 -0
- package/schemas/language-module.json +116 -0
- package/schemas/prompt-lang.json +38 -3
- package/schemas/structures.json +145 -0
- package/src/ai/prompt-builder.js +184 -0
- package/src/ai/providers.js +247 -0
- package/src/annotations/registry.js +39 -0
- package/src/annotations/tags.json +24 -0
- package/src/commands/ai-gen.js +161 -0
- package/src/commands/component.js +242 -212
- package/src/commands/context.js +184 -109
- package/src/commands/extract.js +242 -0
- package/src/commands/figma.js +15 -15
- package/src/commands/init.js +197 -93
- package/src/commands/integrate.js +406 -0
- package/src/commands/lang.js +148 -0
- package/src/commands/qa-gen.js +139 -0
- package/src/commands/scaffold.js +127 -0
- package/src/commands/suggest.js +24 -14
- package/src/commands/teach.js +110 -0
- package/src/commands/validate.js +143 -83
- package/src/commands/wizard.js +456 -0
- package/src/generators/figma-prompt.js +20 -12
- package/src/language-service/plugin.cjs +94 -0
- package/src/language-service/plugin.d.ts +6 -0
- package/src/mcp-server.js +605 -0
- package/src/templates/langs/react/INDEX.json +262 -0
- package/src/templates/langs/react/MODULE.json +166 -0
- package/src/templates/langs/react/templates/hooks/useAuth.template.ts +134 -0
- package/src/templates/langs/react/templates/hooks/useDebounce.template.ts +45 -0
- package/src/templates/langs/react/templates/hooks/useForm.template.ts +146 -0
- package/src/templates/langs/react/templates/hooks/usePagination.template.ts +108 -0
- package/src/templates/langs/react/templates/services/apiService.template.ts +123 -0
- package/src/templates/langs/react/templates/ui/Button.template.tsx +87 -0
- package/src/templates/langs/react/templates/ui/Card.template.tsx +85 -0
- package/src/templates/langs/react/templates/ui/DataTable.template.tsx +163 -0
- package/src/templates/langs/react/templates/ui/Input.template.tsx +96 -0
- package/src/templates/langs/react/templates/ui/Modal.template.tsx +133 -0
- package/src/templates/langs/react/templates/ui/Select.template.tsx +99 -0
- package/src/templates/langs/vue/INDEX.json +246 -0
- package/src/templates/langs/vue/MODULE.json +105 -0
- package/src/templates/langs/vue/templates/composables/useAuth.template.ts +106 -0
- package/src/templates/langs/vue/templates/composables/useDebounce.template.ts +47 -0
- package/src/templates/langs/vue/templates/composables/useFetch.template.ts +54 -0
- package/src/templates/langs/vue/templates/composables/useForm.template.ts +127 -0
- package/src/templates/langs/vue/templates/composables/usePagination.template.ts +98 -0
- package/src/templates/langs/vue/templates/services/apiService.template.ts +116 -0
- package/src/templates/langs/vue/templates/ui/Button.template.vue +79 -0
- package/src/templates/langs/vue/templates/ui/Card.template.vue +73 -0
- package/src/templates/langs/vue/templates/ui/DataTable.template.vue +115 -0
- package/src/templates/langs/vue/templates/ui/Input.template.vue +70 -0
- package/src/templates/langs/vue/templates/ui/Modal.template.vue +112 -0
- package/src/templates/langs/vue/templates/ui/Select.template.vue +77 -0
- package/src/templates/scripts/log-actividad.sh +32 -0
- package/src/templates/scripts/log-commit.sh +35 -0
- package/src/templates/scripts/log-error.sh +45 -0
- package/src/templates/scripts/validate.sh +23 -0
- package/src/ts-transformer/index.cjs +86 -0
- package/src/utils/ai.js +35 -53
- package/src/utils/annotations.js +260 -214
- package/src/utils/config.js +61 -13
- package/src/utils/error-learner.js +203 -0
- package/src/utils/file-utils.js +119 -0
- package/src/utils/language-loader.js +167 -0
- package/src/utils/template-utils.js +45 -0
- package/src/vite-plugin/index.js +54 -0
- package/vscode-extension/package.json +23 -2
- package/vscode-extension/snippets/promptlang.json +1 -3
- package/vscode-extension/syntaxes/annotations-code.tmGrammar.json +15 -0
|
@@ -0,0 +1,605 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { readFileSync, readdirSync, existsSync, statSync } from "fs"
|
|
4
|
+
import { join, extname } from "path"
|
|
5
|
+
import { lintFile } from "./utils/annotations.js"
|
|
6
|
+
import { loadConfig } from "./utils/config.js"
|
|
7
|
+
import {
|
|
8
|
+
searchTemplates,
|
|
9
|
+
getTemplate,
|
|
10
|
+
getLanguage,
|
|
11
|
+
getLanguageIndex,
|
|
12
|
+
} from "./utils/language-loader.js"
|
|
13
|
+
import { scanForLearnErrors, generateTests, writeTestFile } from "./utils/error-learner.js"
|
|
14
|
+
import { scaffoldFolders } from "./commands/scaffold.js"
|
|
15
|
+
import { extractLesson, extractTags } from "./utils/template-utils.js"
|
|
16
|
+
import { findAllFiles, countFiles } from "./utils/file-utils.js"
|
|
17
|
+
|
|
18
|
+
const SERVER_INFO = {
|
|
19
|
+
name: "openPrompt-Lang",
|
|
20
|
+
version: "0.3.0",
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const TOOLS = [
|
|
24
|
+
{
|
|
25
|
+
name: "validate",
|
|
26
|
+
description:
|
|
27
|
+
"[PRE-COMMIT CHECK] Scan project for annotation errors. Use BEFORE commits or when troubleshooting annotation warnings. Returns file-by-file errors and warnings. If errors found, use lint_file on specific files to debug.",
|
|
28
|
+
inputSchema: {
|
|
29
|
+
type: "object",
|
|
30
|
+
properties: {
|
|
31
|
+
filePath: { type: "string", description: "Path to file to validate" },
|
|
32
|
+
dir: { type: "string", description: "Directory to scan for files (default: cwd)" },
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
name: "lint_file",
|
|
38
|
+
description:
|
|
39
|
+
"[FILE LINTING] Debug a single file's annotations in detail. Returns parsed annotations, errors, and warnings. More detailed than validate for individual files. Use strict mode for thorough checking.",
|
|
40
|
+
inputSchema: {
|
|
41
|
+
type: "object",
|
|
42
|
+
properties: {
|
|
43
|
+
filePath: { type: "string", description: "Path to the source file" },
|
|
44
|
+
strict: { type: "boolean", description: "Enable strict mode (warnings become errors)" },
|
|
45
|
+
},
|
|
46
|
+
required: ["filePath"],
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
name: "parse_code",
|
|
51
|
+
description:
|
|
52
|
+
"[QUICK PARSE] Validate annotation syntax inline without reading from disk. Use during code generation or editing to verify annotation syntax before writing to file.",
|
|
53
|
+
inputSchema: {
|
|
54
|
+
type: "object",
|
|
55
|
+
properties: {
|
|
56
|
+
code: { type: "string", description: "Source code containing @annotations" },
|
|
57
|
+
},
|
|
58
|
+
required: ["code"],
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
name: "context",
|
|
63
|
+
description:
|
|
64
|
+
"[CONTEXT EXTRACTION] Extract project structure with config summary and annotated file list. Use when you need a bird's-eye view of the project or before generating documentation.",
|
|
65
|
+
inputSchema: {
|
|
66
|
+
type: "object",
|
|
67
|
+
properties: {
|
|
68
|
+
dir: { type: "string", description: "Project directory (default: cwd)" },
|
|
69
|
+
scope: { type: "string", description: "Filter by @scope module name" },
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
name: "search_templates",
|
|
75
|
+
description:
|
|
76
|
+
"[TEMPLATE DISCOVERY] Find relevant templates before using teach_template. Searches by keyword (button, auth, fetch, form). Returns matching templates with descriptions and metadata.",
|
|
77
|
+
inputSchema: {
|
|
78
|
+
type: "object",
|
|
79
|
+
properties: {
|
|
80
|
+
query: { type: "string", description: "Search query" },
|
|
81
|
+
lang: { type: "string", description: "Language module (react, vue)" },
|
|
82
|
+
},
|
|
83
|
+
required: ["query"],
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
name: "generate_tests",
|
|
88
|
+
description:
|
|
89
|
+
"[TEST GENERATION] Generate Vitest test cases from @learn-error annotations. AFTER documenting a bug with @learn-error, run this to create regression tests. Optionally write to file with output parameter.",
|
|
90
|
+
inputSchema: {
|
|
91
|
+
type: "object",
|
|
92
|
+
properties: {
|
|
93
|
+
sourceDir: { type: "string", description: "Source directory to scan (default: cwd)" },
|
|
94
|
+
lang: { type: "string", description: "Language module (react, vue)" },
|
|
95
|
+
output: { type: "string", description: "Output path for generated test file (optional)" },
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
name: "teach_template",
|
|
101
|
+
description:
|
|
102
|
+
"[LEARN PATTERNS] Show structured lesson and code from a template. Use when implementing a known pattern (button, auth, form, fetch). Takes templateId like 'button-shadcn' or 'hook-useAuth'. Returns lesson, best practices, fix history, and optionally the code. Run search_templates first to find relevant templates.",
|
|
103
|
+
inputSchema: {
|
|
104
|
+
type: "object",
|
|
105
|
+
properties: {
|
|
106
|
+
templateId: {
|
|
107
|
+
type: "string",
|
|
108
|
+
description: "Template ID to teach (e.g. button-shadcn, hook-useAuth)",
|
|
109
|
+
},
|
|
110
|
+
lang: { type: "string", description: "Language module (react, vue)" },
|
|
111
|
+
showCode: { type: "boolean", description: "Include full template code in output" },
|
|
112
|
+
},
|
|
113
|
+
required: ["templateId"],
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
name: "analyze_project",
|
|
118
|
+
description:
|
|
119
|
+
"[PROJECT AUDIT] First tool to run when starting work on an unknown project. Returns config summary, file counts by type, and annotation stats. Use to understand project structure before making changes.",
|
|
120
|
+
inputSchema: {
|
|
121
|
+
type: "object",
|
|
122
|
+
properties: {
|
|
123
|
+
dir: { type: "string", description: "Project directory (default: cwd)" },
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
name: "scaffold_project",
|
|
129
|
+
description:
|
|
130
|
+
"[PROJECT SETUP] Create project folder structure and scaffold files. Use when initializing a new project or adding a module. Takes framework id (react, vue). Creates folders, .gitignore, prompt-lang.json, AGENTS.md.",
|
|
131
|
+
inputSchema: {
|
|
132
|
+
type: "object",
|
|
133
|
+
properties: {
|
|
134
|
+
framework: { type: "string", description: "Framework/language id (react, vue)" },
|
|
135
|
+
name: { type: "string", description: "Project subdirectory name (optional)" },
|
|
136
|
+
force: { type: "boolean", description: "Overwrite existing files" },
|
|
137
|
+
},
|
|
138
|
+
required: ["framework"],
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
]
|
|
142
|
+
|
|
143
|
+
function sendMessage(msg) {
|
|
144
|
+
const data = JSON.stringify(msg)
|
|
145
|
+
process.stdout.write(data + "\n")
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function sendError(id, code, message) {
|
|
149
|
+
sendMessage({ jsonrpc: "2.0", id, error: { code, message } })
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function sendResult(id, result) {
|
|
153
|
+
sendMessage({ jsonrpc: "2.0", id, result })
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function formatResult(type, summary, details, actions) {
|
|
157
|
+
const parts = [`## Result: ${type}`, "", summary]
|
|
158
|
+
if (details && details.length) {
|
|
159
|
+
parts.push("", "### Details", ...details.map((d) => `- ${d}`))
|
|
160
|
+
}
|
|
161
|
+
if (actions && actions.length) {
|
|
162
|
+
parts.push("", "### Recommended Actions", ...actions.map((a) => `- ${a}`))
|
|
163
|
+
}
|
|
164
|
+
return parts.join("\n")
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function toolResult(text, isError = false) {
|
|
168
|
+
return { content: [{ type: "text", text }], isError }
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async function handleToolCall(name, args) {
|
|
172
|
+
switch (name) {
|
|
173
|
+
case "validate": {
|
|
174
|
+
const dir = args?.dir || args?.filePath || process.cwd()
|
|
175
|
+
const target = join(process.cwd(), dir)
|
|
176
|
+
if (!existsSync(target)) return toolResult(`Path not found: ${target}`, true)
|
|
177
|
+
|
|
178
|
+
// If target is a directory, scan for annotated files
|
|
179
|
+
if (statSync(target).isDirectory()) {
|
|
180
|
+
const allFiles = []
|
|
181
|
+
function walk(d) {
|
|
182
|
+
let entries
|
|
183
|
+
try {
|
|
184
|
+
entries = readdirSync(d, { withFileTypes: true })
|
|
185
|
+
} catch {
|
|
186
|
+
return
|
|
187
|
+
}
|
|
188
|
+
for (const e of entries) {
|
|
189
|
+
if (e.name.startsWith(".") || e.name === "node_modules" || e.name === "dist") continue
|
|
190
|
+
const p = join(d, e.name)
|
|
191
|
+
if (e.isDirectory()) walk(p)
|
|
192
|
+
else if (e.isFile() && /\.(ts|tsx|js|jsx)$/.test(e.name)) allFiles.push(p)
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
walk(target)
|
|
196
|
+
if (allFiles.length === 0)
|
|
197
|
+
return toolResult(
|
|
198
|
+
formatResult(
|
|
199
|
+
"info",
|
|
200
|
+
"No source files found in directory.",
|
|
201
|
+
[`Scanned: ${target}`],
|
|
202
|
+
["Run scaffold_project to initialize project structure."]
|
|
203
|
+
)
|
|
204
|
+
)
|
|
205
|
+
let totalAnn = 0,
|
|
206
|
+
totalErr = 0,
|
|
207
|
+
totalWarn = 0
|
|
208
|
+
const details = []
|
|
209
|
+
for (const f of allFiles) {
|
|
210
|
+
const code = readFileSync(f, "utf-8")
|
|
211
|
+
const result = lintFile(code)
|
|
212
|
+
totalAnn += result.annotations.length
|
|
213
|
+
totalErr += result.errors.length
|
|
214
|
+
totalWarn += result.warnings.length
|
|
215
|
+
if (result.errors.length || result.warnings.length) {
|
|
216
|
+
const rel = f.replace(target + "/", "")
|
|
217
|
+
for (const e of result.errors) details.push(`❌ ${rel}: ${e}`)
|
|
218
|
+
for (const w of result.warnings) details.push(`⚠ ${rel}: ${w}`)
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
const status = totalErr > 0 ? "error" : totalWarn > 0 ? "warning" : "success"
|
|
222
|
+
const actions = []
|
|
223
|
+
if (totalErr > 0) actions.push("Fix errors listed above, then run validate again")
|
|
224
|
+
if (totalWarn > 0) actions.push("Review warnings — some may indicate potential issues")
|
|
225
|
+
if (totalErr === 0 && totalWarn === 0)
|
|
226
|
+
actions.push("Project annotations are valid. Ready for commit.")
|
|
227
|
+
return toolResult(
|
|
228
|
+
formatResult(
|
|
229
|
+
status,
|
|
230
|
+
`Scanned ${allFiles.length} files: ${totalAnn} annotations, ${totalErr} errors, ${totalWarn} warnings`,
|
|
231
|
+
details.length ? details : [`✅ All annotations valid across ${allFiles.length} files`],
|
|
232
|
+
actions
|
|
233
|
+
)
|
|
234
|
+
)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const result = lintFile(readFileSync(target, "utf-8"))
|
|
238
|
+
const status =
|
|
239
|
+
result.errors.length > 0 ? "error" : result.warnings.length > 0 ? "warning" : "success"
|
|
240
|
+
const actions = []
|
|
241
|
+
if (result.errors.length > 0) actions.push("Fix errors, then re-run validate on this file")
|
|
242
|
+
if (result.warnings.length > 0) actions.push("Review warnings before committing")
|
|
243
|
+
if (result.errors.length === 0 && result.warnings.length === 0)
|
|
244
|
+
actions.push("File is valid. No issues found.")
|
|
245
|
+
return toolResult(
|
|
246
|
+
formatResult(
|
|
247
|
+
status,
|
|
248
|
+
`File: ${args?.filePath || target}: ${result.annotations.length} annotations, ${result.errors.length} errors, ${result.warnings.length} warnings`,
|
|
249
|
+
[...result.errors.map((e) => `❌ ${e}`), ...result.warnings.map((w) => `⚠ ${w}`)],
|
|
250
|
+
actions
|
|
251
|
+
)
|
|
252
|
+
)
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
case "lint_file": {
|
|
256
|
+
const filePath = join(process.cwd(), args.filePath)
|
|
257
|
+
if (!existsSync(filePath))
|
|
258
|
+
return toolResult(
|
|
259
|
+
formatResult(
|
|
260
|
+
"error",
|
|
261
|
+
"File not found",
|
|
262
|
+
[`Path: ${filePath}`],
|
|
263
|
+
["Verify the file path and try again"]
|
|
264
|
+
),
|
|
265
|
+
true
|
|
266
|
+
)
|
|
267
|
+
const code = readFileSync(filePath, "utf-8")
|
|
268
|
+
const result = lintFile(code, args.strict)
|
|
269
|
+
const status =
|
|
270
|
+
result.errors.length > 0 ? "error" : result.warnings.length > 0 ? "warning" : "success"
|
|
271
|
+
return toolResult(
|
|
272
|
+
formatResult(
|
|
273
|
+
status,
|
|
274
|
+
`${args.filePath}: ${result.annotations.length} annotations`,
|
|
275
|
+
[
|
|
276
|
+
...result.annotations.map((a) => `@${a.name}(${a.raw})`),
|
|
277
|
+
...result.errors.map((e) => `❌ ${e}`),
|
|
278
|
+
...result.warnings.map((w) => `⚠ ${w}`),
|
|
279
|
+
],
|
|
280
|
+
result.errors.length
|
|
281
|
+
? ["Fix errors and re-run lint_file"]
|
|
282
|
+
: result.warnings.length
|
|
283
|
+
? ["Review warnings"]
|
|
284
|
+
: ["File is valid"]
|
|
285
|
+
)
|
|
286
|
+
)
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
case "parse_code": {
|
|
290
|
+
if (!args?.code)
|
|
291
|
+
return toolResult(
|
|
292
|
+
formatResult(
|
|
293
|
+
"error",
|
|
294
|
+
"No code provided",
|
|
295
|
+
[],
|
|
296
|
+
["Pass a 'code' parameter with annotation source text"]
|
|
297
|
+
),
|
|
298
|
+
true
|
|
299
|
+
)
|
|
300
|
+
const result = lintFile(args.code)
|
|
301
|
+
const status =
|
|
302
|
+
result.errors.length > 0 ? "error" : result.warnings.length > 0 ? "warning" : "success"
|
|
303
|
+
return toolResult(
|
|
304
|
+
formatResult(
|
|
305
|
+
status,
|
|
306
|
+
`${result.annotations.length} annotations found`,
|
|
307
|
+
[
|
|
308
|
+
...result.annotations.map((a) => `@${a.name}(${a.raw})`),
|
|
309
|
+
...result.errors.map((e) => `❌ ${e}`),
|
|
310
|
+
...result.warnings.map((w) => `⚠ ${w}`),
|
|
311
|
+
],
|
|
312
|
+
result.errors.length ? ["Fix annotation syntax"] : ["Annotation syntax is valid"]
|
|
313
|
+
)
|
|
314
|
+
)
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
case "context": {
|
|
318
|
+
const dir = args?.dir || process.cwd()
|
|
319
|
+
const cwd = join(process.cwd(), dir)
|
|
320
|
+
let config = null
|
|
321
|
+
try {
|
|
322
|
+
config = loadConfig(cwd)
|
|
323
|
+
} catch {}
|
|
324
|
+
const details = [`Directory: ${dir}`, `Config: ${config ? "found" : "none"}`]
|
|
325
|
+
if (config) {
|
|
326
|
+
details.push(
|
|
327
|
+
`Name: ${config.name || "unnamed"}`,
|
|
328
|
+
`Language: ${config.lenguaje || "unknown"}`,
|
|
329
|
+
`Stack: ${(config.stack?.base || []).join(", ")}`
|
|
330
|
+
)
|
|
331
|
+
}
|
|
332
|
+
const actions = config
|
|
333
|
+
? ["Run validate to check annotation health"]
|
|
334
|
+
: ["Run init or scaffold_project to create project config"]
|
|
335
|
+
return toolResult(formatResult("success", "Project context extracted", details, actions))
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
case "search_templates": {
|
|
339
|
+
const lang = args?.lang || "react"
|
|
340
|
+
const query = args?.query || ""
|
|
341
|
+
const results = searchTemplates(query, { lang })
|
|
342
|
+
if (!results.length)
|
|
343
|
+
return toolResult(
|
|
344
|
+
formatResult(
|
|
345
|
+
"info",
|
|
346
|
+
`No templates match "${query}" in ${lang}`,
|
|
347
|
+
[],
|
|
348
|
+
["Try a different query or check available templates with lang list"]
|
|
349
|
+
)
|
|
350
|
+
)
|
|
351
|
+
return toolResult(
|
|
352
|
+
formatResult(
|
|
353
|
+
"success",
|
|
354
|
+
`${results.length} templates found for "${query}" in ${lang}`,
|
|
355
|
+
results.map(
|
|
356
|
+
(t, i) => `${i + 1}. ${t.id}: ${t.description || "no description"} — ${t.category}`
|
|
357
|
+
),
|
|
358
|
+
[
|
|
359
|
+
"Run teach_template with a template ID to learn more",
|
|
360
|
+
"Use showCode=true in teach_template to see the implementation",
|
|
361
|
+
]
|
|
362
|
+
)
|
|
363
|
+
)
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
case "generate_tests": {
|
|
367
|
+
const sourceDir = args?.sourceDir || process.cwd()
|
|
368
|
+
const lang = args?.lang || "react"
|
|
369
|
+
const dir = join(process.cwd(), sourceDir)
|
|
370
|
+
if (!existsSync(dir))
|
|
371
|
+
return toolResult(formatResult("error", "Directory not found", [`Path: ${dir}`], []), true)
|
|
372
|
+
const errors = scanForLearnErrors(dir, lang)
|
|
373
|
+
if (!errors.length)
|
|
374
|
+
return toolResult(
|
|
375
|
+
formatResult(
|
|
376
|
+
"info",
|
|
377
|
+
"No @learn-error annotations found",
|
|
378
|
+
[`Scanned: ${dir}`],
|
|
379
|
+
["After fixing a bug, add @learn-error to the file and re-run generate_tests"]
|
|
380
|
+
)
|
|
381
|
+
)
|
|
382
|
+
const tests = generateTests(errors, lang)
|
|
383
|
+
const details = [`Learned errors found: ${errors.length}`, `Tests generated: ${tests.length}`]
|
|
384
|
+
if (args?.output) {
|
|
385
|
+
const outPath = join(process.cwd(), args.output)
|
|
386
|
+
writeTestFile(errors, outPath, lang)
|
|
387
|
+
details.push(`Written to: ${args.output}`)
|
|
388
|
+
}
|
|
389
|
+
const actions = [
|
|
390
|
+
args?.output
|
|
391
|
+
? `Run: npx vitest run ${args.output}`
|
|
392
|
+
: "Pass 'output' parameter to write test file to disk",
|
|
393
|
+
]
|
|
394
|
+
return toolResult(
|
|
395
|
+
formatResult(
|
|
396
|
+
"success",
|
|
397
|
+
`Generated ${tests.length} tests from ${errors.length} learned errors`,
|
|
398
|
+
[...details, "", ...tests],
|
|
399
|
+
actions
|
|
400
|
+
)
|
|
401
|
+
)
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
case "teach_template": {
|
|
405
|
+
const lang = args?.lang || "react"
|
|
406
|
+
const template = getTemplate(args.templateId, lang)
|
|
407
|
+
if (!template)
|
|
408
|
+
return toolResult(
|
|
409
|
+
formatResult(
|
|
410
|
+
"error",
|
|
411
|
+
`Template "${args.templateId}" not found in ${lang}`,
|
|
412
|
+
[],
|
|
413
|
+
[`Run search_templates with keywords to find available templates in ${lang}`]
|
|
414
|
+
),
|
|
415
|
+
true
|
|
416
|
+
)
|
|
417
|
+
const langObj = getLanguage(lang)
|
|
418
|
+
const meta = template.metadata
|
|
419
|
+
const details = [
|
|
420
|
+
`${meta.name}: ${meta.description || ""}`,
|
|
421
|
+
`Category: ${meta.category}`,
|
|
422
|
+
`Purposes: ${meta.purposes?.join(", ") || "general"}`,
|
|
423
|
+
]
|
|
424
|
+
if (meta.variants?.length) details.push(`Variants: ${meta.variants.join(", ")}`)
|
|
425
|
+
if (meta.sizes?.length) details.push(`Sizes: ${meta.sizes.join(", ")}`)
|
|
426
|
+
details.push(
|
|
427
|
+
`Dark mode: ${meta.darkMode ? "yes" : "no"}`,
|
|
428
|
+
`Responsive: ${meta.responsive ? "yes" : "no"}`,
|
|
429
|
+
`Deps: ${meta.dependencies?.join(", ") || "none"}`
|
|
430
|
+
)
|
|
431
|
+
if (meta.hasTeachMe) {
|
|
432
|
+
const lesson = extractLesson(template.content)
|
|
433
|
+
if (lesson) details.push("", "--- LESSON ---", lesson)
|
|
434
|
+
}
|
|
435
|
+
const goodP = extractTags(template.content, "@goodPractice")
|
|
436
|
+
const badP = extractTags(template.content, "@badPractice")
|
|
437
|
+
if (goodP.length) details.push("", "--- GOOD PRACTICES ---", ...goodP.map((g) => `✅ ${g}`))
|
|
438
|
+
if (badP.length) details.push("", "--- BAD PRACTICES ---", ...badP.map((b) => `❌ ${b}`))
|
|
439
|
+
if (args?.showCode) {
|
|
440
|
+
const clean = template.content
|
|
441
|
+
.split("\n")
|
|
442
|
+
.filter((l) => !l.trim().startsWith("// @"))
|
|
443
|
+
.join("\n")
|
|
444
|
+
details.push("", "--- CODE ---", clean)
|
|
445
|
+
}
|
|
446
|
+
const actions = [
|
|
447
|
+
"Use this pattern in your project",
|
|
448
|
+
`Run scaffold_project to set up the project structure`,
|
|
449
|
+
]
|
|
450
|
+
return toolResult(
|
|
451
|
+
formatResult("success", `${meta.name} — ${langObj?.name || lang}`, details, actions)
|
|
452
|
+
)
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
case "analyze_project": {
|
|
456
|
+
const dir = args?.dir || process.cwd()
|
|
457
|
+
const cwd = join(process.cwd(), dir)
|
|
458
|
+
if (!existsSync(cwd))
|
|
459
|
+
return toolResult(formatResult("error", "Directory not found", [`Path: ${cwd}`], []), true)
|
|
460
|
+
let config = null
|
|
461
|
+
try {
|
|
462
|
+
config = loadConfig(cwd)
|
|
463
|
+
} catch {}
|
|
464
|
+
const counts = countFiles(cwd)
|
|
465
|
+
const allFiles = findAllFiles(cwd)
|
|
466
|
+
const annResults = []
|
|
467
|
+
for (const f of allFiles) {
|
|
468
|
+
try {
|
|
469
|
+
const code = readFileSync(f, "utf-8")
|
|
470
|
+
const r = lintFile(code)
|
|
471
|
+
if (r.annotations.length || r.errors.length || r.warnings.length) {
|
|
472
|
+
annResults.push({ file: f.replace(cwd + "/", ""), ...r })
|
|
473
|
+
}
|
|
474
|
+
} catch {}
|
|
475
|
+
}
|
|
476
|
+
const details = [
|
|
477
|
+
`Config: ${config ? "found" : "none"}`,
|
|
478
|
+
`Name: ${config?.name || "unnamed"}`,
|
|
479
|
+
`Language: ${config?.lenguaje || "unknown"}`,
|
|
480
|
+
`Stack: ${(config?.stack?.base || []).join(", ")}`,
|
|
481
|
+
"",
|
|
482
|
+
`Total files: ${counts.total}`,
|
|
483
|
+
`.ts: ${counts.ts} | .tsx: ${counts.tsx} | .js: ${counts.js} | .jsx: ${counts.jsx} | .css: ${counts.css}`,
|
|
484
|
+
"",
|
|
485
|
+
`Files with annotations: ${annResults.length}`,
|
|
486
|
+
...annResults.map(
|
|
487
|
+
(r) =>
|
|
488
|
+
`${r.file}: ${r.annotations.length} annotations, ${r.errors.length} errors, ${r.warnings.length} warnings`
|
|
489
|
+
),
|
|
490
|
+
]
|
|
491
|
+
const actions = config
|
|
492
|
+
? [
|
|
493
|
+
"Run validate to check annotation consistency",
|
|
494
|
+
"Run search_templates to find relevant patterns",
|
|
495
|
+
]
|
|
496
|
+
: ["Run init to set up project configuration"]
|
|
497
|
+
return toolResult(formatResult("success", `Project analysis: ${dir}`, details, actions))
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
case "scaffold_project": {
|
|
501
|
+
const output = await scaffoldFolders(args.framework, {
|
|
502
|
+
name: args?.name,
|
|
503
|
+
force: args?.force,
|
|
504
|
+
})
|
|
505
|
+
if (!output)
|
|
506
|
+
return toolResult(
|
|
507
|
+
formatResult(
|
|
508
|
+
"error",
|
|
509
|
+
`Failed to scaffold "${args.framework}"`,
|
|
510
|
+
[],
|
|
511
|
+
["Verify the framework id is valid (react, vue)"]
|
|
512
|
+
),
|
|
513
|
+
true
|
|
514
|
+
)
|
|
515
|
+
const actions = [
|
|
516
|
+
"Run validate to verify structure",
|
|
517
|
+
"Run search_templates to find relevant patterns for your first component",
|
|
518
|
+
]
|
|
519
|
+
return toolResult(
|
|
520
|
+
formatResult(
|
|
521
|
+
"success",
|
|
522
|
+
`Scaffold "${args.framework}" created`,
|
|
523
|
+
[`Location: ${output}`],
|
|
524
|
+
actions
|
|
525
|
+
)
|
|
526
|
+
)
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
default:
|
|
530
|
+
return toolResult(`Unknown tool: ${name}`, true)
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
export function startServer() {
|
|
535
|
+
let buffer = ""
|
|
536
|
+
|
|
537
|
+
process.stdin.setEncoding("utf-8")
|
|
538
|
+
process.stdin.on("data", async (chunk) => {
|
|
539
|
+
buffer += chunk
|
|
540
|
+
const lines = buffer.split("\n")
|
|
541
|
+
buffer = lines.pop() || ""
|
|
542
|
+
|
|
543
|
+
for (const line of lines) {
|
|
544
|
+
const trimmed = line.trim()
|
|
545
|
+
if (!trimmed) continue
|
|
546
|
+
|
|
547
|
+
let msg
|
|
548
|
+
try {
|
|
549
|
+
msg = JSON.parse(trimmed)
|
|
550
|
+
} catch {
|
|
551
|
+
continue
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
if (msg.jsonrpc !== "2.0") continue
|
|
555
|
+
|
|
556
|
+
const { id, method, params } = msg
|
|
557
|
+
|
|
558
|
+
switch (method) {
|
|
559
|
+
case "initialize": {
|
|
560
|
+
sendResult(id, {
|
|
561
|
+
protocolVersion: "1.0",
|
|
562
|
+
capabilities: { tools: {} },
|
|
563
|
+
serverInfo: SERVER_INFO,
|
|
564
|
+
})
|
|
565
|
+
break
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
case "tools/list": {
|
|
569
|
+
sendResult(id, { tools: TOOLS })
|
|
570
|
+
break
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
case "tools/call": {
|
|
574
|
+
try {
|
|
575
|
+
const result = await handleToolCall(params.name, params.arguments)
|
|
576
|
+
sendResult(id, result)
|
|
577
|
+
} catch (err) {
|
|
578
|
+
sendError(id, -1, err.message)
|
|
579
|
+
}
|
|
580
|
+
break
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
case "shutdown": {
|
|
584
|
+
sendResult(id, null)
|
|
585
|
+
process.exitCode = 0
|
|
586
|
+
process.stdout.once("drain", () => process.exit(0))
|
|
587
|
+
// Force exit after 1s even if drain never fires
|
|
588
|
+
setTimeout(() => process.exit(0), 1000).unref()
|
|
589
|
+
break
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
default:
|
|
593
|
+
sendError(id, -32601, `Method not found: ${method}`)
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
})
|
|
597
|
+
|
|
598
|
+
process.stdin.on("end", () => process.exit(0))
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// Auto-start when executed directly, not when imported
|
|
602
|
+
const isDirectRun =
|
|
603
|
+
process.argv[1] &&
|
|
604
|
+
(process.argv[1].endsWith("mcp-server.js") || process.argv[1].endsWith("mcp-server"))
|
|
605
|
+
if (isDirectRun) startServer()
|