openprompt-lang 1.2.4 → 1.2.7

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.
@@ -1,1913 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import {
4
- readFileSync,
5
- writeFileSync,
6
- readdirSync,
7
- existsSync,
8
- statSync,
9
- rmSync,
10
- mkdirSync,
11
- } from "fs"
12
- import { join, extname, dirname } from "path"
13
- import { fileURLToPath } from "url"
14
- import { lintFile } from "./utils/annotations.js"
15
- import { loadConfig, getDefaultConfig } from "./utils/config.js"
16
- import {
17
- searchTemplates,
18
- getTemplate,
19
- getLanguage,
20
- getLanguageIndex,
21
- getLanguages,
22
- } from "./utils/language-loader.js"
23
- import { scanForLearnErrors, generateTests, writeTestFile } from "./utils/error-learner.js"
24
- import { scaffoldFolders } from "./commands/scaffold.js"
25
- import { extractLesson, extractTags } from "./utils/template-utils.js"
26
- import { findAllFiles, countFiles } from "./utils/file-utils.js"
27
- import {
28
- searchKnowledge,
29
- knowledgeList as kList,
30
- knowledgeRead,
31
- knowledgeConcept,
32
- } from "./utils/knowledge-search.js"
33
- import { searchKnowledge as kSearchTemplates } from "./utils/knowledge-search.js"
34
- import {
35
- loadDomains,
36
- classifyBook,
37
- getBooksByDomain,
38
- expandConcept,
39
- searchByDomain,
40
- listAllDomainsWithInfo,
41
- } from "./utils/knowledge-taxonomy.js"
42
- import {
43
- getSessionState,
44
- saveSessionState,
45
- recordToolCall,
46
- setActiveDomain,
47
- markValidated,
48
- setPhase,
49
- setPlanFile,
50
- } from "./mcp-session.js"
51
- import {
52
- checkPreconditions,
53
- prepareToolResponse,
54
- generateRichInstructions,
55
- shouldAutoTicket,
56
- } from "./mcp-workflow.js"
57
-
58
- const __filename = fileURLToPath(import.meta.url)
59
- const __dirname = dirname(__filename)
60
-
61
- function getServerVersion() {
62
- try {
63
- const pkg = JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf-8"))
64
- return pkg.version || "0.9.0"
65
- } catch {
66
- return "0.9.0"
67
- }
68
- }
69
-
70
- const SUPPORTED_PROTOCOL_VERSIONS = ["2024-11-05"]
71
- const LATEST_PROTOCOL_VERSION = SUPPORTED_PROTOCOL_VERSIONS[0]
72
-
73
- const SERVER_INFO = {
74
- name: "OPL",
75
- version: getServerVersion(),
76
- }
77
-
78
- const TOOLS = [
79
- {
80
- name: "validate",
81
- description:
82
- "[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.",
83
- inputSchema: {
84
- type: "object",
85
- properties: {
86
- filePath: { type: "string", description: "Path to file to validate" },
87
- dir: { type: "string", description: "Directory to scan for files (default: cwd)" },
88
- },
89
- },
90
- },
91
- {
92
- name: "lint_file",
93
- description:
94
- "[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.",
95
- inputSchema: {
96
- type: "object",
97
- properties: {
98
- filePath: { type: "string", description: "Path to the source file" },
99
- strict: { type: "boolean", description: "Enable strict mode (warnings become errors)" },
100
- },
101
- required: ["filePath"],
102
- },
103
- },
104
- {
105
- name: "parse_code",
106
- description:
107
- "[QUICK PARSE] Validate annotation syntax inline without reading from disk. Use during code generation or editing to verify annotation syntax before writing to file.",
108
- inputSchema: {
109
- type: "object",
110
- properties: {
111
- code: { type: "string", description: "Source code containing @annotations" },
112
- },
113
- required: ["code"],
114
- },
115
- },
116
- {
117
- name: "context",
118
- description:
119
- "[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.",
120
- inputSchema: {
121
- type: "object",
122
- properties: {
123
- dir: { type: "string", description: "Project directory (default: cwd)" },
124
- scope: { type: "string", description: "Filter by @scope module name" },
125
- },
126
- },
127
- },
128
- {
129
- name: "search_templates",
130
- description:
131
- "[TEMPLATE DISCOVERY] Find relevant templates before using teach_template. Searches by keyword (button, auth, fetch, form). Returns matching templates with descriptions and metadata.",
132
- inputSchema: {
133
- type: "object",
134
- properties: {
135
- query: { type: "string", description: "Search query" },
136
- lang: { type: "string", description: "Language module (react, vue)" },
137
- },
138
- required: ["query"],
139
- },
140
- },
141
- {
142
- name: "generate_tests",
143
- description:
144
- "[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.",
145
- inputSchema: {
146
- type: "object",
147
- properties: {
148
- sourceDir: { type: "string", description: "Source directory to scan (default: cwd)" },
149
- lang: { type: "string", description: "Language module (react, vue)" },
150
- output: { type: "string", description: "Output path for generated test file (optional)" },
151
- },
152
- },
153
- },
154
- {
155
- name: "teach_template",
156
- description:
157
- "[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.",
158
- inputSchema: {
159
- type: "object",
160
- properties: {
161
- templateId: {
162
- type: "string",
163
- description: "Template ID to teach (e.g. button-shadcn, hook-useAuth)",
164
- },
165
- lang: { type: "string", description: "Language module (react, vue)" },
166
- showCode: { type: "boolean", description: "Include full template code in output" },
167
- },
168
- required: ["templateId"],
169
- },
170
- },
171
- {
172
- name: "analyze_project",
173
- description:
174
- "[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.",
175
- inputSchema: {
176
- type: "object",
177
- properties: {
178
- dir: { type: "string", description: "Project directory (default: cwd)" },
179
- },
180
- },
181
- },
182
- {
183
- name: "init_project",
184
- description:
185
- "[PROJECT INIT] Initialize a new openPrompt-Lang project. Creates prompt-lang.json, AGENTS.md, and base directory structure. Use for fresh projects before scaffold_project.",
186
- inputSchema: {
187
- type: "object",
188
- properties: {
189
- name: { type: "string", description: "Project name" },
190
- dir: { type: "string", description: "Target directory (default: cwd)" },
191
- stack: {
192
- type: "string",
193
- description: "Stack items (comma-separated, e.g. react,typescript,vite,tailwind)",
194
- },
195
- },
196
- },
197
- },
198
- {
199
- name: "component_list",
200
- description:
201
- "[COMPONENT LIST] List available components from templates and language modules. Use to discover what components exist before generating new ones.",
202
- inputSchema: {
203
- type: "object",
204
- properties: {
205
- lang: { type: "string", description: "Language module (react, vue, angular, etc.)" },
206
- },
207
- },
208
- },
209
- {
210
- name: "scaffold_project",
211
- description:
212
- "[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.",
213
- inputSchema: {
214
- type: "object",
215
- properties: {
216
- framework: { type: "string", description: "Framework/language id (react, vue)" },
217
- name: { type: "string", description: "Project subdirectory name (optional)" },
218
- force: { type: "boolean", description: "Overwrite existing files" },
219
- },
220
- required: ["framework"],
221
- },
222
- },
223
- {
224
- name: "knowledge_search",
225
- description:
226
- "[KNOWLEDGE SEARCH] Search across all knowledge sources (books, chapters, concepts, templates) using a text query. Returns ranked results with snippets. Use as primary knowledge lookup when the AI needs information about programming topics, frameworks, or APIs.",
227
- inputSchema: {
228
- type: "object",
229
- properties: {
230
- query: {
231
- type: "string",
232
- description: 'Search query (e.g. "ownership rust", "react hooks", "payment api")',
233
- },
234
- category: {
235
- type: "string",
236
- description:
237
- "Filter by category: lenguajes, web, metodologias, algoritmos, api, accesibilidad",
238
- },
239
- bookId: { type: "string", description: "Filter results to a specific book ID" },
240
- limit: { type: "number", description: "Maximum results to return (default: 20)" },
241
- },
242
- required: ["query"],
243
- },
244
- },
245
- {
246
- name: "knowledge_list",
247
- description:
248
- "[KNOWLEDGE LIST] List all available books in the knowledge base with metadata (title, pages, chapters, method). Use to discover what knowledge is available before searching or reading.",
249
- inputSchema: {
250
- type: "object",
251
- properties: {
252
- category: { type: "string", description: "Filter by category" },
253
- },
254
- },
255
- },
256
- {
257
- name: "knowledge_read",
258
- description:
259
- "[KNOWLEDGE READ] Read the full content of a specific chapter from a book. Use after knowledge_search to get complete content. Returns chapter title, content, and word count.",
260
- inputSchema: {
261
- type: "object",
262
- properties: {
263
- bookId: {
264
- type: "string",
265
- description: 'Book ID (e.g. "rust-libro-espanol", "capacitorjs-docs")',
266
- },
267
- chapterIndex: {
268
- type: "number",
269
- description:
270
- "Chapter index number (from search results). If omitted, returns first chapter.",
271
- },
272
- },
273
- required: ["bookId"],
274
- },
275
- },
276
- {
277
- name: "knowledge_concept",
278
- description:
279
- "[KNOWLEDGE CONCEPT] Expand a semantic concept from a book, showing its content and related concepts from the knowledge graph. Use to understand relationships between topics.",
280
- inputSchema: {
281
- type: "object",
282
- properties: {
283
- bookId: { type: "string", description: "Book ID" },
284
- conceptSlug: {
285
- type: "string",
286
- description: "Specific concept slug to read (if omitted, returns first concept)",
287
- },
288
- },
289
- required: ["bookId"],
290
- },
291
- },
292
- {
293
- name: "knowledge_domain",
294
- description:
295
- "[KNOWLEDGE DOMAIN] List knowledge domains or books within a domain. Use to discover organized knowledge areas (payments, mobile-pwa, systems, qa, etc.). Returns domain descriptions and book counts.",
296
- inputSchema: {
297
- type: "object",
298
- properties: {
299
- domain: {
300
- type: "string",
301
- description:
302
- 'Domain name to list books for (e.g. "payments", "mobile-pwa", "systems", "qa", "frontend", "backend", "accessibility", "algorithms", "fundamentals"). If omitted, lists all domains.',
303
- },
304
- },
305
- },
306
- },
307
- {
308
- name: "knowledge_playbook",
309
- description:
310
- "[KNOWLEDGE PLAYBOOK] Read an actionable playbook for a domain. Playbooks contain step-by-step procedures for implementing specific solutions. Available domains: payments, mobile-pwa, systems, qa.",
311
- inputSchema: {
312
- type: "object",
313
- properties: {
314
- domain: {
315
- type: "string",
316
- description: "Domain playbook to read (payments, mobile-pwa, systems, qa)",
317
- },
318
- },
319
- required: ["domain"],
320
- },
321
- },
322
- {
323
- name: "recall",
324
- description:
325
- "[MEMORY RECALL] Search project memory: learnings, concepts, errors, and tickets. Use BEFORE implementing to avoid repeating past mistakes. Saves ~30min of debugging.",
326
- inputSchema: {
327
- type: "object",
328
- properties: {
329
- query: { type: "string", description: "Search query for project memory" },
330
- domain: {
331
- type: "string",
332
- description:
333
- "Filter by domain (programming, reports, business, legal, product, technical-writing)",
334
- },
335
- all: { type: "boolean", description: "Search across all domains" },
336
- },
337
- required: ["query"],
338
- },
339
- },
340
- {
341
- name: "context_unified",
342
- description:
343
- "[UNIFIED CONTEXT] Cross-source search across 7 sources: knowledge, learning, semantic, templates, tickets, errors, and patterns. Use before planning to gather all relevant project context. Saves ~20min of research.",
344
- inputSchema: {
345
- type: "object",
346
- properties: {
347
- query: { type: "string", description: "Search query across all sources" },
348
- domain: { type: "string", description: "Filter by active domain" },
349
- },
350
- },
351
- },
352
- {
353
- name: "domain_status",
354
- description:
355
- "[DOMAIN STATUS] Show active domains, file counts, pattern counts, and context loaded. Use to understand what domains and patterns are active in the current context.",
356
- inputSchema: {
357
- type: "object",
358
- properties: {},
359
- },
360
- },
361
- {
362
- name: "work_context_status",
363
- description:
364
- "[WORK SESSION STATUS] Show current work session state: phase, tools called, plan status, domain. Use before starting any work to know where you are in the workflow.",
365
- inputSchema: {
366
- type: "object",
367
- properties: {},
368
- },
369
- },
370
- {
371
- name: "work_context_plan",
372
- description:
373
- "[WORK PLAN] Create a work plan for the current task. Use BEFORE implementing to avoid going in wrong directions. Saves ~1hr of rework.",
374
- inputSchema: {
375
- type: "object",
376
- properties: {
377
- task: { type: "string", description: "Task description for the plan" },
378
- domain: {
379
- type: "string",
380
- description: "Domain for this plan (programming, reports, etc.)",
381
- },
382
- },
383
- required: ["task"],
384
- },
385
- },
386
- {
387
- name: "work_context_start",
388
- description:
389
- "[WORK SESSION START] Start or resume a work session. Tracks what tools are called and workflow phase. Enable workflow enforcement when session is active.",
390
- inputSchema: {
391
- type: "object",
392
- properties: {
393
- task: { type: "string", description: "Task being started" },
394
- domain: { type: "string", description: "Active domain for this session" },
395
- },
396
- },
397
- },
398
- {
399
- name: "work_context_close",
400
- description:
401
- "[WORK SESSION CLOSE] Close the current work session. Records metrics, lessons learned, and validates state. REQUIRES work_context_start to have been called.",
402
- inputSchema: {
403
- type: "object",
404
- properties: {
405
- summary: { type: "string", description: "Summary of what was accomplished" },
406
- },
407
- },
408
- },
409
- {
410
- name: "workflow_check",
411
- description:
412
- "[WORKFLOW GATE] Check what steps are still needed in the current workflow. Returns a checklist of completed vs pending steps. Use to verify you are following the optimal flow that saves time.",
413
- inputSchema: {
414
- type: "object",
415
- properties: {},
416
- },
417
- },
418
- {
419
- name: "teach_progress",
420
- description:
421
- "[TEACH PROGRESS] Show consolidated learning progress from SQLite. Returns concept-by-concept status (mastered/in_progress/not_started), scores, levels, and next recommended concept. Use before starting a study session to know current state.",
422
- inputSchema: {
423
- type: "object",
424
- properties: {
425
- userId: { type: "string", description: "User ID (default: $USER)" },
426
- domain: { type: "string", description: "Filter by domain (sql, javascript, etc.)" },
427
- },
428
- },
429
- },
430
- {
431
- name: "teach_assess",
432
- description:
433
- "[TEACH ASSESS] Diagnostic level of domain for a concept. Reads progress from SQLite and returns assessed level, score, next level, recommendations, and estimated time. Read-only — does not persist. Use to know user level before teaching.",
434
- inputSchema: {
435
- type: "object",
436
- properties: {
437
- userId: { type: "string", description: "User ID (default: $USER)" },
438
- conceptId: { type: "string", description: "Concept ID to assess (e.g. joins-sql)" },
439
- domain: { type: "string", description: "Domain (e.g. sql, javascript)" },
440
- },
441
- required: ["conceptId"],
442
- },
443
- },
444
- {
445
- name: "teach_study",
446
- description:
447
- "[TEACH STUDY] Generate a complete adapted pedagogy unit for a concept. Returns objective, prerequisites, explanations, examples, common errors, mastery criteria, and exercises. Adapts to user level automatically from SQLite progress or explicit level parameter.",
448
- inputSchema: {
449
- type: "object",
450
- properties: {
451
- userId: { type: "string", description: "User ID (default: $USER)" },
452
- conceptId: { type: "string", description: "Concept ID to study (e.g. joins-sql)" },
453
- level: {
454
- type: "string",
455
- description:
456
- "Force specific level (recognizes, explains, etc.). Auto-detected from progress if omitted.",
457
- },
458
- includeExercises: {
459
- type: "boolean",
460
- description: "Include exercise in response (default: false)",
461
- },
462
- },
463
- required: ["conceptId"],
464
- },
465
- },
466
- ]
467
-
468
- function sendMessage(msg) {
469
- const data = JSON.stringify(msg)
470
- process.stdout.write(`${data}\n`)
471
- }
472
-
473
- function sendError(id, code, message) {
474
- sendMessage({ jsonrpc: "2.0", id, error: { code, message } })
475
- }
476
-
477
- function sendResult(id, result) {
478
- sendMessage({ jsonrpc: "2.0", id, result })
479
- }
480
-
481
- function formatResult(type, summary, details, actions) {
482
- const parts = [`## Result: ${type}`, "", summary]
483
- if (details && details.length) {
484
- parts.push("", "### Details", ...details.map((d) => `- ${d}`))
485
- }
486
- if (actions && actions.length) {
487
- parts.push("", "### Recommended Actions", ...actions.map((a) => `- ${a}`))
488
- }
489
- return parts.join("\n")
490
- }
491
-
492
- function toolResult(text, isError = false) {
493
- return { content: [{ type: "text", text }], isError }
494
- }
495
-
496
- async function handleToolCall(name, args) {
497
- switch (name) {
498
- case "validate": {
499
- const dir = args?.dir || args?.filePath || process.cwd()
500
- const target = join(process.cwd(), dir)
501
- if (!existsSync(target)) return toolResult(`Path not found: ${target}`, true)
502
-
503
- // If target is a directory, scan for annotated files
504
- if (statSync(target).isDirectory()) {
505
- const allFiles = []
506
- function walk(d) {
507
- let entries
508
- try {
509
- entries = readdirSync(d, { withFileTypes: true })
510
- } catch {
511
- return
512
- }
513
- for (const e of entries) {
514
- if (e.name.startsWith(".") || e.name === "node_modules" || e.name === "dist") continue
515
- const p = join(d, e.name)
516
- if (e.isDirectory()) walk(p)
517
- else if (e.isFile() && /\.(ts|tsx|js|jsx)$/.test(e.name)) allFiles.push(p)
518
- }
519
- }
520
- walk(target)
521
- if (allFiles.length === 0) {
522
- return toolResult(
523
- formatResult(
524
- "info",
525
- "No source files found in directory.",
526
- [`Scanned: ${target}`],
527
- ["Run scaffold_project to initialize project structure."]
528
- )
529
- )
530
- }
531
- let totalAnn = 0
532
- let totalErr = 0
533
- let totalWarn = 0
534
- const details = []
535
- for (const f of allFiles) {
536
- const code = readFileSync(f, "utf-8")
537
- const result = lintFile(code)
538
- totalAnn += result.annotations.length
539
- totalErr += result.errors.length
540
- totalWarn += result.warnings.length
541
- if (result.errors.length || result.warnings.length) {
542
- const rel = f.replace(`${target}/`, "")
543
- for (const e of result.errors) details.push(`❌ ${rel}: ${e}`)
544
- for (const w of result.warnings) details.push(`⚠ ${rel}: ${w}`)
545
- }
546
- }
547
- const status = totalErr > 0 ? "error" : totalWarn > 0 ? "warning" : "success"
548
- const actions = []
549
- if (totalErr > 0) actions.push("Fix errors listed above, then run validate again")
550
- if (totalWarn > 0) actions.push("Review warnings — some may indicate potential issues")
551
- if (totalErr === 0 && totalWarn === 0) {
552
- actions.push("Project annotations are valid. Ready for commit.")
553
- }
554
- return toolResult(
555
- formatResult(
556
- status,
557
- `Scanned ${allFiles.length} files: ${totalAnn} annotations, ${totalErr} errors, ${totalWarn} warnings`,
558
- details.length ? details : [`✅ All annotations valid across ${allFiles.length} files`],
559
- actions
560
- )
561
- )
562
- }
563
-
564
- const result = lintFile(readFileSync(target, "utf-8"))
565
- const status =
566
- result.errors.length > 0 ? "error" : result.warnings.length > 0 ? "warning" : "success"
567
- const actions = []
568
- if (result.errors.length > 0) actions.push("Fix errors, then re-run validate on this file")
569
- if (result.warnings.length > 0) actions.push("Review warnings before committing")
570
- if (result.errors.length === 0 && result.warnings.length === 0) {
571
- actions.push("File is valid. No issues found.")
572
- }
573
- return toolResult(
574
- formatResult(
575
- status,
576
- `File: ${args?.filePath || target}: ${result.annotations.length} annotations, ${result.errors.length} errors, ${result.warnings.length} warnings`,
577
- [...result.errors.map((e) => `❌ ${e}`), ...result.warnings.map((w) => `⚠ ${w}`)],
578
- actions
579
- )
580
- )
581
- }
582
-
583
- case "lint_file": {
584
- const filePath = join(process.cwd(), args.filePath)
585
- if (!existsSync(filePath)) {
586
- return toolResult(
587
- formatResult(
588
- "error",
589
- "File not found",
590
- [`Path: ${filePath}`],
591
- ["Verify the file path and try again"]
592
- ),
593
- true
594
- )
595
- }
596
- const code = readFileSync(filePath, "utf-8")
597
- const result = lintFile(code, args.strict)
598
- const status =
599
- result.errors.length > 0 ? "error" : result.warnings.length > 0 ? "warning" : "success"
600
- return toolResult(
601
- formatResult(
602
- status,
603
- `${args.filePath}: ${result.annotations.length} annotations`,
604
- [
605
- ...result.annotations.map((a) => `@${a.name}(${a.raw})`),
606
- ...result.errors.map((e) => `❌ ${e}`),
607
- ...result.warnings.map((w) => `⚠ ${w}`),
608
- ],
609
- result.errors.length
610
- ? ["Fix errors and re-run lint_file"]
611
- : result.warnings.length
612
- ? ["Review warnings"]
613
- : ["File is valid"]
614
- )
615
- )
616
- }
617
-
618
- case "parse_code": {
619
- if (!args?.code) {
620
- return toolResult(
621
- formatResult(
622
- "error",
623
- "No code provided",
624
- [],
625
- ["Pass a 'code' parameter with annotation source text"]
626
- ),
627
- true
628
- )
629
- }
630
- const result = lintFile(args.code)
631
- const status =
632
- result.errors.length > 0 ? "error" : result.warnings.length > 0 ? "warning" : "success"
633
- return toolResult(
634
- formatResult(
635
- status,
636
- `${result.annotations.length} annotations found`,
637
- [
638
- ...result.annotations.map((a) => `@${a.name}(${a.raw})`),
639
- ...result.errors.map((e) => `❌ ${e}`),
640
- ...result.warnings.map((w) => `⚠ ${w}`),
641
- ],
642
- result.errors.length ? ["Fix annotation syntax"] : ["Annotation syntax is valid"]
643
- )
644
- )
645
- }
646
-
647
- case "context": {
648
- const dir = args?.dir || process.cwd()
649
- const cwd = join(process.cwd(), dir)
650
- let config = null
651
- try {
652
- config = loadConfig(cwd)
653
- } catch {
654
- console.error("[mcp] ⚠ No se pudo cargar configuración en context tool")
655
- }
656
- const details = [`Directory: ${dir}`, `Config: ${config ? "found" : "none"}`]
657
- if (config) {
658
- details.push(
659
- `Name: ${config.name || "unnamed"}`,
660
- `Language: ${config.lenguaje || "unknown"}`,
661
- `Stack: ${(config.stack?.base || []).join(", ")}`
662
- )
663
- }
664
- const actions = config
665
- ? ["Run validate to check annotation health"]
666
- : ["Run init or scaffold_project to create project config"]
667
- return toolResult(formatResult("success", "Project context extracted", details, actions))
668
- }
669
-
670
- case "search_templates": {
671
- const lang = args?.lang || "react"
672
- const query = args?.query || ""
673
- const results = searchTemplates(query, { lang })
674
- if (!results.length) {
675
- return toolResult(
676
- formatResult(
677
- "info",
678
- `No templates match "${query}" in ${lang}`,
679
- [],
680
- ["Try a different query or check available templates with lang list"]
681
- )
682
- )
683
- }
684
- return toolResult(
685
- formatResult(
686
- "success",
687
- `${results.length} templates found for "${query}" in ${lang}`,
688
- results.map(
689
- (t, i) => `${i + 1}. ${t.id}: ${t.description || "no description"} — ${t.category}`
690
- ),
691
- [
692
- "Run teach_template with a template ID to learn more",
693
- "Use showCode=true in teach_template to see the implementation",
694
- ]
695
- )
696
- )
697
- }
698
-
699
- case "generate_tests": {
700
- const sourceDir = args?.sourceDir || process.cwd()
701
- const lang = args?.lang || "react"
702
- const dir = join(process.cwd(), sourceDir)
703
- if (!existsSync(dir)) {
704
- return toolResult(formatResult("error", "Directory not found", [`Path: ${dir}`], []), true)
705
- }
706
- const errors = scanForLearnErrors(dir, lang)
707
- if (!errors.length) {
708
- return toolResult(
709
- formatResult(
710
- "info",
711
- "No @learn-error annotations found",
712
- [`Scanned: ${dir}`],
713
- ["After fixing a bug, add @learn-error to the file and re-run generate_tests"]
714
- )
715
- )
716
- }
717
- const tests = generateTests(errors, lang)
718
- const details = [`Learned errors found: ${errors.length}`, `Tests generated: ${tests.length}`]
719
- if (args?.output) {
720
- const outPath = join(process.cwd(), args.output)
721
- writeTestFile(errors, outPath, lang)
722
- details.push(`Written to: ${args.output}`)
723
- }
724
- const actions = [
725
- args?.output
726
- ? `Run: npx vitest run ${args.output}`
727
- : "Pass 'output' parameter to write test file to disk",
728
- ]
729
- return toolResult(
730
- formatResult(
731
- "success",
732
- `Generated ${tests.length} tests from ${errors.length} learned errors`,
733
- [...details, "", ...tests],
734
- actions
735
- )
736
- )
737
- }
738
-
739
- case "teach_template": {
740
- const lang = args?.lang || "react"
741
- const template = getTemplate(args.templateId, lang)
742
- if (!template) {
743
- return toolResult(
744
- formatResult(
745
- "error",
746
- `Template "${args.templateId}" not found in ${lang}`,
747
- [],
748
- [`Run search_templates with keywords to find available templates in ${lang}`]
749
- ),
750
- true
751
- )
752
- }
753
- const langObj = getLanguage(lang)
754
- const meta = template.metadata
755
- const details = [
756
- `${meta.name}: ${meta.description || ""}`,
757
- `Category: ${meta.category}`,
758
- `Purposes: ${meta.purposes?.join(", ") || "general"}`,
759
- ]
760
- if (meta.variants?.length) details.push(`Variants: ${meta.variants.join(", ")}`)
761
- if (meta.sizes?.length) details.push(`Sizes: ${meta.sizes.join(", ")}`)
762
- details.push(
763
- `Dark mode: ${meta.darkMode ? "yes" : "no"}`,
764
- `Responsive: ${meta.responsive ? "yes" : "no"}`,
765
- `Deps: ${meta.dependencies?.join(", ") || "none"}`
766
- )
767
- if (meta.hasTeachMe) {
768
- const lesson = extractLesson(template.content)
769
- if (lesson) details.push("", "--- LESSON ---", lesson)
770
- }
771
- const goodP = extractTags(template.content, "@goodPractice")
772
- const badP = extractTags(template.content, "@badPractice")
773
- if (goodP.length) details.push("", "--- GOOD PRACTICES ---", ...goodP.map((g) => `✅ ${g}`))
774
- if (badP.length) details.push("", "--- BAD PRACTICES ---", ...badP.map((b) => `❌ ${b}`))
775
- if (args?.showCode) {
776
- const clean = template.content
777
- .split("\n")
778
- .filter((l) => !l.trim().startsWith("// @"))
779
- .join("\n")
780
- details.push("", "--- CODE ---", clean)
781
- }
782
- const actions = [
783
- "Use this pattern in your project",
784
- "Run scaffold_project to set up the project structure",
785
- ]
786
- return toolResult(
787
- formatResult("success", `${meta.name} — ${langObj?.name || lang}`, details, actions)
788
- )
789
- }
790
-
791
- case "analyze_project": {
792
- const dir = args?.dir || process.cwd()
793
- const cwd = join(process.cwd(), dir)
794
- if (!existsSync(cwd)) {
795
- return toolResult(formatResult("error", "Directory not found", [`Path: ${cwd}`], []), true)
796
- }
797
- let config = null
798
- try {
799
- config = loadConfig(cwd)
800
- } catch {
801
- console.error("[mcp] ⚠ No se pudo cargar configuración en analyze_project")
802
- }
803
- const counts = countFiles(cwd)
804
- const allFiles = findAllFiles(cwd)
805
- const annResults = []
806
- for (const f of allFiles) {
807
- try {
808
- const code = readFileSync(f, "utf-8")
809
- const r = lintFile(code)
810
- if (r.annotations.length || r.errors.length || r.warnings.length) {
811
- annResults.push({ file: f.replace(`${cwd}/`, ""), ...r })
812
- }
813
- } catch {
814
- /* skip unreadable files */
815
- }
816
- }
817
- const details = [
818
- `Config: ${config ? "found" : "none"}`,
819
- `Name: ${config?.name || "unnamed"}`,
820
- `Language: ${config?.lenguaje || "unknown"}`,
821
- `Stack: ${(config?.stack?.base || []).join(", ")}`,
822
- "",
823
- `Total files: ${counts.total}`,
824
- `.ts: ${counts.ts} | .tsx: ${counts.tsx} | .js: ${counts.js} | .jsx: ${counts.jsx} | .css: ${counts.css}`,
825
- "",
826
- `Files with annotations: ${annResults.length}`,
827
- ...annResults.map(
828
- (r) =>
829
- `${r.file}: ${r.annotations.length} annotations, ${r.errors.length} errors, ${r.warnings.length} warnings`
830
- ),
831
- ]
832
- const actions = config
833
- ? [
834
- "Run validate to check annotation consistency",
835
- "Run search_templates to find relevant patterns",
836
- ]
837
- : ["Run init to set up project configuration"]
838
- return toolResult(formatResult("success", `Project analysis: ${dir}`, details, actions))
839
- }
840
-
841
- case "init_project": {
842
- const targetDir = join(process.cwd(), args?.dir || "")
843
- const defaultConfig = {
844
- ...getDefaultConfig(),
845
- name: args?.name || targetDir.split("/").pop() || "mi-proyecto",
846
- stack: args?.stack
847
- ? { base: args.stack.split(",").map((s) => s.trim()) }
848
- : { base: ["react", "typescript", "vite", "tailwind"] },
849
- }
850
- try {
851
- writeFileSync(
852
- join(targetDir, "prompt-lang.json"),
853
- JSON.stringify(defaultConfig, null, 2),
854
- "utf-8"
855
- )
856
- writeFileSync(
857
- join(targetDir, "AGENTS.md"),
858
- `# AGENTS.md — Contexto para IA
859
-
860
- ## Stack
861
- - ${defaultConfig.stack.base.join("\n- ")}
862
-
863
- ## Convenciones
864
- - Framework: openPrompt-Lang (@kind, @contract, @limit annotations)
865
- - NO any types
866
- - NO exceed line limits without refactoring
867
- - Annotations must declare @use() at file start
868
- - Run validation before every commit
869
- `,
870
- "utf-8"
871
- )
872
- } catch (err) {
873
- return toolResult(
874
- formatResult("error", "Failed to initialize project", [err.message], []),
875
- true
876
- )
877
- }
878
- const actions = [
879
- "Run scaffold_project <framework> to create folder structure",
880
- "Run validate to verify setup",
881
- ]
882
- return toolResult(
883
- formatResult(
884
- "success",
885
- "Project initialized",
886
- [
887
- `prompt-lang.json created with stack: ${defaultConfig.stack.base.join(", ")}`,
888
- "AGENTS.md created with conventions",
889
- ],
890
- actions
891
- )
892
- )
893
- }
894
-
895
- case "component_list": {
896
- const langFilter = args?.lang
897
- const langs = getLanguages(true)
898
- const details = []
899
- for (const lang of langs) {
900
- if (langFilter && lang.id !== langFilter) continue
901
- const index = getLanguageIndex(lang.id)
902
- const templates = index?.templates || []
903
- const withTeach = templates.filter((t) => t.hasTeachMe).length
904
- details.push(`**${lang.id}**: ${templates.length} templates (${withTeach} with @teachMe)`)
905
- for (const t of templates.slice(0, 5)) {
906
- details.push(` - ${t.id}: ${t.description}`)
907
- }
908
- if (templates.length > 5) {
909
- details.push(` - ... and ${templates.length - 5} more`)
910
- }
911
- }
912
- if (details.length === 0) {
913
- return toolResult(formatResult("info", "No components found", [], []))
914
- }
915
- return toolResult(
916
- formatResult("success", `Components found in ${langFilter || "all languages"}`, details, [
917
- "Use teach_template <id> to learn about a specific component",
918
- ])
919
- )
920
- }
921
-
922
- case "scaffold_project": {
923
- const output = await scaffoldFolders(args.framework, {
924
- name: args?.name,
925
- force: args?.force,
926
- })
927
- if (!output) {
928
- return toolResult(
929
- formatResult(
930
- "error",
931
- `Failed to scaffold "${args.framework}"`,
932
- [],
933
- ["Verify the framework id is valid (react, vue)"]
934
- ),
935
- true
936
- )
937
- }
938
- return toolResult(
939
- formatResult(
940
- "success",
941
- `Scaffold "${args.framework}" created`,
942
- [`Location: ${output}`],
943
- ["Run validate to verify structure", "Run knowledge_search to find relevant knowledge"]
944
- )
945
- )
946
- }
947
-
948
- case "knowledge_search": {
949
- const ksResult = searchKnowledge(args.query || "", {
950
- category: args?.category,
951
- bookId: args?.bookId,
952
- limit: args?.limit || 20,
953
- dir: args?.dir,
954
- })
955
- if (ksResult.results.length === 0) {
956
- return toolResult(
957
- formatResult(
958
- "empty",
959
- `No knowledge found for "${args.query}"`,
960
- ["Try a different query or broader terms", "Use knowledge_list to see available books"],
961
- []
962
- )
963
- )
964
- }
965
- const details = ksResult.results
966
- .slice(0, 10)
967
- .map(
968
- (r) =>
969
- `**[${r.type}]** ${r.bookTitle || r.bookId} — ${r.title?.substring(0, 80) || ""}\n ${r.snippet?.substring(0, 200) || ""} (relevance: ${r.relevance})`
970
- )
971
- return toolResult(
972
- formatResult(
973
- "success",
974
- `Found ${ksResult.total} results for "${args.query}" (showing ${Math.min(ksResult.results.length, 10)})`,
975
- details,
976
- [
977
- "Use knowledge_read to get full chapter content",
978
- "Use knowledge_concept to explore related concepts",
979
- ]
980
- )
981
- )
982
- }
983
-
984
- case "knowledge_list": {
985
- const books = kList({ category: args?.category, dir: args?.dir })
986
- if (books.length === 0) {
987
- return toolResult(formatResult("empty", "No knowledge books available", [], []))
988
- }
989
- const bookLines = books.map(
990
- (b) =>
991
- `| ${b.id} | ${(b.title || "").substring(0, 50)} | ${b.pages} págs | ${b.chapters?.length || 0} caps | ${b.method} |`
992
- )
993
- return toolResult(
994
- formatResult("success", `${books.length} knowledge books available`, bookLines, [
995
- "Use knowledge_search to search within books",
996
- "Use knowledge_read to read a chapter",
997
- ])
998
- )
999
- }
1000
-
1001
- case "knowledge_read": {
1002
- const chapter = knowledgeRead(args.bookId, args?.chapterIndex, { dir: args?.dir })
1003
- if (!chapter) {
1004
- return toolResult(
1005
- formatResult(
1006
- "error",
1007
- `Book "${args.bookId}" not found or no chapters available`,
1008
- [],
1009
- ["Use knowledge_list to see available books"]
1010
- ),
1011
- true
1012
- )
1013
- }
1014
- const content = chapter.content?.substring(0, 8000) || "No content available"
1015
- const details = [
1016
- `**Book:** ${chapter.bookTitle} (${chapter.bookId})`,
1017
- `**Chapter ${chapter.chapterIndex}:** ${chapter.chapterTitle}`,
1018
- `**Words:** ${chapter.words} | **Total chapters:** ${chapter.totalChapters}`,
1019
- "",
1020
- "---",
1021
- "",
1022
- content,
1023
- ]
1024
- return toolResult(
1025
- formatResult(
1026
- "success",
1027
- `Chapter ${chapter.chapterIndex} of ${chapter.totalChapters}: "${chapter.chapterTitle}"`,
1028
- details,
1029
- []
1030
- )
1031
- )
1032
- }
1033
-
1034
- case "knowledge_concept": {
1035
- const concept = knowledgeConcept(args.bookId, args?.conceptSlug, { dir: args?.dir })
1036
- if (!concept) {
1037
- return toolResult(
1038
- formatResult(
1039
- "error",
1040
- `Book or concept "${args.bookId}" not found`,
1041
- [],
1042
- ["Use knowledge_list to see available books"]
1043
- ),
1044
- true
1045
- )
1046
- }
1047
- const relatedLines = concept.related.map((r) => ` - ${r.nodeId} (${r.relation})`).join("\n")
1048
- const content =
1049
- (typeof concept.concept === "string" ? concept.concept : "").substring(0, 5000) ||
1050
- "No content available"
1051
- const details = [
1052
- `**Book:** ${concept.bookTitle} (${concept.bookId})`,
1053
- `**Concept:** ${concept.conceptSlug}`,
1054
- `**Related concepts:** ${concept.related.length}`,
1055
- relatedLines,
1056
- "",
1057
- "---",
1058
- "",
1059
- content,
1060
- ]
1061
- return toolResult(
1062
- formatResult(
1063
- "success",
1064
- `Concept: ${concept.conceptSlug} from ${concept.bookTitle}`,
1065
- details,
1066
- []
1067
- )
1068
- )
1069
- }
1070
-
1071
- case "knowledge_domain": {
1072
- const projectDir = args?.dir || process.cwd()
1073
- if (args?.domain) {
1074
- const books = getBooksByDomain(args.domain, projectDir)
1075
- if (books.length === 0) {
1076
- return toolResult(
1077
- formatResult(
1078
- "empty",
1079
- `No books found for domain "${args.domain}"`,
1080
- [],
1081
- ["Use knowledge_domain without args to list all domains"]
1082
- ),
1083
- true
1084
- )
1085
- }
1086
- const bookLines = books.map(
1087
- (b) =>
1088
- `| ${b.id} | ${(b.title || "").substring(0, 50)} | ${b.chapters || "?"} caps | ${b.method || "unknown"} |`
1089
- )
1090
- return toolResult(
1091
- formatResult("success", `Domain "${args.domain}": ${books.length} books`, bookLines, [
1092
- "Use knowledge_search to search within this domain",
1093
- ])
1094
- )
1095
- }
1096
- const domains = listAllDomainsWithInfo(projectDir)
1097
- const domainLines = domains.map(
1098
- (d) =>
1099
- `| ${d.domain} | ${d.description} | ${d.bookCount} | ${d.playbooks.join(", ") || "—"} |`
1100
- )
1101
- return toolResult(
1102
- formatResult(
1103
- "success",
1104
- `${domains.length} knowledge domains available`,
1105
- [
1106
- "| Domain | Description | Books | Playbooks |",
1107
- "|--------|-------------|-------|----------|",
1108
- ...domainLines,
1109
- ],
1110
- [
1111
- "Use knowledge_domain with a domain name to list its books",
1112
- "Use knowledge_playbook to read a playbook",
1113
- ]
1114
- )
1115
- )
1116
- }
1117
-
1118
- case "knowledge_playbook": {
1119
- const domain = args.domain
1120
- const repoDir = args?.dir || process.cwd()
1121
- const playbookDir = join(repoDir, "knowledge-repo", "playbooks", domain)
1122
- const pkgPlaybookDir = join(
1123
- __dirname,
1124
- "..",
1125
- "templates",
1126
- "knowledge-repo",
1127
- "playbooks",
1128
- domain
1129
- )
1130
- let playbookContent = null
1131
-
1132
- for (const dir of [playbookDir, pkgPlaybookDir]) {
1133
- const files = existsSync(dir) ? readdirSync(dir).filter((f) => f.endsWith(".md")) : []
1134
- if (files.length > 0) {
1135
- playbookContent = readFileSync(join(dir, files[0]), "utf-8")
1136
- break
1137
- }
1138
- }
1139
-
1140
- if (!playbookContent) {
1141
- return toolResult(
1142
- formatResult(
1143
- "error",
1144
- `No playbook found for domain "${domain}"`,
1145
- [],
1146
- [
1147
- "Available domains: payments, mobile-pwa, systems, qa",
1148
- "Use knowledge_domain to list available domains",
1149
- ]
1150
- ),
1151
- true
1152
- )
1153
- }
1154
-
1155
- const content = playbookContent.substring(0, 8000)
1156
- const details = [`**Domain:** ${domain}`, "", "---", "", content]
1157
- return toolResult(
1158
- formatResult("success", `Playbook: ${domain}`, details, [
1159
- "Use knowledge_search to find specific topics in this domain",
1160
- ])
1161
- )
1162
- }
1163
-
1164
- case "recall": {
1165
- const activeDomain = args?.domain || "shared"
1166
- let output = ""
1167
- const recallArgs = { domain: args?.domain || activeDomain, all: args?.all }
1168
-
1169
- // Buscar en learnings acumulados de SQLite
1170
- let sqliteResults = []
1171
- try {
1172
- const { open, close } = await import("./persistence/sqlite/connection.js")
1173
- const { ensureSchema } = await import("./persistence/sqlite/schema.js")
1174
- const dbPath = join(process.cwd(), ".opencode/opl.db")
1175
- const conn = open(dbPath)
1176
- const { db } = conn
1177
- ensureSchema(db)
1178
- const raw = db
1179
- .prepare("SELECT value FROM project_metadata WHERE key = 'accumulated_learnings'")
1180
- .get()
1181
- if (raw) {
1182
- const learnings = JSON.parse(raw.value)
1183
- const query = (args?.query || "").toLowerCase()
1184
- sqliteResults = query
1185
- ? learnings.filter((l) => l.toLowerCase().includes(query))
1186
- : learnings
1187
- }
1188
- close()
1189
- } catch {
1190
- /* SQLite opcional */
1191
- }
1192
-
1193
- try {
1194
- const { recall } = await import("./commands/recall.js")
1195
- const results = await recall(args?.query || "", recallArgs)
1196
- if (results) {
1197
- const parts = []
1198
- if (results.concepts?.length > 0) {
1199
- parts.push(
1200
- `**Conceptos (${results.concepts.length}):** ${results.concepts.map((c) => `${c.name} (${c.category})`).join(", ")}`
1201
- )
1202
- }
1203
- if (results.errors?.length > 0) {
1204
- parts.push(
1205
- `**Errores conocidos (${results.errors.length}):** ${results.errors.map((e) => `${e.id}: ${e.problem}`).join("; ")}`
1206
- )
1207
- }
1208
- if (results.tickets?.length > 0) {
1209
- parts.push(
1210
- `**Tickets relacionados (${results.tickets.length}):** ${results.tickets.map((t) => `${t.id} — ${t.title}`).join("; ")}`
1211
- )
1212
- }
1213
- if (sqliteResults.length > 0) {
1214
- parts.push(
1215
- `**Aprendizajes de sesiones (${sqliteResults.length}):** ${sqliteResults.map((l) => l.substring(0, 100)).join("; ")}`
1216
- )
1217
- }
1218
- output = parts.join("\n\n")
1219
- } else if (sqliteResults.length > 0) {
1220
- output = `**Aprendizajes de sesiones (${sqliteResults.length}):** ${sqliteResults.map((l) => l.substring(0, 100)).join("; ")}`
1221
- }
1222
- } catch {
1223
- output =
1224
- sqliteResults.length > 0
1225
- ? `**Aprendizajes de sesiones (${sqliteResults.length}):** ${sqliteResults.map((l) => l.substring(0, 100)).join("; ")}`
1226
- : "Recall executed via memory scan."
1227
- }
1228
- if (!output)
1229
- output = `No se encontró "${args.query}" en la memoria del proyecto (dominio: ${recallArgs.domain}).`
1230
- return toolResult(
1231
- formatResult(
1232
- "success",
1233
- `Recall: "${args.query}"`,
1234
- [output],
1235
- [
1236
- "Usa estos resultados para evitar repetir errores pasados",
1237
- "Si no hay resultados, consulta knowledge_search también",
1238
- ]
1239
- )
1240
- )
1241
- }
1242
-
1243
- case "context_unified": {
1244
- try {
1245
- const activeDomain = args?.domain || "shared"
1246
- const { contextUnified } = await import("./commands/context.js")
1247
- const unified = await contextUnified({
1248
- query: args?.query || "",
1249
- domain: args?.domain || activeDomain,
1250
- dir: ".",
1251
- })
1252
- if (unified) {
1253
- const summary = []
1254
- if (unified.knowledge?.length)
1255
- summary.push(`Knowledge: ${unified.knowledge.length} resultados`)
1256
- if (unified.learning?.length)
1257
- summary.push(`Learning: ${unified.learning.length} conceptos`)
1258
- if (unified.templates?.length) summary.push(`Templates: ${unified.templates.length}`)
1259
- if (unified.tickets?.length) summary.push(`Tickets: ${unified.tickets.length}`)
1260
- if (unified.errors?.length) summary.push(`Errores: ${unified.errors.length}`)
1261
- if (unified.patterns?.length) summary.push(`Patrones: ${unified.patterns.length}`)
1262
- const total =
1263
- unified.knowledge?.length +
1264
- unified.learning?.length +
1265
- unified.templates?.length +
1266
- unified.tickets?.length +
1267
- unified.errors?.length +
1268
- unified.patterns?.length || 0
1269
- return toolResult(
1270
- formatResult(
1271
- "success",
1272
- `Contexto unificado: ${total} resultados en 7 fuentes`,
1273
- summary,
1274
- [
1275
- "Usa estos resultados para planificar antes de implementar",
1276
- "Considera los patrones existentes antes de crear nuevos",
1277
- ]
1278
- )
1279
- )
1280
- }
1281
- } catch {}
1282
- return toolResult(
1283
- formatResult(
1284
- "empty",
1285
- "Contexto unificado no disponible",
1286
- [],
1287
- ["Ejecuta context domain init primero"]
1288
- )
1289
- )
1290
- }
1291
-
1292
- case "domain_status": {
1293
- let output = "Dominios activos:"
1294
- try {
1295
- const { getActiveDomains } = await import("./utils/domains.js")
1296
- const active = getActiveDomains()
1297
- output = `Dominio activo: ${active.join(" + ")}`
1298
- output += "\n\nUsa este dominio para filtrar knowledge, learning y tickets."
1299
- output +=
1300
- "\nComandos: context domain use <domain> | context domain switch <domain> | context domain clear"
1301
- } catch {
1302
- output = "Dominios no inicializados. Ejecuta context domain init."
1303
- }
1304
- return toolResult(
1305
- formatResult(
1306
- "success",
1307
- output,
1308
- [],
1309
- [
1310
- "Usa context domain use <domain> para cambiar de dominio",
1311
- "El dominio actual filtra automáticamente knowledge, learning y tickets",
1312
- ]
1313
- )
1314
- )
1315
- }
1316
-
1317
- case "work_context_status": {
1318
- const session = getSessionState()
1319
- const details = [
1320
- `**Sesión:** ${session.id}`,
1321
- `**Fase:** ${session.phase}`,
1322
- `**Tools llamados:** ${session.tools_called?.join(", ") || "ninguno"}`,
1323
- `**Plan:** ${session.plan_file || "no creado"}`,
1324
- `**Validado:** ${session.validated_at || "no"}`,
1325
- `**Dominio activo:** ${session.active_domain?.join(" + ") || "shared"}`,
1326
- `**Modo estricto:** ${session.strict_mode ? "sí" : "no (guide mode)"}`,
1327
- ]
1328
- if (session.friction_events?.length > 0) {
1329
- details.push(`**Eventos de fricción:** ${session.friction_events.length}`)
1330
- }
1331
- const pendingSteps = []
1332
- if (!session.tools_called?.includes("analyze_project"))
1333
- pendingSteps.push("1. analyze_project")
1334
- if (
1335
- !session.tools_called?.includes("context_unified") &&
1336
- !session.tools_called?.includes("knowledge_search") &&
1337
- !session.tools_called?.includes("recall")
1338
- ) {
1339
- pendingSteps.push("2. context_unified o knowledge_search — buscar antes de crear")
1340
- }
1341
- if (!session.plan_file)
1342
- pendingSteps.push("3. work_context_plan — planificar antes de implementar")
1343
- if (!session.validated_at) pendingSteps.push("4. validate — verificar antes de cerrar")
1344
-
1345
- return toolResult(
1346
- formatResult(
1347
- "success",
1348
- "Estado de sesión de trabajo",
1349
- details,
1350
- pendingSteps.length > 0
1351
- ? pendingSteps
1352
- : ["✅ Flujo completo. Puedes cerrar la sesión con work_context_close."]
1353
- )
1354
- )
1355
- }
1356
-
1357
- case "work_context_plan": {
1358
- try {
1359
- const { plan } = await import("./commands/work-context.js")
1360
- await plan(args.task, {
1361
- domain: args?.domain || getSessionState()?.active_domain?.[0] || "programming",
1362
- })
1363
- const session = getSessionState()
1364
- const planFile = join(".opencode", "work-context", "PLANS", `plan-${Date.now()}.md`)
1365
- setPlanFile(session, planFile)
1366
- saveSessionState(session)
1367
- return toolResult(
1368
- formatResult(
1369
- "success",
1370
- "Plan de trabajo creado",
1371
- [
1372
- `**Tarea:** ${args.task}`,
1373
- `**Dominio:** ${args?.domain || "programming"}`,
1374
- `**Plan:** ${planFile}`,
1375
- ],
1376
- [
1377
- "Ahora ejecuta analyze_project para obtener contexto",
1378
- "Luego busca en knowledge_search antes de implementar",
1379
- ]
1380
- )
1381
- )
1382
- } catch (err) {
1383
- return toolResult(
1384
- formatResult(
1385
- "error",
1386
- "Error creando plan",
1387
- [err.message],
1388
- ["Verifica que work-context esté inicializado: work-context init"]
1389
- ),
1390
- true
1391
- )
1392
- }
1393
- }
1394
-
1395
- case "work_context_start": {
1396
- const session = getSessionState()
1397
- setPhase(session, "active")
1398
- if (args?.domain) setActiveDomain(session, args.domain)
1399
- recordToolCall(session, "work_context_start")
1400
- saveSessionState(session)
1401
-
1402
- // Cargar nivel de aprendizaje del usuario desde SQLite
1403
- let userLevelInfo = ""
1404
- try {
1405
- const { open, close } = await import("./persistence/sqlite/connection.js")
1406
- const { ensureSchema } = await import("./persistence/sqlite/schema.js")
1407
- const dbPath = join(process.cwd(), ".opencode/opl.db")
1408
- const conn = open(dbPath)
1409
- const { db } = conn
1410
- ensureSchema(db)
1411
- const concepts = db
1412
- .prepare(
1413
- "SELECT concept_id, user_level FROM learning_progress ORDER BY updated_at DESC LIMIT 3"
1414
- )
1415
- .all()
1416
- close()
1417
- if (concepts.length > 0) {
1418
- userLevelInfo = `**Nivel aprendizaje:** ${concepts.map((c) => `${c.concept_id}: ${c.user_level}`).join(", ")}`
1419
- }
1420
- } catch {
1421
- /* SQLite opcional — no bloquear */
1422
- }
1423
-
1424
- const details = [
1425
- `**Sesión:** ${session.id}`,
1426
- `**Tarea:** ${args?.task || "no especificada"}`,
1427
- `**Dominio:** ${session.active_domain?.join(" + ") || "shared"}`,
1428
- `**Fase:** active`,
1429
- ]
1430
- if (userLevelInfo) details.push(userLevelInfo)
1431
-
1432
- const pendingSteps = []
1433
- if (!session.tools_called?.includes("analyze_project"))
1434
- pendingSteps.push("analyze_project — conocer el proyecto")
1435
- if (!session.tools_called?.includes("context_unified"))
1436
- pendingSteps.push("context_unified — contexto unificado")
1437
- return toolResult(
1438
- formatResult(
1439
- "success",
1440
- "Sesión de trabajo iniciada",
1441
- details,
1442
- pendingSteps.length > 0
1443
- ? ["⚠ Pasos recomendados antes de implementar:", ...pendingSteps]
1444
- : ["✅ Puedes empezar a implementar"]
1445
- )
1446
- )
1447
- }
1448
-
1449
- case "work_context_close": {
1450
- const session = getSessionState()
1451
- const precondition = checkPreconditions("work_context_close")
1452
- if (!precondition.allowed) {
1453
- return prepareToolResponse(
1454
- toolResult(
1455
- formatResult(
1456
- "error",
1457
- "No se puede cerrar sesión",
1458
- [precondition.reason],
1459
- ["Ejecuta work_context_start primero"]
1460
- ),
1461
- true
1462
- ),
1463
- precondition
1464
- )
1465
- }
1466
- setPhase(session, "closed")
1467
- saveSessionState(session)
1468
- let autoTicketNote = ""
1469
- const result = toolResult(
1470
- formatResult(
1471
- "success",
1472
- "Sesión cerrada",
1473
- [
1474
- `**Resumen:** ${args?.summary || "completada"}`,
1475
- `**Tools llamados:** ${session.tools_called?.join(", ") || "ninguno"}`,
1476
- ],
1477
- ["La sesión ha sido registrada"]
1478
- )
1479
- )
1480
- if (shouldAutoTicket(session, result)) {
1481
- autoTicketNote =
1482
- "\n\n---\n📋 Se ha detectado fricción en el flujo. Considera crear un ticket para mejorar el sistema."
1483
- }
1484
- return autoTicketNote
1485
- ? { ...result, content: [{ type: "text", text: result.content[0].text + autoTicketNote }] }
1486
- : result
1487
- }
1488
-
1489
- case "workflow_check": {
1490
- const session = getSessionState()
1491
- const checklist = [
1492
- {
1493
- step: "analyze_project",
1494
- done: session.tools_called?.includes("analyze_project"),
1495
- rationale: "Conocer el proyecto (30s, ahorra ~15min)",
1496
- },
1497
- {
1498
- step: "context_unified o knowledge_search o recall",
1499
- done:
1500
- session.tools_called?.includes("context_unified") ||
1501
- session.tools_called?.includes("knowledge_search") ||
1502
- session.tools_called?.includes("recall"),
1503
- rationale: "Buscar antes de crear (10s, ahorra ~30min)",
1504
- },
1505
- {
1506
- step: "work_context_plan",
1507
- done: !!session.plan_file,
1508
- rationale: "Planificar antes de implementar (1min, ahorra ~1hr)",
1509
- },
1510
- {
1511
- step: "validate o lint_file",
1512
- done:
1513
- !!session.validated_at ||
1514
- session.tools_called?.includes("validate") ||
1515
- session.tools_called?.includes("lint_file"),
1516
- rationale: "Verificar antes de cerrar (5s, previene bugs)",
1517
- },
1518
- ]
1519
- const doneCount = checklist.filter((c) => c.done).length
1520
- const details = checklist.map((c) => `${c.done ? "✅" : "⬜"} **${c.step}** — ${c.rationale}`)
1521
- const actions = checklist
1522
- .filter((c) => !c.done)
1523
- .map((c) => `Ejecuta ${c.step.split(" o ")[0]}`)
1524
- return toolResult(
1525
- formatResult(
1526
- doneCount === checklist.length ? "success" : "info",
1527
- `Workflow: ${doneCount}/${checklist.length} pasos completados`,
1528
- details,
1529
- actions.length > 0 ? actions : ["✅ Flujo completo"]
1530
- )
1531
- )
1532
- }
1533
-
1534
- case "teach_progress": {
1535
- const { showProgress } = await import("./commands/teach/progress/index.js")
1536
- const result = await showProgress({
1537
- userId: args?.userId || process.env.USER || "mcp-user",
1538
- domain: args?.domain || null,
1539
- dbPath: join(process.cwd(), ".opencode/opl.db"),
1540
- json: true,
1541
- raw: true,
1542
- })
1543
- const lines = ["## Learning Progress", ""]
1544
- if (result.concepts.length === 0) {
1545
- lines.push("No concepts registered yet. Run `opl teach assess` to start.")
1546
- } else {
1547
- lines.push(
1548
- `**${result.total}** concepts: ${result.mastered} mastered, ${result.inProgress} in progress, ${result.notStarted} not started.`
1549
- )
1550
- lines.push("")
1551
- for (const c of result.concepts) {
1552
- const icon = c.status === "mastered" ? "🟢" : c.status === "in_progress" ? "🟡" : "⚪"
1553
- const level = c.currentLevel || "—"
1554
- const score = c.score !== null ? `${(c.score * 100).toFixed(0)}%` : "—"
1555
- lines.push(`${icon} **${c.name}** — ${level} (${score})`)
1556
- }
1557
- if (result.nextRecommended) {
1558
- lines.push("")
1559
- lines.push(`🎯 Next recommended: **${result.nextRecommended}**`)
1560
- }
1561
- }
1562
- return toolResult(lines.join("\n"))
1563
- }
1564
-
1565
- case "teach_assess": {
1566
- // MCP assess: read-only, sin preguntas, usa historial de SQLite
1567
- const { open, close } = await import("./persistence/sqlite/connection.js")
1568
- const { ensureSchema } = await import("./persistence/sqlite/schema.js")
1569
- const { getProgress } = await import("./persistence/sqlite/queries.js")
1570
- const { assessLevel } = await import("./core/learning/assessor.js")
1571
- const { getLevel } = await import("./core/learning/levels.js")
1572
-
1573
- const dbPath = join(process.cwd(), ".opencode/opl.db")
1574
- const conn = open(dbPath)
1575
- const { db } = conn
1576
- ensureSchema(db)
1577
-
1578
- const userId = args?.userId || process.env.USER || "mcp-user"
1579
- const conceptId = args?.conceptId
1580
- const domain = args?.domain || "general"
1581
-
1582
- if (!conceptId) return toolResult("conceptId is required", true)
1583
-
1584
- const existing = getProgress(db, conceptId)
1585
- const history = existing
1586
- ? {
1587
- assessedLevel: existing.user_level,
1588
- scores: existing.assessments_taken > 0 ? { [existing.user_level]: 0.8 } : undefined,
1589
- }
1590
- : undefined
1591
-
1592
- const result = assessLevel({ userId, domain, conceptId, history })
1593
- const level = getLevel(result.assessedLevel)
1594
- close()
1595
-
1596
- const lines = [
1597
- `## Assessment: ${conceptId}`,
1598
- "",
1599
- `**Level:** ${level.label} (${result.assessedLevel})`,
1600
- `**Score:** ${result.scores[result.assessedLevel] !== null ? `${(result.scores[result.assessedLevel] * 100).toFixed(0)}%` : "—"}`,
1601
- result.nextLevel
1602
- ? `**Next:** ${getLevel(result.nextLevel).label}`
1603
- : "🎉 **Max level reached**",
1604
- `**Time estimate:** ${result.estimatedTime}`,
1605
- "",
1606
- "**Recommendations:**",
1607
- ...result.recommendations.map((r) => `- ${r}`),
1608
- ]
1609
-
1610
- return toolResult(lines.join("\n"))
1611
- }
1612
-
1613
- case "teach_study": {
1614
- // MCP study: genera unidad pedagógica adaptada al nivel del usuario
1615
- const { open, close } = await import("./persistence/sqlite/connection.js")
1616
- const { ensureSchema } = await import("./persistence/sqlite/schema.js")
1617
- const { getProgress } = await import("./persistence/sqlite/queries.js")
1618
- const { buildTeachMeUnit, adaptUnitToLevel } = await import("./core/learning/pedagogue.js")
1619
- const { getLevel } = await import("./core/learning/levels.js")
1620
- const { generateGuidedExercise, generateFreeExercise } =
1621
- await import("./core/learning/exercise.js")
1622
- const { scanForLearnErrors } = await import("./utils/error-learner.js")
1623
-
1624
- const dbPath = join(process.cwd(), ".opencode/opl.db")
1625
- const conn = open(dbPath)
1626
- const { db } = conn
1627
- ensureSchema(db)
1628
-
1629
- const userId = args?.userId || process.env.USER || "mcp-user"
1630
- const conceptId = args?.conceptId
1631
- const explicitLevel = args?.level || null
1632
- const includeExercises = args?.includeExercises || false
1633
-
1634
- if (!conceptId) return toolResult("conceptId is required", true)
1635
-
1636
- // Determinar nivel
1637
- let userLevel = explicitLevel
1638
- if (!userLevel) {
1639
- const existing = getProgress(db, conceptId)
1640
- userLevel = existing?.user_level || "recognizes"
1641
- }
1642
- close()
1643
-
1644
- try {
1645
- const fullUnit = buildTeachMeUnit(conceptId, userLevel)
1646
- const unit = adaptUnitToLevel(fullUnit, userLevel)
1647
-
1648
- const lines = [
1649
- `## Study: ${unit.conceptId}`,
1650
- `**Level:** ${getLevel(userLevel).label}`,
1651
- "",
1652
- `**Objective:** ${unit.objective}`,
1653
- "",
1654
- ]
1655
-
1656
- if (unit.prerequisites?.length > 0) {
1657
- lines.push("**Prerequisites:**")
1658
- for (const p of unit.prerequisites) lines.push(`- ${p.name} (${p.minLevel})`)
1659
- lines.push("")
1660
- }
1661
-
1662
- if (unit.simpleExplanation) {
1663
- lines.push("**Simple explanation:**", unit.simpleExplanation, "")
1664
- }
1665
-
1666
- if (unit.technicalExplanation) {
1667
- lines.push("**Technical explanation:**", unit.technicalExplanation, "")
1668
- }
1669
-
1670
- if (unit.minimalExample) {
1671
- lines.push("**Minimal example:**", "```", unit.minimalExample, "```", "")
1672
- }
1673
-
1674
- if (unit.relationships?.length > 0) {
1675
- lines.push("**Relationships:**")
1676
- for (const r of unit.relationships) lines.push(`- ${r.concept}: ${r.relation}`)
1677
- lines.push("")
1678
- }
1679
-
1680
- if (unit.commonErrors?.length > 0) {
1681
- lines.push("**Common errors:**")
1682
- for (const e of unit.commonErrors) lines.push(`- ⚠ ${e.symptom} → ${e.fix}`)
1683
- lines.push("")
1684
- }
1685
-
1686
- try {
1687
- const allErrors = scanForLearnErrors(process.cwd())
1688
- const projectErrors = allErrors.filter(
1689
- (e) =>
1690
- e.id.toLowerCase().includes(conceptId.toLowerCase()) ||
1691
- (e.category && e.category.toLowerCase() === conceptId.toLowerCase())
1692
- )
1693
- if (projectErrors.length > 0) {
1694
- lines.push("**Project Learned Errors (@learn-error):**")
1695
- for (const e of projectErrors) lines.push(`- ⚠ ${e.id}: ${e.problem} → ${e.solution}`)
1696
- lines.push("")
1697
- }
1698
- } catch (err) {
1699
- // Ignore scanning errors silently
1700
- }
1701
-
1702
- if (includeExercises) {
1703
- let exercise
1704
- if (getLevel(userLevel).index <= 3) {
1705
- exercise = generateGuidedExercise(conceptId)
1706
- } else {
1707
- exercise = generateFreeExercise(conceptId)
1708
- }
1709
- lines.push("**Exercise:**", exercise.prompt, "")
1710
- if (exercise.steps) lines.push("Steps:", ...exercise.steps.map((s) => `- ${s}`))
1711
- if (exercise.hints) lines.push("Hints:", ...exercise.hints.map((h) => `- ${h}`))
1712
- if (exercise.criteria) lines.push("Criteria:", ...exercise.criteria.map((c) => `- ${c}`))
1713
- lines.push("")
1714
- }
1715
-
1716
- if (unit.masteryCriteria?.length > 0) {
1717
- lines.push("**Mastery criteria:**")
1718
- for (const c of unit.masteryCriteria) lines.push(`- ${c}`)
1719
- }
1720
-
1721
- return toolResult(lines.join("\n"))
1722
- } catch (err) {
1723
- return toolResult(`Concept not found: "${conceptId}". Register it first.`, true)
1724
- }
1725
- }
1726
-
1727
- default:
1728
- return toolResult(`Unknown tool: ${name}`, true)
1729
- }
1730
- }
1731
-
1732
- export function startServer() {
1733
- // PID lock: evitar servidores duplicados
1734
- const PID_PATH = join(process.cwd(), ".opencode", "mcp.pid")
1735
- try {
1736
- if (existsSync(PID_PATH)) {
1737
- const oldPid = readFileSync(PID_PATH, "utf-8").trim()
1738
- try {
1739
- process.kill(parseInt(oldPid), 0)
1740
- console.error(`[mcp] ⚠ Ya hay un servidor MCP activo (PID ${oldPid}). Saliendo.`)
1741
- process.exit(1)
1742
- } catch {
1743
- /* PID huérfano, continuar */
1744
- }
1745
- }
1746
- mkdirSync(dirname(PID_PATH), { recursive: true })
1747
- writeFileSync(PID_PATH, String(process.pid))
1748
- } catch {
1749
- /* fallback: continuar sin PID lock */
1750
- }
1751
-
1752
- // Graceful shutdown: limpiar PID file y conexiones
1753
- function cleanupAndExit() {
1754
- try {
1755
- if (existsSync(PID_PATH)) rmSync(PID_PATH)
1756
- } catch {
1757
- /* ignore */
1758
- }
1759
- try {
1760
- import("./persistence/sqlite/connection.js").then((m) => m.close())
1761
- } catch {
1762
- /* ignore */
1763
- }
1764
- process.exit(0)
1765
- }
1766
- process.on("SIGTERM", cleanupAndExit)
1767
- process.on("SIGINT", cleanupAndExit)
1768
-
1769
- let buffer = ""
1770
-
1771
- process.stdin.setEncoding("utf-8")
1772
- process.stdin.on("data", async (chunk) => {
1773
- buffer += chunk
1774
- const lines = buffer.split("\n")
1775
- buffer = lines.pop() || ""
1776
-
1777
- for (const line of lines) {
1778
- const trimmed = line.trim()
1779
- if (!trimmed) continue
1780
-
1781
- let msg
1782
- try {
1783
- msg = JSON.parse(trimmed)
1784
- } catch {
1785
- continue
1786
- }
1787
-
1788
- if (msg.jsonrpc !== "2.0") continue
1789
-
1790
- const { id, method, params } = msg
1791
-
1792
- const isNotification = id === undefined || id === null
1793
-
1794
- switch (method) {
1795
- case "initialize": {
1796
- const clientVersion = params?.protocolVersion
1797
- const clientSupportsLatest =
1798
- clientVersion && SUPPORTED_PROTOCOL_VERSIONS.includes(clientVersion)
1799
- const negotiatedVersion = clientSupportsLatest ? clientVersion : LATEST_PROTOCOL_VERSION
1800
-
1801
- // Generate rich dynamic instructions based on project context
1802
- const instructions = generateRichInstructions()
1803
-
1804
- sendResult(id, {
1805
- protocolVersion: negotiatedVersion,
1806
- capabilities: {
1807
- tools: { listChanged: false },
1808
- logging: {},
1809
- },
1810
- serverInfo: SERVER_INFO,
1811
- instructions,
1812
- })
1813
- break
1814
- }
1815
-
1816
- case "notifications/initialized": {
1817
- // Per MCP spec: client MUST send this notification after initialize response.
1818
- // No response is sent for notifications (they have no id).
1819
- break
1820
- }
1821
-
1822
- case "tools/list": {
1823
- sendResult(id, { tools: TOOLS })
1824
- break
1825
- }
1826
-
1827
- case "tools/call": {
1828
- try {
1829
- const toolName = params.name
1830
- const toolArgs = params.arguments
1831
-
1832
- // Load session and check preconditions
1833
- const session = getSessionState()
1834
- const precondition = checkPreconditions(toolName, process.cwd())
1835
-
1836
- if (!precondition.allowed) {
1837
- // Strict mode: block execution
1838
- sendResult(
1839
- id,
1840
- prepareToolResponse(
1841
- toolResult(`Workflow bloqueado: ${precondition.reason}`, true),
1842
- precondition
1843
- )
1844
- )
1845
- return
1846
- }
1847
-
1848
- // Execute the tool
1849
- const result = await handleToolCall(toolName, toolArgs)
1850
-
1851
- // Handle guide-level warnings (soft enforcement)
1852
- let finalResult = result
1853
- if (precondition.reason) {
1854
- finalResult = prepareToolResponse(result, precondition)
1855
- }
1856
-
1857
- // Update session state
1858
- recordToolCall(session, toolName)
1859
- if (toolName === "validate" || toolName === "lint_file") markValidated(session)
1860
- if (
1861
- toolName === "context_unified" ||
1862
- toolName === "knowledge_search" ||
1863
- toolName === "recall"
1864
- ) {
1865
- setPhase(session, "researching")
1866
- }
1867
- saveSessionState(session)
1868
-
1869
- // Check for auto-ticket on friction
1870
- if (shouldAutoTicket(session, finalResult)) {
1871
- const frictionNote = `\n\n---\n📋 **Fricción detectada:** Has encontrado errores después de omitir pasos del workflow. Considera crear un ticket para mejorar el sistema: \`ticket create --title "Fricción en workflow: ${toolName}" --domain ${session.active_domain?.[0] || "programming"}\``
1872
- finalResult = {
1873
- ...finalResult,
1874
- content: [{ type: "text", text: finalResult.content[0].text + frictionNote }],
1875
- }
1876
- }
1877
-
1878
- sendResult(id, finalResult)
1879
- } catch (err) {
1880
- sendError(id, -1, err.message)
1881
- }
1882
- break
1883
- }
1884
-
1885
- case "shutdown": {
1886
- sendResult(id, null)
1887
- process.exitCode = 0
1888
- try {
1889
- if (existsSync(PID_PATH)) rmSync(PID_PATH)
1890
- } catch {
1891
- /* ignore */
1892
- }
1893
- process.stdout.once("drain", () => process.exit(0))
1894
- setTimeout(() => process.exit(0), 1000).unref()
1895
- break
1896
- }
1897
-
1898
- default:
1899
- if (!isNotification) {
1900
- sendError(id, -32601, `Method not found: ${method}`)
1901
- }
1902
- }
1903
- }
1904
- })
1905
-
1906
- process.stdin.on("end", () => process.exit(0))
1907
- }
1908
-
1909
- // Auto-start when executed directly, not when imported
1910
- const isDirectRun =
1911
- process.argv[1] &&
1912
- (process.argv[1].endsWith("mcp-server.js") || process.argv[1].endsWith("mcp-server"))
1913
- if (isDirectRun) startServer()