openprompt-lang 0.8.0 → 0.9.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openprompt-lang",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "PromptLang CLI — Context Engine de anotaciones para desarrollo asistido por IA",
5
5
  "type": "module",
6
6
  "main": "./bin/cli.js",
@@ -1,11 +1,13 @@
1
- import { readFileSync } from 'fs'
1
+ import { readFileSync, existsSync, readdirSync } from 'fs'
2
2
  import { join } from 'path'
3
3
  import { getLanguageIndex, getLanguagePath } from '../utils/language-loader.js'
4
+ import { searchKnowledge } from '../utils/knowledge-search.js'
4
5
 
5
6
  export function buildPrompt (description, options = {}) {
6
7
  const langId = options.lang || 'react'
7
8
  const index = getLanguageIndex(langId)
8
9
  const profile = options.profile || 'mid'
10
+ const baseDir = options.dir || process.cwd()
9
11
 
10
12
  const relevantTemplates = findRelevantTemplates(description, index)
11
13
 
@@ -20,6 +22,10 @@ export function buildPrompt (description, options = {}) {
20
22
  }
21
23
  }
22
24
 
25
+ // Inject project context: session, learning, learn-errors, config
26
+ const projectContext = buildProjectContext(baseDir, langId, description)
27
+ const knowledgeContext = buildKnowledgeContext(description)
28
+
23
29
  const kind = detectKind(description)
24
30
  const componentName = options.name || suggestName(description, kind)
25
31
 
@@ -37,12 +43,14 @@ export function buildPrompt (description, options = {}) {
37
43
  '- Include @use() annotation at the top with the tags you use',
38
44
  '- Include @kind, @props (if component), @contract (if hook/service), @limit, @test annotations',
39
45
  templateExamples,
46
+ projectContext,
47
+ knowledgeContext,
40
48
  `## Language: ${langId}`,
41
49
  '## Profile rules:',
42
50
  ...getProfileRules(profile),
43
51
  '',
44
52
  'Return ONLY the code, no markdown wrapping or explanation.'
45
- ].join('\n')
53
+ ].filter(Boolean).join('\n')
46
54
 
47
55
  return { componentName, kind, systemPrompt, userPrompt, prompt: userPrompt }
48
56
  }
@@ -180,3 +188,69 @@ function getProfileRules (profile) {
180
188
 
181
189
  return rules[profile] || rules.mid
182
190
  }
191
+
192
+ function buildProjectContext (baseDir, langId, description) {
193
+ const parts = []
194
+
195
+ // Session
196
+ const sessionPath = join(baseDir, '.opencode', 'work-context', 'SESSION.json')
197
+ if (existsSync(sessionPath)) {
198
+ try {
199
+ const session = JSON.parse(readFileSync(sessionPath, 'utf-8'))
200
+ if (session?.session?.task?.description) {
201
+ parts.push(`- Current session task: ${session.session.task.description}`)
202
+ }
203
+ } catch {}
204
+ }
205
+
206
+ // Project config
207
+ const configPath = join(baseDir, 'prompt-lang.json')
208
+ if (existsSync(configPath)) {
209
+ try {
210
+ const config = JSON.parse(readFileSync(configPath, 'utf-8'))
211
+ parts.push(`- Project: ${config.name || 'unnamed'} (${config.lenguaje || langId})`)
212
+ if (config.stack?.base) parts.push(`- Stack: ${config.stack.base.join(', ')}`)
213
+ if (config.stack?.extensions?.length) parts.push(`- Extensions: ${config.stack.extensions.join(', ')}`)
214
+ } catch {}
215
+ }
216
+
217
+ // Learning concepts relevant to description
218
+ const learningDir = join(baseDir, '.opencode', 'learning', 'concepts')
219
+ if (existsSync(learningDir)) {
220
+ try {
221
+ const desc = description.toLowerCase()
222
+ const found = []
223
+ for (const cat of readdirSync(learningDir)) {
224
+ const catDir = join(learningDir, cat)
225
+ for (const entry of readdirSync(catDir)) {
226
+ const conceptMd = join(catDir, entry, 'concept.md')
227
+ if (existsSync(conceptMd)) {
228
+ const content = readFileSync(conceptMd, 'utf-8').toLowerCase()
229
+ if (content.includes(desc) || entry.includes(desc)) {
230
+ found.push(`${entry} (${cat})`)
231
+ }
232
+ }
233
+ }
234
+ }
235
+ if (found.length > 0) parts.push(`- Relevant learning concepts: ${found.slice(0, 3).join(', ')}`)
236
+ } catch {}
237
+ }
238
+
239
+ if (parts.length === 0) return ''
240
+ return '\n## Project context:\n' + parts.map(p => `${p}`).join('\n') + '\n'
241
+ }
242
+
243
+ function buildKnowledgeContext (description) {
244
+ try {
245
+ const kResults = searchKnowledge(description, { limit: 3 })
246
+ if (!kResults || kResults.length === 0) return ''
247
+
248
+ const parts = ['\n## Relevant knowledge:\n']
249
+ for (const r of kResults) {
250
+ parts.push(`- ${r.title || r.bookTitle}: ${(r.snippet || '').slice(0, 150)}`)
251
+ }
252
+ return parts.join('\n') + '\n'
253
+ } catch {
254
+ return ''
255
+ }
256
+ }
@@ -15,11 +15,39 @@ export function register(program) {
15
15
  .option('--domain <domain>', 'Filtrar por dominio de negocio (ecommerce, saas, mobile, api, admin, blog)')
16
16
  .option('--projects <list>', 'Proyectos vecinos a referenciar (separados por coma)')
17
17
  .option('--dir <path>', 'Directorio base del proyecto (default: cwd)')
18
+ .option('--unified', 'Búsqueda unificada: knowledge + learning + templates + tickets + semántico')
18
19
  .action(async (options) => {
19
- const { context } = await import('../commands/context.js')
20
+ const { context, contextUnified } = await import('../commands/context.js')
21
+ if (options.unified) {
22
+ await contextUnified(options)
23
+ return
24
+ }
20
25
  await context(options)
21
26
  })
22
27
 
28
+ program
29
+ .command('docs')
30
+ .description('Generar FRAMEWORK.md desde el estado actual del proyecto')
31
+ .option('--output <file>', 'Archivo de salida', '.openprompt/FRAMEWORK.md')
32
+ .option('--dir <path>', 'Directorio del proyecto (default: cwd)')
33
+ .option('--dry-run', 'Mostrar sin escribir archivo')
34
+ .action(async (options) => {
35
+ const { generateFrameworkMd } = await import('../utils/generate-framework-md.js')
36
+ const output = generateFrameworkMd({ dir: options.dir || process.cwd() })
37
+ if (options.dryRun) {
38
+ console.log(output.slice(0, 2000))
39
+ console.log('\n... (truncado en dry-run)')
40
+ return
41
+ }
42
+ const { writeFileSync, existsSync, mkdirSync } = await import('fs')
43
+ const { join, dirname } = await import('path')
44
+ const outputPath = join(options.dir || process.cwd(), options.output)
45
+ const outDir = dirname(outputPath)
46
+ if (!existsSync(outDir)) mkdirSync(outDir, { recursive: true })
47
+ writeFileSync(outputPath, output, 'utf-8')
48
+ console.log(`✅ FRAMEWORK.md generado: ${outputPath}`)
49
+ })
50
+
23
51
  program
24
52
  .command('validate')
25
53
  .description('Ejecutar pipeline de validación y verificar anotaciones')
@@ -47,6 +47,7 @@ export function register(program) {
47
47
  knowledge
48
48
  .command('to-learning')
49
49
  .description('Integrar PDFs procesados a la memoria semántica (learning)')
50
+ .option('--all', 'Procesar todos los PDFs disponibles')
50
51
  .option('--pdf <id>', 'Integrar solo un PDF específico por ID')
51
52
  .option('--force', 'Re-integran aunque ya estén integrados')
52
53
  .action(async (options) => {
@@ -29,6 +29,7 @@ export function register(program) {
29
29
  .command('add <name>')
30
30
  .description('Crear un nuevo concepto con template')
31
31
  .option('--category <cat>', 'Categoría del concepto (default: visual)')
32
+ .option('--tags <tags>', 'Tags separados por coma (ej: modal,accesibilidad)')
32
33
  .action(async (name, options) => {
33
34
  const { add } = await import('../commands/learning.js')
34
35
  await add(name, options)
@@ -125,11 +125,22 @@ export function register(program) {
125
125
  .option('--template <id>', 'Generate tests only for a specific template')
126
126
  .option('--dry-run', 'Preview without writing files')
127
127
  .option('--no-scan', 'Skip scanning source for new @learn-error annotations')
128
+ .option('--from-tickets', 'Read tickets from .opencode/bugs/ and generate tests from them')
128
129
  .action(async (options) => {
129
130
  const { qaGen } = await import('../commands/qa-gen.js')
130
131
  await qaGen(options)
131
132
  })
132
133
 
134
+ program
135
+ .command('lint-file <file>')
136
+ .description('Lint a single file for OPL annotation errors')
137
+ .option('--strict', 'Enable strict mode (warnings become errors)')
138
+ .option('--lang <lang>', 'Language module for tag resolution (default: react)')
139
+ .action(async (file, options) => {
140
+ const { lintFileCli } = await import('../commands/qa-gen.js')
141
+ await lintFileCli(file, options)
142
+ })
143
+
133
144
  program
134
145
  .command('qa-learn')
135
146
  .description('Parse @learn-error from a file and persist to language module')
@@ -175,4 +186,63 @@ export function register(program) {
175
186
  const { startServer } = await import('../mcp-server.js')
176
187
  await startServer()
177
188
  })
189
+
190
+ const ticket = program
191
+ .command('ticket')
192
+ .description('Gestión de tickets de bugs (proyecto y OPL)')
193
+
194
+ ticket
195
+ .command('create')
196
+ .description('Crear un nuevo ticket de bug')
197
+ .option('--title <title>', 'Título del ticket')
198
+ .option('--severity <severity>', 'Severidad: critical, high, medium, low')
199
+ .option('--project <name>', 'Nombre del proyecto')
200
+ .option('--file <path>', 'Archivo fuente del bug')
201
+ .option('--description <desc>', 'Descripción del bug')
202
+ .option('--learn-error <id>', 'ID de @learn-error relacionado')
203
+ .option('--type <type>', 'Tipo: OPL (framework) o project (default)')
204
+ .action(async (options) => {
205
+ const { ticketCreate } = await import('../commands/ticket.js')
206
+ await ticketCreate(options)
207
+ })
208
+
209
+ ticket
210
+ .command('list')
211
+ .description('Listar tickets del proyecto y OPL')
212
+ .option('--status <status>', 'Filtrar por estado: open, wip, fixed, wontfix')
213
+ .option('--severity <severity>', 'Filtrar por severidad')
214
+ .option('--project <name>', 'Filtrar por proyecto')
215
+ .action(async (options) => {
216
+ const { ticketList } = await import('../commands/ticket.js')
217
+ await ticketList(options)
218
+ })
219
+
220
+ ticket
221
+ .command('close <id>')
222
+ .description('Cerrar un ticket (marcar como fixed)')
223
+ .option('--fix <desc>', 'Descripción del fix aplicado')
224
+ .action(async (id, options) => {
225
+ const { ticketClose } = await import('../commands/ticket.js')
226
+ await ticketClose(id, options)
227
+ })
228
+
229
+ program
230
+ .command('doctor')
231
+ .description('Health-check: verificar que OPL y el proyecto están sanos')
232
+ .option('--dir <path>', 'Directorio del proyecto (default: cwd)')
233
+ .option('--verbose', 'Mostrar detalles adicionales')
234
+ .action(async (options) => {
235
+ const { doctor } = await import('../commands/doctor.js')
236
+ await doctor(options)
237
+ })
238
+
239
+ program
240
+ .command('recall <query>')
241
+ .description('Buscar en memoria del proyecto: aprendizajes, conceptos, errores, tickets')
242
+ .option('--lang <lang>', 'Módulo de lenguaje (default: react)')
243
+ .option('--dir <path>', 'Directorio del proyecto (default: cwd)')
244
+ .action(async (query, options) => {
245
+ const { recall } = await import('../commands/recall.js')
246
+ await recall(query, options)
247
+ })
178
248
  }
@@ -1,15 +1,21 @@
1
- // @use(kind, contract, limit)
2
- // @kind(service)
3
- // @contract(in: options -> out: void, sideEffect: contexto.md)
4
- // @limit(lines: 150)
1
+ // @use(kind, contract, limit, learn-error)
2
+ // @kind(feature)
3
+ // @contract(in: options -> out: void, sideEffect: contexto.md + unified search)
4
+ // @limit(lines: 600)
5
5
 
6
6
  import { readFileSync, writeFileSync, readdirSync, statSync, existsSync } from 'fs'
7
- import { join, relative, extname, basename } from 'path'
7
+ import { join, relative, extname, basename, dirname } from 'path'
8
+ import { fileURLToPath } from 'url'
8
9
  import { loadConfig } from '../utils/config.js'
9
10
  import { lintFile, parseAnnotations } from '../utils/annotations.js'
10
11
  import { searchConcepts } from '../utils/semantic-index.js'
12
+ import { searchKnowledge } from '../utils/knowledge-search.js'
13
+ import { searchTemplates as langSearchTemplates } from '../utils/language-loader.js'
14
+ import { scanForLearnErrors } from '../utils/error-learner.js'
11
15
  import chalk from 'chalk'
12
16
 
17
+ const __dirname = dirname(fileURLToPath(import.meta.url))
18
+
13
19
  const IGNORED_DIRS = new Set([
14
20
  '.git', 'node_modules', 'dist', 'build', '.next', '.gemini',
15
21
  'coverage', '.nyc_output', '__pycache__', '.cache'
@@ -402,3 +408,169 @@ export async function context (options) {
402
408
  if (options.domain) console.log(chalk.green(` Dominio: ${options.domain}`))
403
409
  console.log(chalk.green(`📄 Output: ${outputFile}`))
404
410
  }
411
+
412
+ export async function contextUnified (options) {
413
+ const baseDir = options.dir || process.cwd()
414
+ const query = options.query || ''
415
+ const domain = options.domain
416
+ const langId = options.lang || 'react'
417
+
418
+ console.log(chalk.cyan(`\n🔍 Búsqueda unificada: "${query || domain || 'todo'}"\n`))
419
+
420
+ const results = {
421
+ knowledge: [],
422
+ learning: [],
423
+ semantic: [],
424
+ templates: [],
425
+ tickets: [],
426
+ errors: []
427
+ }
428
+
429
+ // 1. Knowledge search
430
+ if (query) {
431
+ console.log(chalk.blue(' 📚 Buscando en knowledge...'))
432
+ try {
433
+ const kResults = searchKnowledge(query, { limit: 5 })
434
+ results.knowledge = (kResults || []).map(r => ({
435
+ title: r.title || r.bookTitle,
436
+ bookTitle: r.bookTitle || r.bookId,
437
+ snippet: r.snippet,
438
+ relevance: r.relevance || 1
439
+ }))
440
+ } catch { /* knowledge unavailable */ }
441
+ }
442
+
443
+ // 2. Semantic index
444
+ if (query) {
445
+ try {
446
+ const semantic = searchConcepts(query, langId)
447
+ if (Array.isArray(semantic)) results.semantic = semantic
448
+ } catch {}
449
+ }
450
+
451
+ // 3. Template search
452
+ if (query) {
453
+ try {
454
+ const langTemplates = langSearchTemplates(query, { lang: langId, limit: 5 })
455
+ results.templates = (langTemplates || []).map(t => ({
456
+ id: t.id,
457
+ name: t.name || t.id,
458
+ category: t.category || 'general',
459
+ description: t.description || ''
460
+ }))
461
+ } catch {}
462
+ }
463
+
464
+ // 4. Learning concepts
465
+ const learningDir = join(baseDir, '.opencode', 'learning', 'concepts')
466
+ if (existsSync(learningDir)) {
467
+ try {
468
+ for (const cat of readdirSync(learningDir)) {
469
+ if (cat === 'knowledge') continue // skip auto-extracted
470
+ const catDir = join(learningDir, cat)
471
+ for (const entry of readdirSync(catDir)) {
472
+ const conceptMd = join(catDir, entry, 'concept.md')
473
+ if (existsSync(conceptMd)) {
474
+ const c = readFileSync(conceptMd, 'utf-8')
475
+ if (!query || c.toLowerCase().includes(query.toLowerCase()) || entry.toLowerCase().includes(query.toLowerCase())) {
476
+ results.learning.push({ name: entry, category: cat, path: join(catDir, entry) })
477
+ }
478
+ }
479
+ }
480
+ }
481
+ } catch {}
482
+ }
483
+
484
+ // 5. Tickets
485
+ const ticketDirs = [
486
+ join(baseDir, '.opencode', 'bugs'),
487
+ join(__dirname, '..', '..', 'BUGS-IDENTIFIED-IN-PROJECTS')
488
+ ]
489
+ for (const td of ticketDirs) {
490
+ if (!existsSync(td)) continue
491
+ try {
492
+ const idxPath = join(td, 'INDEX.md')
493
+ if (!existsSync(idxPath)) continue
494
+ const idx = readFileSync(idxPath, 'utf-8')
495
+ for (const line of idx.split('\n')) {
496
+ if (line.includes('| BUG-') && (!query || line.toLowerCase().includes(query.toLowerCase()))) {
497
+ const cols = line.split('|').map(c => c.trim()).filter(Boolean)
498
+ if (cols.length >= 4 && cols[3] !== 'fixed') {
499
+ results.tickets.push({ id: cols[0], title: cols[1], severity: cols[2], status: cols[3] })
500
+ }
501
+ }
502
+ }
503
+ } catch {}
504
+ }
505
+
506
+ // 6. Learn-errors from source
507
+ if (query) {
508
+ try {
509
+ const foundErrors = scanForLearnErrors(baseDir, langId)
510
+ results.errors = foundErrors.filter(e =>
511
+ e.id.toLowerCase().includes(query.toLowerCase()) ||
512
+ (e.problem && e.problem.toLowerCase().includes(query.toLowerCase())) ||
513
+ (e.category && e.category.toLowerCase().includes(query.toLowerCase()))
514
+ ).slice(0, 10)
515
+ } catch {}
516
+ }
517
+
518
+ // Output
519
+ let totalResults = results.knowledge.length + results.learning.length + results.semantic.length +
520
+ results.templates.length + results.tickets.length + results.errors.length
521
+
522
+ if (totalResults === 0) {
523
+ console.log(chalk.yellow(' 📭 Sin resultados para esta búsqueda.\n'))
524
+ return
525
+ }
526
+
527
+ if (results.knowledge.length > 0) {
528
+ console.log(chalk.cyan(`\n📚 Knowledge (${results.knowledge.length}):\n`))
529
+ for (const r of results.knowledge.slice(0, 5)) {
530
+ console.log(` ${chalk.bold(r.title)} (relevance: ${r.relevance})`)
531
+ if (r.snippet) console.log(chalk.gray(` ${r.snippet.slice(0, 100)}...`))
532
+ if (r.bookTitle) console.log(chalk.gray(` Fuente: ${r.bookTitle}`))
533
+ }
534
+ }
535
+
536
+ if (results.semantic.length > 0) {
537
+ console.log(chalk.cyan(`\n🧩 Semántico (${results.semantic.length}):\n`))
538
+ for (const r of results.semantic.slice(0, 5)) {
539
+ console.log(` ${chalk.bold(r.concept)} → ${(r.components || []).join(', ')}`)
540
+ if (r.patterns?.length) console.log(chalk.gray(` Patrones: ${r.patterns.join(', ')}`))
541
+ }
542
+ }
543
+
544
+ if (results.templates.length > 0) {
545
+ console.log(chalk.cyan(`\n📄 Templates (${results.templates.length}):\n`))
546
+ for (const t of results.templates.slice(0, 5)) {
547
+ console.log(` ${chalk.bold(t.id)} [${t.category}] — ${t.description || t.name}`)
548
+ }
549
+ }
550
+
551
+ if (results.learning.length > 0) {
552
+ console.log(chalk.cyan(`\n🧠 Learning (${results.learning.length}):\n`))
553
+ for (const l of results.learning.slice(0, 5)) {
554
+ console.log(` ${chalk.bold(l.name)} (${l.category})`)
555
+ }
556
+ }
557
+
558
+ if (results.tickets.length > 0) {
559
+ console.log(chalk.cyan(`\n📋 Tickets (${results.tickets.length}):\n`))
560
+ for (const t of results.tickets.slice(0, 5)) {
561
+ const sevColor = { critical: chalk.red, high: chalk.yellow, medium: chalk.cyan, low: chalk.gray }[t.severity] || chalk.white
562
+ console.log(` ${t.id} ${sevColor(`[${t.severity}]`)} — ${t.title}`)
563
+ }
564
+ }
565
+
566
+ if (results.errors.length > 0) {
567
+ console.log(chalk.cyan(`\n🐛 Errores aprendidos (${results.errors.length}):\n`))
568
+ for (const e of results.errors.slice(0, 5)) {
569
+ console.log(` ${chalk.bold(e.id)}: ${e.problem}`)
570
+ if (e.solution) console.log(chalk.gray(` Fix: ${e.solution}`))
571
+ }
572
+ }
573
+
574
+ console.log(chalk.green(`\n ✅ ${totalResults} resultados en 6 fuentes\n`))
575
+ return results
576
+ }