@korl3one/ccode 2.0.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 +329 -0
- package/dist/ai/claude.d.ts +10 -0
- package/dist/ai/claude.js +34 -0
- package/dist/ai/manager.d.ts +17 -0
- package/dist/ai/manager.js +41 -0
- package/dist/ai/ollama.d.ts +10 -0
- package/dist/ai/ollama.js +27 -0
- package/dist/ai/provider.d.ts +12 -0
- package/dist/ai/provider.js +1 -0
- package/dist/cli/brand.d.ts +27 -0
- package/dist/cli/brand.js +90 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +737 -0
- package/dist/cli/watcher.d.ts +31 -0
- package/dist/cli/watcher.js +90 -0
- package/dist/core/context.d.ts +26 -0
- package/dist/core/context.js +45 -0
- package/dist/core/prompt-builder.d.ts +38 -0
- package/dist/core/prompt-builder.js +250 -0
- package/dist/core/tasks.d.ts +35 -0
- package/dist/core/tasks.js +59 -0
- package/dist/utils/files.d.ts +12 -0
- package/dist/utils/files.js +34 -0
- package/package.json +68 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FileWatcher: observa cambios en el proyecto en tiempo real.
|
|
3
|
+
* Acumula cambios entre interacciones del usuario.
|
|
4
|
+
*/
|
|
5
|
+
export declare class FileWatcher {
|
|
6
|
+
private changes;
|
|
7
|
+
private watcher;
|
|
8
|
+
private active;
|
|
9
|
+
private debounceTimer;
|
|
10
|
+
private onChangeCallback;
|
|
11
|
+
private static readonly IGNORE;
|
|
12
|
+
start(projectDir: string): void;
|
|
13
|
+
/**
|
|
14
|
+
* Callback que se ejecuta cuando hay cambios (con debounce).
|
|
15
|
+
*/
|
|
16
|
+
onChange(callback: (changes: Map<string, string>) => void): void;
|
|
17
|
+
/**
|
|
18
|
+
* Obtiene y limpia los cambios acumulados.
|
|
19
|
+
*/
|
|
20
|
+
flush(): string[];
|
|
21
|
+
/**
|
|
22
|
+
* Cantidad de cambios pendientes.
|
|
23
|
+
*/
|
|
24
|
+
get pendingCount(): number;
|
|
25
|
+
stop(): void;
|
|
26
|
+
isActive(): boolean;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Muestra un resumen de los cambios detectados.
|
|
30
|
+
*/
|
|
31
|
+
export declare function displayChanges(files: string[]): void;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { c } from './brand.js';
|
|
4
|
+
/**
|
|
5
|
+
* FileWatcher: observa cambios en el proyecto en tiempo real.
|
|
6
|
+
* Acumula cambios entre interacciones del usuario.
|
|
7
|
+
*/
|
|
8
|
+
export class FileWatcher {
|
|
9
|
+
changes = new Map(); // filename → event type
|
|
10
|
+
watcher = null;
|
|
11
|
+
active = false;
|
|
12
|
+
debounceTimer = null;
|
|
13
|
+
onChangeCallback = null;
|
|
14
|
+
static IGNORE = [
|
|
15
|
+
'node_modules', '.ccode', '.git', 'dist', '.next', '__pycache__',
|
|
16
|
+
'.venv', '.DS_Store', '.env', 'coverage', '.turbo', '.cache',
|
|
17
|
+
];
|
|
18
|
+
start(projectDir) {
|
|
19
|
+
if (this.active)
|
|
20
|
+
return;
|
|
21
|
+
try {
|
|
22
|
+
this.watcher = fs.watch(projectDir, { recursive: true }, (event, filename) => {
|
|
23
|
+
if (!filename)
|
|
24
|
+
return;
|
|
25
|
+
// Ignorar directorios y archivos del sistema
|
|
26
|
+
const parts = filename.split(path.sep);
|
|
27
|
+
if (parts.some(p => FileWatcher.IGNORE.includes(p) || p.startsWith('.')))
|
|
28
|
+
return;
|
|
29
|
+
this.changes.set(filename, event);
|
|
30
|
+
// Debounce: esperar 2s de inactividad antes de notificar
|
|
31
|
+
if (this.debounceTimer)
|
|
32
|
+
clearTimeout(this.debounceTimer);
|
|
33
|
+
this.debounceTimer = setTimeout(() => {
|
|
34
|
+
if (this.onChangeCallback && this.changes.size > 0) {
|
|
35
|
+
this.onChangeCallback(new Map(this.changes));
|
|
36
|
+
}
|
|
37
|
+
}, 2000);
|
|
38
|
+
});
|
|
39
|
+
this.active = true;
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
// fs.watch puede fallar en algunos sistemas — ignorar silenciosamente
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Callback que se ejecuta cuando hay cambios (con debounce).
|
|
47
|
+
*/
|
|
48
|
+
onChange(callback) {
|
|
49
|
+
this.onChangeCallback = callback;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Obtiene y limpia los cambios acumulados.
|
|
53
|
+
*/
|
|
54
|
+
flush() {
|
|
55
|
+
const files = [...this.changes.keys()];
|
|
56
|
+
this.changes.clear();
|
|
57
|
+
return files;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Cantidad de cambios pendientes.
|
|
61
|
+
*/
|
|
62
|
+
get pendingCount() {
|
|
63
|
+
return this.changes.size;
|
|
64
|
+
}
|
|
65
|
+
stop() {
|
|
66
|
+
if (this.debounceTimer)
|
|
67
|
+
clearTimeout(this.debounceTimer);
|
|
68
|
+
this.watcher?.close();
|
|
69
|
+
this.watcher = null;
|
|
70
|
+
this.active = false;
|
|
71
|
+
this.changes.clear();
|
|
72
|
+
}
|
|
73
|
+
isActive() {
|
|
74
|
+
return this.active;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Muestra un resumen de los cambios detectados.
|
|
79
|
+
*/
|
|
80
|
+
export function displayChanges(files) {
|
|
81
|
+
if (files.length === 0)
|
|
82
|
+
return;
|
|
83
|
+
console.log('');
|
|
84
|
+
console.log(c.accent(` ⟳ ${files.length} archivo${files.length > 1 ? 's' : ''} modificado${files.length > 1 ? 's' : ''} detectado${files.length > 1 ? 's' : ''}:`));
|
|
85
|
+
const show = files.slice(0, 8);
|
|
86
|
+
show.forEach(f => console.log(c.dim(` ${f}`)));
|
|
87
|
+
if (files.length > 8)
|
|
88
|
+
console.log(c.dim(` ... y ${files.length - 8} más`));
|
|
89
|
+
console.log('');
|
|
90
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export interface IProjectContext {
|
|
2
|
+
name: string;
|
|
3
|
+
version: string;
|
|
4
|
+
architecture_file: string;
|
|
5
|
+
rules_file: string;
|
|
6
|
+
tasks_file: string;
|
|
7
|
+
}
|
|
8
|
+
export type WorkflowStage = 'created' | 'connected' | 'planned' | 'in_progress' | 'idle';
|
|
9
|
+
export interface IProjectState {
|
|
10
|
+
current_task_id: string | null;
|
|
11
|
+
workflow_stage: WorkflowStage;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Motor de Contexto: gestiona el estado persistente del proyecto .ccode/
|
|
15
|
+
*/
|
|
16
|
+
export declare class ContextEngine {
|
|
17
|
+
private static readonly CCODE_DIR;
|
|
18
|
+
private context;
|
|
19
|
+
private state;
|
|
20
|
+
get ccodePath(): string;
|
|
21
|
+
load(): Promise<boolean>;
|
|
22
|
+
getContext(): IProjectContext;
|
|
23
|
+
getState(): IProjectState;
|
|
24
|
+
updateState(newState: Partial<IProjectState>): Promise<void>;
|
|
25
|
+
readContextFile(filename: string): Promise<string>;
|
|
26
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import * as path from 'path';
|
|
2
|
+
import { FileUtils } from '../utils/files.js';
|
|
3
|
+
/**
|
|
4
|
+
* Motor de Contexto: gestiona el estado persistente del proyecto .ccode/
|
|
5
|
+
*/
|
|
6
|
+
export class ContextEngine {
|
|
7
|
+
static CCODE_DIR = '.ccode';
|
|
8
|
+
context = null;
|
|
9
|
+
state = null;
|
|
10
|
+
get ccodePath() {
|
|
11
|
+
return path.join(process.cwd(), ContextEngine.CCODE_DIR);
|
|
12
|
+
}
|
|
13
|
+
async load() {
|
|
14
|
+
if (!(await FileUtils.exists(this.ccodePath))) {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
try {
|
|
18
|
+
this.context = await FileUtils.readJson(path.join(this.ccodePath, 'context.json'));
|
|
19
|
+
this.state = await FileUtils.readJson(path.join(this.ccodePath, 'state.json'));
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
getContext() {
|
|
27
|
+
if (!this.context)
|
|
28
|
+
throw new Error('Contexto no cargado.');
|
|
29
|
+
return this.context;
|
|
30
|
+
}
|
|
31
|
+
getState() {
|
|
32
|
+
if (!this.state)
|
|
33
|
+
throw new Error('Estado no cargado.');
|
|
34
|
+
return this.state;
|
|
35
|
+
}
|
|
36
|
+
async updateState(newState) {
|
|
37
|
+
if (!this.state)
|
|
38
|
+
throw new Error('Estado no cargado.');
|
|
39
|
+
this.state = { ...this.state, ...newState };
|
|
40
|
+
await FileUtils.writeJson(path.join(this.ccodePath, 'state.json'), this.state);
|
|
41
|
+
}
|
|
42
|
+
async readContextFile(filename) {
|
|
43
|
+
return FileUtils.readFileSafe(path.join(this.ccodePath, filename));
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { ITask } from './tasks.js';
|
|
2
|
+
/**
|
|
3
|
+
* PromptBuilder: genera contexto de proyecto, NO código.
|
|
4
|
+
*
|
|
5
|
+
* CCODE es una capa de contexto. Su trabajo es:
|
|
6
|
+
* - Generar documentación, arquitectura, reglas y tareas
|
|
7
|
+
* - Adaptar la complejidad al tamaño real del proyecto
|
|
8
|
+
* - Las tareas son un checklist descriptivo, no implementaciones
|
|
9
|
+
*/
|
|
10
|
+
export declare class PromptBuilder {
|
|
11
|
+
private static readonly CCODE_DIR;
|
|
12
|
+
/**
|
|
13
|
+
* Meta-prompt inteligente: adapta la complejidad del contexto al proyecto.
|
|
14
|
+
* Proyecto simple → contexto ligero. Proyecto complejo → arquitectura detallada.
|
|
15
|
+
*/
|
|
16
|
+
static getContextGenerationPrompt(name: string, description: string, features: string, techStack: string, projectType: string): string;
|
|
17
|
+
/**
|
|
18
|
+
* Prompt para regenerar/refinar tareas basado en el estado actual.
|
|
19
|
+
*/
|
|
20
|
+
static getPlanRegenerationPrompt(projectMd: string, archMd: string, rulesMd: string, currentTasks: ITask[], extraContext: string): string;
|
|
21
|
+
/**
|
|
22
|
+
* Prompt para verificar el estado de las tareas (qué se cumplió y qué falta).
|
|
23
|
+
*/
|
|
24
|
+
static getVerificationPrompt(projectMd: string, archMd: string, tasks: ITask[], projectFiles: string): string;
|
|
25
|
+
/**
|
|
26
|
+
* Extrae JSON de la respuesta de la IA.
|
|
27
|
+
*/
|
|
28
|
+
static parseJSON<T>(response: string): T;
|
|
29
|
+
/**
|
|
30
|
+
* Genera el prompt de contexto completo del proyecto.
|
|
31
|
+
* Este es el contexto que cualquier IA puede leer para entender el proyecto.
|
|
32
|
+
*/
|
|
33
|
+
buildContextPrompt(): Promise<string>;
|
|
34
|
+
/**
|
|
35
|
+
* Genera prompt enfocado en una tarea específica.
|
|
36
|
+
*/
|
|
37
|
+
buildTaskPrompt(taskId: string | null): Promise<string>;
|
|
38
|
+
}
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import * as path from 'path';
|
|
2
|
+
import { TaskEngine } from './tasks.js';
|
|
3
|
+
import { FileUtils } from '../utils/files.js';
|
|
4
|
+
/**
|
|
5
|
+
* PromptBuilder: genera contexto de proyecto, NO código.
|
|
6
|
+
*
|
|
7
|
+
* CCODE es una capa de contexto. Su trabajo es:
|
|
8
|
+
* - Generar documentación, arquitectura, reglas y tareas
|
|
9
|
+
* - Adaptar la complejidad al tamaño real del proyecto
|
|
10
|
+
* - Las tareas son un checklist descriptivo, no implementaciones
|
|
11
|
+
*/
|
|
12
|
+
export class PromptBuilder {
|
|
13
|
+
static CCODE_DIR = '.ccode';
|
|
14
|
+
/**
|
|
15
|
+
* Meta-prompt inteligente: adapta la complejidad del contexto al proyecto.
|
|
16
|
+
* Proyecto simple → contexto ligero. Proyecto complejo → arquitectura detallada.
|
|
17
|
+
*/
|
|
18
|
+
static getContextGenerationPrompt(name, description, features, techStack, projectType) {
|
|
19
|
+
return `Eres un arquitecto de software senior. Tu trabajo es generar el CONTEXTO DE TRABAJO para un proyecto, NO código.
|
|
20
|
+
|
|
21
|
+
IMPORTANTE — ADAPTA LA COMPLEJIDAD:
|
|
22
|
+
- Si el proyecto es simple (pocas funcionalidades, stack básico, prototipo o prueba), genera contexto LIGERO: documentación breve, arquitectura mínima, pocas reglas, 3-5 tareas.
|
|
23
|
+
- Si el proyecto es mediano, genera contexto MODERADO: documentación clara, arquitectura con estructura de carpetas, reglas prácticas, 5-8 tareas.
|
|
24
|
+
- Si el proyecto es complejo (múltiples módulos, integraciones, escalabilidad), genera contexto COMPLETO: documentación detallada, arquitectura con diagramas, patrones, reglas exhaustivas, 8-12 tareas.
|
|
25
|
+
|
|
26
|
+
Analiza la descripción y decide el nivel de complejidad automáticamente. NO sobre-documentes un proyecto simple.
|
|
27
|
+
|
|
28
|
+
=== PROYECTO ===
|
|
29
|
+
NOMBRE: ${name}
|
|
30
|
+
DESCRIPCIÓN: ${description}
|
|
31
|
+
FUNCIONALIDADES: ${features}
|
|
32
|
+
STACK: ${techStack}
|
|
33
|
+
TIPO: ${projectType}
|
|
34
|
+
|
|
35
|
+
Responde ÚNICAMENTE con JSON válido. Sin bloques de código markdown, sin texto antes ni después:
|
|
36
|
+
|
|
37
|
+
{
|
|
38
|
+
"project_name": "nombre del proyecto",
|
|
39
|
+
"complexity": "simple|medium|complex",
|
|
40
|
+
"project": "Contenido Markdown para project.md. Visión, objetivos, alcance y funcionalidades. Proporcionado al nivel de detalle que el proyecto necesita.",
|
|
41
|
+
"architecture": "Contenido Markdown para architecture.md. Estructura del proyecto, tecnologías y sus roles, estructura de carpetas. Solo incluye diagramas y patrones si la complejidad lo justifica.",
|
|
42
|
+
"rules": "Contenido Markdown para rules.md. Convenciones de código y estándares adaptados al stack. Breve para proyectos simples, detallado para complejos.",
|
|
43
|
+
"tasks": [
|
|
44
|
+
{
|
|
45
|
+
"title": "Descripción clara de QUÉ hacer (no CÓMO implementarlo)",
|
|
46
|
+
"description": "Criterios de aceptación: qué debe cumplirse para considerar esta tarea completa",
|
|
47
|
+
"priority": "high",
|
|
48
|
+
"module": "fase o área"
|
|
49
|
+
}
|
|
50
|
+
]
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
REGLAS PARA LAS TAREAS:
|
|
54
|
+
- Las tareas son un CHECKLIST de trabajo, NO instrucciones de código
|
|
55
|
+
- Cada tarea describe QUÉ lograr, no CÓMO programarlo
|
|
56
|
+
- La descripción debe tener criterios de aceptación claros
|
|
57
|
+
- Ordénalas por dependencia lógica (qué va primero)
|
|
58
|
+
- Cada tarea debe ser verificable: se puede confirmar si está hecha o no
|
|
59
|
+
- Ejemplo bueno: "Crear formulario de login con campos usuario y contraseña"
|
|
60
|
+
- Ejemplo malo: "Implementar const form = document.createElement('form')..."
|
|
61
|
+
|
|
62
|
+
REGLAS GENERALES:
|
|
63
|
+
- Todo en español
|
|
64
|
+
- Contenido específico al stack indicado (no genérico)
|
|
65
|
+
- priority: "high", "medium" o "low"
|
|
66
|
+
- NO generes código en ningún campo
|
|
67
|
+
- NO uses backticks de markdown alrededor del JSON`;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Prompt para regenerar/refinar tareas basado en el estado actual.
|
|
71
|
+
*/
|
|
72
|
+
static getPlanRegenerationPrompt(projectMd, archMd, rulesMd, currentTasks, extraContext) {
|
|
73
|
+
const completedTasks = currentTasks.filter(t => t.status === 'completed');
|
|
74
|
+
const pendingTasks = currentTasks.filter(t => t.status === 'pending' || t.status === 'in_progress');
|
|
75
|
+
const failedTasks = currentTasks.filter(t => t.status === 'failed');
|
|
76
|
+
const completedInfo = completedTasks.length > 0
|
|
77
|
+
? completedTasks.map(t => `- ✓ ${t.id}: ${t.title}`).join('\n')
|
|
78
|
+
: 'Ninguna completada aún.';
|
|
79
|
+
const pendingInfo = pendingTasks.length > 0
|
|
80
|
+
? pendingTasks.map(t => `- ○ ${t.id}: ${t.title} [${t.priority}]`).join('\n')
|
|
81
|
+
: 'Ninguna pendiente.';
|
|
82
|
+
const failedInfo = failedTasks.length > 0
|
|
83
|
+
? failedTasks.map(t => `- ✗ ${t.id}: ${t.title}`).join('\n')
|
|
84
|
+
: '';
|
|
85
|
+
return `Eres un gestor de proyecto. Analiza el estado actual y genera las SIGUIENTES tareas necesarias.
|
|
86
|
+
|
|
87
|
+
IMPORTANTE: Solo generas un checklist de tareas. NO generas código. Las tareas describen QUÉ hacer, no CÓMO programarlo.
|
|
88
|
+
|
|
89
|
+
=== PROYECTO ===
|
|
90
|
+
${projectMd}
|
|
91
|
+
|
|
92
|
+
=== ARQUITECTURA ===
|
|
93
|
+
${archMd}
|
|
94
|
+
|
|
95
|
+
=== REGLAS ===
|
|
96
|
+
${rulesMd}
|
|
97
|
+
|
|
98
|
+
=== TAREAS COMPLETADAS ===
|
|
99
|
+
${completedInfo}
|
|
100
|
+
|
|
101
|
+
=== TAREAS PENDIENTES ACTUALES ===
|
|
102
|
+
${pendingInfo}
|
|
103
|
+
|
|
104
|
+
${failedInfo ? `=== TAREAS FALLIDAS (necesitan replantearse) ===\n${failedInfo}\n` : ''}
|
|
105
|
+
=== CONTEXTO ADICIONAL DEL USUARIO ===
|
|
106
|
+
${extraContext || 'Ninguno.'}
|
|
107
|
+
|
|
108
|
+
Genera las próximas tareas. Responde ÚNICAMENTE con JSON válido:
|
|
109
|
+
|
|
110
|
+
{
|
|
111
|
+
"tasks": [
|
|
112
|
+
{
|
|
113
|
+
"title": "Qué lograr (verificable)",
|
|
114
|
+
"description": "Criterios de aceptación claros",
|
|
115
|
+
"priority": "high|medium|low",
|
|
116
|
+
"module": "fase o área"
|
|
117
|
+
}
|
|
118
|
+
]
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
- Considera lo ya completado para no repetir trabajo
|
|
122
|
+
- Las tareas fallidas pueden necesitar un enfoque diferente
|
|
123
|
+
- Ordena por dependencia lógica
|
|
124
|
+
- Cada tarea es verificable: se puede confirmar si está hecha o no
|
|
125
|
+
- NO incluyas instrucciones de código en las descripciones`;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Prompt para verificar el estado de las tareas (qué se cumplió y qué falta).
|
|
129
|
+
*/
|
|
130
|
+
static getVerificationPrompt(projectMd, archMd, tasks, projectFiles) {
|
|
131
|
+
const taskList = tasks.map(t => `- [${t.status === 'completed' ? '✓' : t.status === 'in_progress' ? '◐' : '○'}] ${t.id}: ${t.title}\n Criterios: ${t.description}`).join('\n');
|
|
132
|
+
return `Eres un revisor de proyecto. Tu trabajo es verificar el estado real de las tareas comparando los criterios de aceptación con el estado actual del proyecto.
|
|
133
|
+
|
|
134
|
+
=== PROYECTO ===
|
|
135
|
+
${projectMd}
|
|
136
|
+
|
|
137
|
+
=== ARQUITECTURA ESPERADA ===
|
|
138
|
+
${archMd}
|
|
139
|
+
|
|
140
|
+
=== TAREAS Y SUS CRITERIOS ===
|
|
141
|
+
${taskList}
|
|
142
|
+
|
|
143
|
+
=== ARCHIVOS EXISTENTES EN EL PROYECTO ===
|
|
144
|
+
${projectFiles}
|
|
145
|
+
|
|
146
|
+
Analiza y responde ÚNICAMENTE con JSON válido:
|
|
147
|
+
|
|
148
|
+
{
|
|
149
|
+
"verification": [
|
|
150
|
+
{
|
|
151
|
+
"task_id": "TASK-001",
|
|
152
|
+
"status": "completed|in_progress|pending|blocked",
|
|
153
|
+
"evidence": "Qué encontraste que confirma o niega el cumplimiento",
|
|
154
|
+
"missing": "Qué falta para considerar la tarea completa (null si está completa)"
|
|
155
|
+
}
|
|
156
|
+
],
|
|
157
|
+
"summary": "Resumen general del progreso del proyecto",
|
|
158
|
+
"next_recommended": "TASK-XXX - qué tarea debería abordarse siguiente y por qué"
|
|
159
|
+
}`;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Extrae JSON de la respuesta de la IA.
|
|
163
|
+
*/
|
|
164
|
+
static parseJSON(response) {
|
|
165
|
+
try {
|
|
166
|
+
return JSON.parse(response);
|
|
167
|
+
}
|
|
168
|
+
catch { /* intentar extraer */ }
|
|
169
|
+
const start = response.indexOf('{');
|
|
170
|
+
const end = response.lastIndexOf('}');
|
|
171
|
+
if (start !== -1 && end !== -1 && end > start) {
|
|
172
|
+
try {
|
|
173
|
+
return JSON.parse(response.substring(start, end + 1));
|
|
174
|
+
}
|
|
175
|
+
catch { /* intentar con regex */ }
|
|
176
|
+
}
|
|
177
|
+
const jsonBlock = response.match(/```(?:json)?\s*([\s\S]*?)```/);
|
|
178
|
+
if (jsonBlock?.[1]) {
|
|
179
|
+
try {
|
|
180
|
+
return JSON.parse(jsonBlock[1].trim());
|
|
181
|
+
}
|
|
182
|
+
catch { /* falló todo */ }
|
|
183
|
+
}
|
|
184
|
+
throw new Error('No se pudo extraer JSON válido de la respuesta de la IA.');
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Genera el prompt de contexto completo del proyecto.
|
|
188
|
+
* Este es el contexto que cualquier IA puede leer para entender el proyecto.
|
|
189
|
+
*/
|
|
190
|
+
async buildContextPrompt() {
|
|
191
|
+
const ccodePath = path.join(process.cwd(), PromptBuilder.CCODE_DIR);
|
|
192
|
+
const projectMd = await FileUtils.readFileSafe(path.join(ccodePath, 'project.md'));
|
|
193
|
+
const archMd = await FileUtils.readFileSafe(path.join(ccodePath, 'architecture.md'));
|
|
194
|
+
const rulesMd = await FileUtils.readFileSafe(path.join(ccodePath, 'rules.md'));
|
|
195
|
+
const memoryMd = await FileUtils.readFileSafe(path.join(ccodePath, 'memory.md'));
|
|
196
|
+
const taskEngine = new TaskEngine();
|
|
197
|
+
await taskEngine.load();
|
|
198
|
+
const tasks = taskEngine.getTasks();
|
|
199
|
+
const stats = taskEngine.getStats();
|
|
200
|
+
const taskList = tasks.map(t => {
|
|
201
|
+
const icon = t.status === 'completed' ? '✓' : t.status === 'in_progress' ? '◐' : '○';
|
|
202
|
+
return `[${icon}] ${t.id} [${t.priority}] ${t.title} — ${t.status}`;
|
|
203
|
+
}).join('\n');
|
|
204
|
+
return `
|
|
205
|
+
=== CONTEXTO DEL PROYECTO ===
|
|
206
|
+
${projectMd}
|
|
207
|
+
|
|
208
|
+
=== ARQUITECTURA ===
|
|
209
|
+
${archMd}
|
|
210
|
+
|
|
211
|
+
=== REGLAS Y ESTÁNDARES ===
|
|
212
|
+
${rulesMd}
|
|
213
|
+
|
|
214
|
+
=== PROGRESO DE TAREAS (${stats.completed}/${stats.total} completadas) ===
|
|
215
|
+
${taskList}
|
|
216
|
+
|
|
217
|
+
=== MEMORIA DE DECISIONES ===
|
|
218
|
+
${memoryMd}
|
|
219
|
+
|
|
220
|
+
=== NOTA ===
|
|
221
|
+
Este contexto fue generado por CCODE. Úsalo como referencia para mantener
|
|
222
|
+
coherencia arquitectónica y seguir las reglas del proyecto.
|
|
223
|
+
`.trim();
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Genera prompt enfocado en una tarea específica.
|
|
227
|
+
*/
|
|
228
|
+
async buildTaskPrompt(taskId) {
|
|
229
|
+
const baseContext = await this.buildContextPrompt();
|
|
230
|
+
let taskFocus = 'No hay una tarea activa seleccionada.';
|
|
231
|
+
if (taskId) {
|
|
232
|
+
const taskEngine = new TaskEngine();
|
|
233
|
+
await taskEngine.load();
|
|
234
|
+
const task = taskEngine.getTaskById(taskId);
|
|
235
|
+
if (task) {
|
|
236
|
+
taskFocus = [
|
|
237
|
+
`TAREA ACTIVA: [${task.id}] ${task.title}`,
|
|
238
|
+
`Criterios de aceptación: ${task.description}`,
|
|
239
|
+
`Prioridad: ${task.priority}`,
|
|
240
|
+
`Módulo: ${task.module}`,
|
|
241
|
+
].join('\n');
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return `${baseContext}
|
|
245
|
+
|
|
246
|
+
=== TAREA EN FOCO ===
|
|
247
|
+
${taskFocus}
|
|
248
|
+
`.trim();
|
|
249
|
+
}
|
|
250
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export type TaskStatus = 'pending' | 'in_progress' | 'completed' | 'failed';
|
|
2
|
+
export type TaskPriority = 'low' | 'medium' | 'high' | 'critical';
|
|
3
|
+
export interface ITask {
|
|
4
|
+
id: string;
|
|
5
|
+
title: string;
|
|
6
|
+
description: string;
|
|
7
|
+
status: TaskStatus;
|
|
8
|
+
priority: TaskPriority;
|
|
9
|
+
module: string;
|
|
10
|
+
}
|
|
11
|
+
export interface ITasksFile {
|
|
12
|
+
tasks: ITask[];
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Gestor de Tareas: manipula el flujo de trabajo en .ccode/tasks.json
|
|
16
|
+
*/
|
|
17
|
+
export declare class TaskEngine {
|
|
18
|
+
private static readonly TASKS_FILE;
|
|
19
|
+
private tasksData;
|
|
20
|
+
load(): Promise<void>;
|
|
21
|
+
save(): Promise<void>;
|
|
22
|
+
getTasks(): ITask[];
|
|
23
|
+
getTaskById(id: string): ITask | undefined;
|
|
24
|
+
addTask(task: Omit<ITask, 'id'>): Promise<string>;
|
|
25
|
+
setTasks(tasks: ITask[]): Promise<void>;
|
|
26
|
+
updateTaskStatus(id: string, status: TaskStatus): Promise<void>;
|
|
27
|
+
getNextTask(): ITask | null;
|
|
28
|
+
getStats(): {
|
|
29
|
+
total: number;
|
|
30
|
+
completed: number;
|
|
31
|
+
in_progress: number;
|
|
32
|
+
pending: number;
|
|
33
|
+
failed: number;
|
|
34
|
+
};
|
|
35
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import * as path from 'path';
|
|
2
|
+
import { FileUtils } from '../utils/files.js';
|
|
3
|
+
/**
|
|
4
|
+
* Gestor de Tareas: manipula el flujo de trabajo en .ccode/tasks.json
|
|
5
|
+
*/
|
|
6
|
+
export class TaskEngine {
|
|
7
|
+
static TASKS_FILE = '.ccode/tasks.json';
|
|
8
|
+
tasksData = { tasks: [] };
|
|
9
|
+
async load() {
|
|
10
|
+
const tasksPath = path.join(process.cwd(), TaskEngine.TASKS_FILE);
|
|
11
|
+
if (await FileUtils.exists(tasksPath)) {
|
|
12
|
+
this.tasksData = await FileUtils.readJson(tasksPath);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
async save() {
|
|
16
|
+
const tasksPath = path.join(process.cwd(), TaskEngine.TASKS_FILE);
|
|
17
|
+
await FileUtils.writeJson(tasksPath, this.tasksData);
|
|
18
|
+
}
|
|
19
|
+
getTasks() {
|
|
20
|
+
return this.tasksData.tasks;
|
|
21
|
+
}
|
|
22
|
+
getTaskById(id) {
|
|
23
|
+
return this.tasksData.tasks.find(t => t.id === id);
|
|
24
|
+
}
|
|
25
|
+
async addTask(task) {
|
|
26
|
+
const id = `TASK-${String(this.tasksData.tasks.length + 1).padStart(3, '0')}`;
|
|
27
|
+
this.tasksData.tasks.push({ id, ...task });
|
|
28
|
+
await this.save();
|
|
29
|
+
return id;
|
|
30
|
+
}
|
|
31
|
+
async setTasks(tasks) {
|
|
32
|
+
this.tasksData.tasks = tasks;
|
|
33
|
+
await this.save();
|
|
34
|
+
}
|
|
35
|
+
async updateTaskStatus(id, status) {
|
|
36
|
+
const task = this.tasksData.tasks.find(t => t.id === id);
|
|
37
|
+
if (!task)
|
|
38
|
+
throw new Error(`Tarea ${id} no encontrada.`);
|
|
39
|
+
task.status = status;
|
|
40
|
+
await this.save();
|
|
41
|
+
}
|
|
42
|
+
getNextTask() {
|
|
43
|
+
const pending = this.tasksData.tasks.filter(t => t.status === 'pending');
|
|
44
|
+
if (pending.length === 0)
|
|
45
|
+
return null;
|
|
46
|
+
const priorities = { critical: 4, high: 3, medium: 2, low: 1 };
|
|
47
|
+
return pending.sort((a, b) => priorities[b.priority] - priorities[a.priority])[0];
|
|
48
|
+
}
|
|
49
|
+
getStats() {
|
|
50
|
+
const tasks = this.tasksData.tasks;
|
|
51
|
+
return {
|
|
52
|
+
total: tasks.length,
|
|
53
|
+
completed: tasks.filter(t => t.status === 'completed').length,
|
|
54
|
+
in_progress: tasks.filter(t => t.status === 'in_progress').length,
|
|
55
|
+
pending: tasks.filter(t => t.status === 'pending').length,
|
|
56
|
+
failed: tasks.filter(t => t.status === 'failed').length,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Capa de abstracción para operaciones de sistema de archivos.
|
|
3
|
+
*/
|
|
4
|
+
export declare class FileUtils {
|
|
5
|
+
static ensureDir(dirPath: string): Promise<void>;
|
|
6
|
+
static writeJson(filePath: string, data: unknown): Promise<void>;
|
|
7
|
+
static writeFile(filePath: string, content: string): Promise<void>;
|
|
8
|
+
static readFile(filePath: string): Promise<string>;
|
|
9
|
+
static readJson<T = unknown>(filePath: string): Promise<T>;
|
|
10
|
+
static exists(itemPath: string): Promise<boolean>;
|
|
11
|
+
static readFileSafe(filePath: string, fallback?: string): Promise<string>;
|
|
12
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import fsExtra from 'fs-extra';
|
|
2
|
+
const fs = fsExtra;
|
|
3
|
+
/**
|
|
4
|
+
* Capa de abstracción para operaciones de sistema de archivos.
|
|
5
|
+
*/
|
|
6
|
+
export class FileUtils {
|
|
7
|
+
static async ensureDir(dirPath) {
|
|
8
|
+
await fs.ensureDir(dirPath);
|
|
9
|
+
}
|
|
10
|
+
static async writeJson(filePath, data) {
|
|
11
|
+
await fs.writeJson(filePath, data, { spaces: 2 });
|
|
12
|
+
}
|
|
13
|
+
static async writeFile(filePath, content) {
|
|
14
|
+
await fs.writeFile(filePath, content, 'utf-8');
|
|
15
|
+
}
|
|
16
|
+
static async readFile(filePath) {
|
|
17
|
+
return await fs.readFile(filePath, 'utf-8');
|
|
18
|
+
}
|
|
19
|
+
static async readJson(filePath) {
|
|
20
|
+
return await fs.readJson(filePath);
|
|
21
|
+
}
|
|
22
|
+
static async exists(itemPath) {
|
|
23
|
+
return await fs.pathExists(itemPath);
|
|
24
|
+
}
|
|
25
|
+
static async readFileSafe(filePath, fallback = '') {
|
|
26
|
+
try {
|
|
27
|
+
if (await fs.pathExists(filePath)) {
|
|
28
|
+
return await fs.readFile(filePath, 'utf-8');
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
catch { /* ignore */ }
|
|
32
|
+
return fallback;
|
|
33
|
+
}
|
|
34
|
+
}
|