@korl3one/ccode 2.2.0 → 3.1.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 +143 -103
- package/dist/ai/claude.d.ts +2 -1
- package/dist/ai/claude.js +15 -12
- package/dist/ai/gemini.d.ts +13 -1
- package/dist/ai/gemini.js +62 -15
- package/dist/ai/manager.d.ts +25 -6
- package/dist/ai/manager.js +83 -21
- package/dist/cli/brand.js +1 -1
- package/dist/cli/index.js +258 -97
- package/dist/core/exports.d.ts +74 -0
- package/dist/core/exports.js +303 -0
- package/package.json +11 -5
- package/dist/ai/deepseek.d.ts +0 -11
- package/dist/ai/deepseek.js +0 -35
- package/dist/ai/groq.d.ts +0 -11
- package/dist/ai/groq.js +0 -35
- package/dist/ai/ollama.d.ts +0 -10
- package/dist/ai/ollama.js +0 -27
- package/dist/ai/openai.d.ts +0 -10
- package/dist/ai/openai.js +0 -34
package/dist/ai/manager.js
CHANGED
|
@@ -1,14 +1,30 @@
|
|
|
1
1
|
import * as path from 'path';
|
|
2
|
+
import axios from 'axios';
|
|
2
3
|
import { ClaudeAdapter } from './claude.js';
|
|
3
|
-
import { OpenAIAdapter } from './openai.js';
|
|
4
4
|
import { GeminiAdapter } from './gemini.js';
|
|
5
|
-
import { DeepSeekAdapter } from './deepseek.js';
|
|
6
|
-
import { GroqAdapter } from './groq.js';
|
|
7
|
-
import { OllamaAdapter } from './ollama.js';
|
|
8
5
|
import { FileUtils } from '../utils/files.js';
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
6
|
+
export const PROVIDER_INFO = {
|
|
7
|
+
gemini: {
|
|
8
|
+
name: 'Google Gemini',
|
|
9
|
+
keyUrl: 'https://aistudio.google.com/apikey',
|
|
10
|
+
envVars: ['GOOGLE_API_KEY', 'GEMINI_API_KEY'],
|
|
11
|
+
models: [
|
|
12
|
+
{ name: 'Gemini 2.5 Flash (recomendado, gratis)', value: 'gemini-2.5-flash' },
|
|
13
|
+
{ name: 'Gemini 2.5 Pro (maxima calidad)', value: 'gemini-2.5-pro' },
|
|
14
|
+
{ name: 'Gemini 2.0 Flash (rapido)', value: 'gemini-2.0-flash' },
|
|
15
|
+
],
|
|
16
|
+
},
|
|
17
|
+
claude: {
|
|
18
|
+
name: 'Claude (Anthropic)',
|
|
19
|
+
keyUrl: 'https://console.anthropic.com/settings/keys',
|
|
20
|
+
envVars: ['ANTHROPIC_API_KEY'],
|
|
21
|
+
models: [
|
|
22
|
+
{ name: 'Claude Sonnet 4 (recomendado)', value: 'claude-sonnet-4-20250514' },
|
|
23
|
+
{ name: 'Claude Haiku 3.5 (rapido)', value: 'claude-haiku-4-5-20251001' },
|
|
24
|
+
{ name: 'Claude Opus 4 (maxima calidad)', value: 'claude-opus-4-20250514' },
|
|
25
|
+
],
|
|
26
|
+
},
|
|
27
|
+
};
|
|
12
28
|
export class AIManager {
|
|
13
29
|
static CONFIG_FILE = '.ccode/config.json';
|
|
14
30
|
static async loadConfig() {
|
|
@@ -24,18 +40,18 @@ export class AIManager {
|
|
|
24
40
|
}
|
|
25
41
|
static getProvider(config) {
|
|
26
42
|
switch (config.provider) {
|
|
43
|
+
case 'gemini': {
|
|
44
|
+
const adapter = new GeminiAdapter({ apiKey: config.apiKey, model: config.model });
|
|
45
|
+
// If using OAuth from Gemini CLI
|
|
46
|
+
if (config.authType === 'oauth') {
|
|
47
|
+
const token = GeminiAdapter.readOAuthToken();
|
|
48
|
+
if (token)
|
|
49
|
+
adapter.setOAuthToken(token);
|
|
50
|
+
}
|
|
51
|
+
return adapter;
|
|
52
|
+
}
|
|
27
53
|
case 'claude':
|
|
28
54
|
return new ClaudeAdapter({ apiKey: config.apiKey, model: config.model });
|
|
29
|
-
case 'openai':
|
|
30
|
-
return new OpenAIAdapter({ apiKey: config.apiKey, model: config.model });
|
|
31
|
-
case 'gemini':
|
|
32
|
-
return new GeminiAdapter({ apiKey: config.apiKey, model: config.model });
|
|
33
|
-
case 'deepseek':
|
|
34
|
-
return new DeepSeekAdapter({ apiKey: config.apiKey, model: config.model });
|
|
35
|
-
case 'groq':
|
|
36
|
-
return new GroqAdapter({ apiKey: config.apiKey, model: config.model });
|
|
37
|
-
case 'ollama':
|
|
38
|
-
return new OllamaAdapter({ model: config.model, baseUrl: config.baseUrl });
|
|
39
55
|
default:
|
|
40
56
|
throw new Error(`Proveedor desconocido: ${config.provider}`);
|
|
41
57
|
}
|
|
@@ -43,11 +59,57 @@ export class AIManager {
|
|
|
43
59
|
static async testConnection(config) {
|
|
44
60
|
try {
|
|
45
61
|
const provider = this.getProvider(config);
|
|
46
|
-
await provider.generate('Responde
|
|
47
|
-
|
|
62
|
+
const result = await provider.generate('Responde unicamente con la palabra "ok".');
|
|
63
|
+
if (!result || result.trim().length === 0) {
|
|
64
|
+
return { ok: false, error: 'La IA respondio pero el contenido esta vacio.' };
|
|
65
|
+
}
|
|
66
|
+
return { ok: true };
|
|
48
67
|
}
|
|
49
|
-
catch {
|
|
50
|
-
|
|
68
|
+
catch (err) {
|
|
69
|
+
if (axios.isAxiosError(err)) {
|
|
70
|
+
const status = err.response?.status;
|
|
71
|
+
const data = err.response?.data;
|
|
72
|
+
if (status === 401 || status === 403) {
|
|
73
|
+
return { ok: false, error: `Credenciales invalidas (HTTP ${status}). Verifica tu autenticacion.` };
|
|
74
|
+
}
|
|
75
|
+
if (status === 404) {
|
|
76
|
+
return { ok: false, error: 'Modelo no encontrado. Verifica el nombre del modelo.' };
|
|
77
|
+
}
|
|
78
|
+
if (status === 429) {
|
|
79
|
+
return { ok: false, error: 'Rate limit. Espera unos segundos e intenta de nuevo.' };
|
|
80
|
+
}
|
|
81
|
+
if (err.code === 'ECONNREFUSED' || err.code === 'ENOTFOUND') {
|
|
82
|
+
return { ok: false, error: 'Sin conexion a internet.' };
|
|
83
|
+
}
|
|
84
|
+
if (err.code === 'ETIMEDOUT' || err.message?.includes('timeout')) {
|
|
85
|
+
return { ok: false, error: 'Timeout: la IA no respondio a tiempo.' };
|
|
86
|
+
}
|
|
87
|
+
const msg = typeof data === 'object' && data?.error?.message
|
|
88
|
+
? data.error.message
|
|
89
|
+
: `Error HTTP ${status || 'desconocido'}`;
|
|
90
|
+
return { ok: false, error: msg };
|
|
91
|
+
}
|
|
92
|
+
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
51
93
|
}
|
|
52
94
|
}
|
|
95
|
+
/**
|
|
96
|
+
* Auto-detect the best available provider.
|
|
97
|
+
* Priority: 1) Gemini CLI OAuth, 2) env vars, 3) null
|
|
98
|
+
*/
|
|
99
|
+
static autoDetect() {
|
|
100
|
+
// 1. Gemini CLI installed and authenticated
|
|
101
|
+
if (GeminiAdapter.isAvailable()) {
|
|
102
|
+
return { provider: 'gemini', authType: 'oauth', source: 'Gemini CLI' };
|
|
103
|
+
}
|
|
104
|
+
// 2. Environment variables
|
|
105
|
+
for (const [provider, info] of Object.entries(PROVIDER_INFO)) {
|
|
106
|
+
for (const envVar of info.envVars) {
|
|
107
|
+
const value = process.env[envVar];
|
|
108
|
+
if (value && value.length > 10) {
|
|
109
|
+
return { provider: provider, authType: 'api-key', apiKey: value, source: envVar };
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
53
115
|
}
|
package/dist/cli/brand.js
CHANGED
|
@@ -27,7 +27,7 @@ export function showLogo() {
|
|
|
27
27
|
LOGO_LINES.forEach((line, i) => {
|
|
28
28
|
console.log(chalk.hex(GRADIENT[i])(line));
|
|
29
29
|
});
|
|
30
|
-
console.log(c.dim(' Context-Persistent AI Development
|
|
30
|
+
console.log(c.dim(' Context-Persistent AI Development v3.0'));
|
|
31
31
|
console.log('');
|
|
32
32
|
}
|
|
33
33
|
// ─── Componentes UI ─────────────────────────────────────────────────
|
package/dist/cli/index.js
CHANGED
|
@@ -4,12 +4,15 @@ import inquirer from 'inquirer';
|
|
|
4
4
|
import ora from 'ora';
|
|
5
5
|
import * as path from 'path';
|
|
6
6
|
import * as fs from 'fs';
|
|
7
|
+
import updateNotifier from 'update-notifier';
|
|
7
8
|
import { showLogo, showWelcome, showHeader, showStep, showSuccess, showError, showWarning, showInfo, showFileTree, showProgressBar, c, } from './brand.js';
|
|
8
9
|
import { FileUtils } from '../utils/files.js';
|
|
9
10
|
import { ContextEngine } from '../core/context.js';
|
|
10
11
|
import { TaskEngine } from '../core/tasks.js';
|
|
11
12
|
import { PromptBuilder } from '../core/prompt-builder.js';
|
|
12
|
-
import { AIManager } from '../ai/manager.js';
|
|
13
|
+
import { AIManager, PROVIDER_INFO } from '../ai/manager.js';
|
|
14
|
+
import { ContextExporter } from '../core/exports.js';
|
|
15
|
+
import { exec } from 'child_process';
|
|
13
16
|
import { FileWatcher, displayChanges } from './watcher.js';
|
|
14
17
|
// ─── Estado global de sesión ────────────────────────────────────────
|
|
15
18
|
const watcher = new FileWatcher();
|
|
@@ -32,84 +35,118 @@ async function requireAI() {
|
|
|
32
35
|
}
|
|
33
36
|
return config;
|
|
34
37
|
}
|
|
38
|
+
function openBrowser(url) {
|
|
39
|
+
const cmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
|
|
40
|
+
exec(`${cmd} "${url}"`);
|
|
41
|
+
}
|
|
35
42
|
async function promptAIConfig() {
|
|
36
|
-
|
|
43
|
+
// 1. Auto-detect — zero config if possible
|
|
44
|
+
const detected = AIManager.autoDetect();
|
|
45
|
+
if (detected) {
|
|
46
|
+
const info = PROVIDER_INFO[detected.provider];
|
|
47
|
+
showSuccess(`${info.name} detectado (${detected.source})`);
|
|
48
|
+
const testSpinner = ora({ text: 'Verificando conexion...', color: 'cyan', spinner: 'dots' }).start();
|
|
49
|
+
const defaultModel = info.models[0].value;
|
|
50
|
+
const config = {
|
|
51
|
+
provider: detected.provider,
|
|
52
|
+
apiKey: detected.apiKey,
|
|
53
|
+
model: defaultModel,
|
|
54
|
+
authType: detected.authType,
|
|
55
|
+
};
|
|
56
|
+
const testResult = await AIManager.testConnection(config);
|
|
57
|
+
if (testResult.ok) {
|
|
58
|
+
testSpinner.succeed(c.success(`Conectado a ${info.name}`));
|
|
59
|
+
const { model } = await inquirer.prompt([{
|
|
60
|
+
type: 'select',
|
|
61
|
+
name: 'model',
|
|
62
|
+
message: 'Modelo:',
|
|
63
|
+
choices: info.models,
|
|
64
|
+
}]);
|
|
65
|
+
config.model = model;
|
|
66
|
+
return config;
|
|
67
|
+
}
|
|
68
|
+
testSpinner.fail(c.error('Sesion expirada'));
|
|
69
|
+
if (detected.authType === 'oauth') {
|
|
70
|
+
showWarning('El token de Gemini CLI expiro.');
|
|
71
|
+
showInfo('Ejecuta "gemini" en otra terminal para refrescar tu sesion.');
|
|
72
|
+
showInfo('Luego vuelve a correr "ccode init".');
|
|
73
|
+
console.log('');
|
|
74
|
+
const { continueManual } = await inquirer.prompt([{
|
|
75
|
+
type: 'confirm', name: 'continueManual',
|
|
76
|
+
message: 'Configurar manualmente mientras tanto?', default: true,
|
|
77
|
+
}]);
|
|
78
|
+
if (!continueManual) {
|
|
79
|
+
throw new Error('Ejecuta "gemini" para refrescar tu sesion y vuelve a intentar.');
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
showError(testResult.error || 'No se pudo conectar.');
|
|
84
|
+
}
|
|
85
|
+
console.log('');
|
|
86
|
+
}
|
|
87
|
+
// 2. Nothing detected — guide setup
|
|
88
|
+
showInfo('No se detecto ningun proveedor de IA.');
|
|
89
|
+
console.log('');
|
|
90
|
+
console.log(c.white(' Opciones para configurar:'));
|
|
91
|
+
console.log(c.accent(' 1. Gemini CLI') + c.dim(' — Instala: npm i -g @anthropic-ai/gemini-cli'));
|
|
92
|
+
console.log(c.dim(' Luego ejecuta "gemini" una vez para autenticarte con Google.'));
|
|
93
|
+
console.log(c.accent(' 2. Variable de entorno') + c.dim(' — export GOOGLE_API_KEY="tu-key"'));
|
|
94
|
+
console.log(c.accent(' 3. Variable de entorno') + c.dim(' — export ANTHROPIC_API_KEY="tu-key"'));
|
|
95
|
+
console.log('');
|
|
96
|
+
const { method } = await inquirer.prompt([{
|
|
37
97
|
type: 'select',
|
|
38
|
-
name: '
|
|
39
|
-
message: '
|
|
98
|
+
name: 'method',
|
|
99
|
+
message: 'Como quieres configurar?',
|
|
40
100
|
choices: [
|
|
41
|
-
{ name: '
|
|
42
|
-
{ name: '
|
|
43
|
-
{ name: '
|
|
44
|
-
{ name: '
|
|
45
|
-
{ name: ' Groq (ultra-rápido)', value: 'groq' },
|
|
46
|
-
{ name: ' Ollama (local, sin API key)', value: 'ollama' },
|
|
101
|
+
{ name: ' Tengo una API Key de Gemini (Google)', value: 'gemini-key' },
|
|
102
|
+
{ name: ' Tengo una API Key de Claude (Anthropic)', value: 'claude-key' },
|
|
103
|
+
{ name: ' Obtener API Key de Gemini gratis (abre navegador)', value: 'gemini-browser' },
|
|
104
|
+
{ name: ' Obtener API Key de Claude (abre navegador)', value: 'claude-browser' },
|
|
47
105
|
],
|
|
48
106
|
}]);
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
openai: [
|
|
58
|
-
{ name: 'GPT-4o (recomendado)', value: 'gpt-4o' },
|
|
59
|
-
{ name: 'GPT-4o mini (rápido)', value: 'gpt-4o-mini' },
|
|
60
|
-
{ name: 'GPT-4.1 (último)', value: 'gpt-4.1' },
|
|
61
|
-
{ name: 'o3-mini (razonamiento)', value: 'o3-mini' },
|
|
62
|
-
],
|
|
63
|
-
gemini: [
|
|
64
|
-
{ name: 'Gemini 2.5 Flash (recomendado)', value: 'gemini-2.5-flash' },
|
|
65
|
-
{ name: 'Gemini 2.5 Pro (máxima calidad)', value: 'gemini-2.5-pro' },
|
|
66
|
-
{ name: 'Gemini 2.0 Flash (rápido)', value: 'gemini-2.0-flash' },
|
|
67
|
-
],
|
|
68
|
-
deepseek: [
|
|
69
|
-
{ name: 'DeepSeek Chat (recomendado)', value: 'deepseek-chat' },
|
|
70
|
-
{ name: 'DeepSeek Reasoner', value: 'deepseek-reasoner' },
|
|
71
|
-
],
|
|
72
|
-
groq: [
|
|
73
|
-
{ name: 'Llama 3.3 70B (recomendado)', value: 'llama-3.3-70b-versatile' },
|
|
74
|
-
{ name: 'Llama 3.1 8B (rápido)', value: 'llama-3.1-8b-instant' },
|
|
75
|
-
{ name: 'Mixtral 8x7B', value: 'mixtral-8x7b-32768' },
|
|
76
|
-
],
|
|
77
|
-
};
|
|
78
|
-
// API Key (todos excepto Ollama)
|
|
79
|
-
if (provider !== 'ollama') {
|
|
80
|
-
const providerNames = {
|
|
81
|
-
claude: 'Anthropic', openai: 'OpenAI', gemini: 'Google AI',
|
|
82
|
-
deepseek: 'DeepSeek', groq: 'Groq',
|
|
83
|
-
};
|
|
84
|
-
const { apiKey } = await inquirer.prompt([{
|
|
85
|
-
type: 'password',
|
|
86
|
-
name: 'apiKey',
|
|
87
|
-
message: `API Key de ${providerNames[provider]}:`,
|
|
88
|
-
mask: '*',
|
|
89
|
-
validate: (v) => v.length > 10 || 'Ingresa una API Key válida',
|
|
90
|
-
}]);
|
|
91
|
-
config.apiKey = apiKey;
|
|
92
|
-
}
|
|
93
|
-
// Selección de modelo
|
|
94
|
-
if (provider === 'ollama') {
|
|
95
|
-
const { model } = await inquirer.prompt([{
|
|
96
|
-
type: 'input',
|
|
97
|
-
name: 'model',
|
|
98
|
-
message: 'Modelo de Ollama:',
|
|
99
|
-
default: 'llama3',
|
|
100
|
-
}]);
|
|
101
|
-
config.model = model;
|
|
107
|
+
const isGemini = method.startsWith('gemini');
|
|
108
|
+
const provider = isGemini ? 'gemini' : 'claude';
|
|
109
|
+
const info = PROVIDER_INFO[provider];
|
|
110
|
+
// Open browser if needed
|
|
111
|
+
if (method.endsWith('-browser')) {
|
|
112
|
+
openBrowser(info.keyUrl);
|
|
113
|
+
showInfo('Se abrio el navegador. Copia tu API Key y pegala aqui.');
|
|
114
|
+
console.log('');
|
|
102
115
|
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
116
|
+
const { apiKey } = await inquirer.prompt([{
|
|
117
|
+
type: 'password',
|
|
118
|
+
name: 'apiKey',
|
|
119
|
+
message: `API Key de ${info.name}:`,
|
|
120
|
+
mask: '*',
|
|
121
|
+
validate: (v) => v.length > 10 || 'API Key muy corta',
|
|
122
|
+
}]);
|
|
123
|
+
const { model } = await inquirer.prompt([{
|
|
124
|
+
type: 'select',
|
|
125
|
+
name: 'model',
|
|
126
|
+
message: 'Modelo:',
|
|
127
|
+
choices: info.models,
|
|
128
|
+
}]);
|
|
129
|
+
const config = { provider, apiKey, model, authType: 'api-key' };
|
|
130
|
+
const testSpinner = ora({ text: 'Verificando conexion...', color: 'cyan', spinner: 'dots' }).start();
|
|
131
|
+
const result = await AIManager.testConnection(config);
|
|
132
|
+
if (result.ok) {
|
|
133
|
+
testSpinner.succeed(c.success('Conexion verificada'));
|
|
134
|
+
const envVar = info.envVars[0];
|
|
135
|
+
showInfo(`Tip: Para que sea automatico la proxima vez:`);
|
|
136
|
+
console.log(c.accent(` export ${envVar}="tu-api-key"`));
|
|
137
|
+
console.log(c.dim(' Agrega esa linea a tu ~/.zshrc o ~/.bashrc'));
|
|
138
|
+
console.log('');
|
|
139
|
+
return config;
|
|
111
140
|
}
|
|
112
|
-
|
|
141
|
+
testSpinner.fail(c.error('Error de conexion'));
|
|
142
|
+
showError(result.error || 'No se pudo conectar.');
|
|
143
|
+
const { retry } = await inquirer.prompt([{
|
|
144
|
+
type: 'confirm', name: 'retry',
|
|
145
|
+
message: 'Intentar de nuevo?', default: true,
|
|
146
|
+
}]);
|
|
147
|
+
if (retry)
|
|
148
|
+
return promptAIConfig();
|
|
149
|
+
throw new Error('Configuracion de IA cancelada.');
|
|
113
150
|
}
|
|
114
151
|
function listProjectFiles(dir, prefix = '') {
|
|
115
152
|
const results = [];
|
|
@@ -179,7 +216,7 @@ async function startSession() {
|
|
|
179
216
|
if (!hasConfig) {
|
|
180
217
|
choices.push({ name: ' 🔌 Conectar proveedor de IA', value: 'connect' });
|
|
181
218
|
}
|
|
182
|
-
choices.push({ name: ' 📋 Generar / actualizar plan de tareas', value: 'plan' }, { name: ` 📊 Ver estado completo`, value: 'status' }, { name: ' 📄 Ver contexto generado', value: 'context' }, { name: ' 🔄 Actualizar contexto (re-analizar proyecto)', value: 'update' }, { name: ' 📤 Exportar contexto para otra IA', value: 'export' }, { name: ' 💡 Explicar proyecto (resumen rápido)', value: 'explain' }, { name: ' 🩺 Doctor (diagnóstico de salud)', value: 'doctor' });
|
|
219
|
+
choices.push({ name: ' 📋 Generar / actualizar plan de tareas', value: 'plan' }, { name: ` 📊 Ver estado completo`, value: 'status' }, { name: ' 📄 Ver contexto generado', value: 'context' }, { name: ' 🔄 Actualizar contexto (re-analizar proyecto)', value: 'update' }, { name: ' 🔗 Sincronizar contexto (AGENTS.md, CLAUDE.md, ...)', value: 'sync' }, { name: ' 📤 Exportar contexto para otra IA', value: 'export' }, { name: ' 💡 Explicar proyecto (resumen rápido)', value: 'explain' }, { name: ' 🩺 Doctor (diagnóstico de salud)', value: 'doctor' });
|
|
183
220
|
if (hasConfig) {
|
|
184
221
|
choices.push({ name: ' 🔌 Reconfigurar IA', value: 'connect' });
|
|
185
222
|
}
|
|
@@ -212,6 +249,7 @@ async function startSession() {
|
|
|
212
249
|
status: handleStatus,
|
|
213
250
|
context: handleContext,
|
|
214
251
|
update: handleUpdate,
|
|
252
|
+
sync: handleSync,
|
|
215
253
|
export: handleExport,
|
|
216
254
|
explain: handleExplain,
|
|
217
255
|
doctor: handleDoctor,
|
|
@@ -341,6 +379,19 @@ async function handleInit() {
|
|
|
341
379
|
{ name: 'config.json ', desc: `IA: ${aiConfig.provider}` },
|
|
342
380
|
]);
|
|
343
381
|
console.log('');
|
|
382
|
+
// ─── Sincronizar contexto a todas las herramientas AI ───
|
|
383
|
+
const syncSpinner = ora({ text: 'Sincronizando contexto con herramientas AI...', color: 'cyan', spinner: 'dots' }).start();
|
|
384
|
+
try {
|
|
385
|
+
const exporter = new ContextExporter();
|
|
386
|
+
const exported = await exporter.exportAll();
|
|
387
|
+
syncSpinner.succeed(c.success('Contexto sincronizado'));
|
|
388
|
+
console.log(c.dim('\n Archivos de contexto generados:\n'));
|
|
389
|
+
showFileTree(exported.map(e => ({ name: e.file, desc: e.label.split('(')[1]?.replace(')', '') || '' })));
|
|
390
|
+
console.log('');
|
|
391
|
+
}
|
|
392
|
+
catch {
|
|
393
|
+
syncSpinner.warn(c.warning('No se pudo sincronizar (los archivos .ccode/ se crearon correctamente)'));
|
|
394
|
+
}
|
|
344
395
|
showSuccess('CCODE se queda activo observando tu proyecto.');
|
|
345
396
|
showInfo('Abre otra terminal y empieza a desarrollar.');
|
|
346
397
|
showInfo('CCODE detectará los cambios automáticamente.');
|
|
@@ -384,17 +435,9 @@ async function handleConnect() {
|
|
|
384
435
|
return;
|
|
385
436
|
}
|
|
386
437
|
const config = await promptAIConfig();
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
spinner.succeed(c.success('Conexión verificada'));
|
|
391
|
-
await AIManager.saveConfig(config);
|
|
392
|
-
showSuccess('Configuración guardada.');
|
|
393
|
-
}
|
|
394
|
-
else {
|
|
395
|
-
spinner.fail(c.error('No se pudo conectar'));
|
|
396
|
-
showError('Verifica tu API Key, conexión a internet, o que Ollama esté corriendo.');
|
|
397
|
-
}
|
|
438
|
+
// promptAIConfig already verifies connection, just save
|
|
439
|
+
await AIManager.saveConfig(config);
|
|
440
|
+
showSuccess('Configuracion guardada.');
|
|
398
441
|
}
|
|
399
442
|
// ─── PLAN ───────────────────────────────────────────────────────────
|
|
400
443
|
async function handlePlan() {
|
|
@@ -780,6 +823,50 @@ Responde ÚNICAMENTE con JSON válido:
|
|
|
780
823
|
console.log(c.accent(' Cambios detectados:'));
|
|
781
824
|
result.changes.forEach(change => console.log(c.dim(` • ${change}`)));
|
|
782
825
|
}
|
|
826
|
+
// Sincronizar exports después de actualizar contexto
|
|
827
|
+
const syncSpinner = ora({ text: 'Sincronizando exports...', color: 'cyan', spinner: 'dots' }).start();
|
|
828
|
+
try {
|
|
829
|
+
const exporter = new ContextExporter();
|
|
830
|
+
const existing = await exporter.detectExisting();
|
|
831
|
+
if (existing.length > 0) {
|
|
832
|
+
for (const format of existing) {
|
|
833
|
+
await exporter.exportFormat(format);
|
|
834
|
+
}
|
|
835
|
+
syncSpinner.succeed(c.success(`${existing.length} export${existing.length > 1 ? 's' : ''} actualizado${existing.length > 1 ? 's' : ''}`));
|
|
836
|
+
}
|
|
837
|
+
else {
|
|
838
|
+
syncSpinner.info(c.dim('Sin exports previos. Usa "export" para generarlos.'));
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
catch {
|
|
842
|
+
syncSpinner.warn(c.warning('No se pudieron actualizar los exports'));
|
|
843
|
+
}
|
|
844
|
+
console.log('');
|
|
845
|
+
}
|
|
846
|
+
catch (error) {
|
|
847
|
+
spinner.fail('Error');
|
|
848
|
+
showError(error instanceof Error ? error.message : String(error));
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
// ─── SYNC ──────────────────────────────────────────────────────────
|
|
852
|
+
async function handleSync() {
|
|
853
|
+
if (!(await requireInit()))
|
|
854
|
+
return;
|
|
855
|
+
showHeader('Sincronizar Contexto', 'Genera archivos de contexto para cada herramienta AI');
|
|
856
|
+
const exporter = new ContextExporter();
|
|
857
|
+
const existing = await exporter.detectExisting();
|
|
858
|
+
if (existing.length > 0) {
|
|
859
|
+
showInfo(`Exports existentes: ${existing.map(f => ContextExporter.FORMATS[f].file).join(', ')}`);
|
|
860
|
+
}
|
|
861
|
+
const spinner = ora({ text: 'Generando archivos de contexto...', color: 'cyan', spinner: 'dots' }).start();
|
|
862
|
+
try {
|
|
863
|
+
const exported = await exporter.exportAll();
|
|
864
|
+
spinner.succeed(c.success('Contexto sincronizado con todas las herramientas'));
|
|
865
|
+
console.log('');
|
|
866
|
+
showFileTree(exported.map(e => ({ name: e.file, desc: e.label.split('(')[1]?.replace(')', '') || '' })));
|
|
867
|
+
console.log('');
|
|
868
|
+
showInfo('Cada herramienta AI leera su archivo automaticamente.');
|
|
869
|
+
showInfo('Los archivos se actualizan con cada "sync", "update" o "init".');
|
|
783
870
|
console.log('');
|
|
784
871
|
}
|
|
785
872
|
catch (error) {
|
|
@@ -792,12 +879,61 @@ async function handleExport() {
|
|
|
792
879
|
if (!(await requireInit()))
|
|
793
880
|
return;
|
|
794
881
|
showHeader('Exportar Contexto');
|
|
795
|
-
const
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
882
|
+
const { mode } = await inquirer.prompt([{
|
|
883
|
+
type: 'select',
|
|
884
|
+
name: 'mode',
|
|
885
|
+
message: 'Tipo de export:',
|
|
886
|
+
choices: [
|
|
887
|
+
{ name: ' Sincronizar con todas las herramientas AI', value: 'all' },
|
|
888
|
+
{ name: ' Solo export universal (.md para copiar/pegar)', value: 'universal' },
|
|
889
|
+
{ name: ' Elegir herramientas específicas', value: 'pick' },
|
|
890
|
+
],
|
|
891
|
+
}]);
|
|
892
|
+
const exporter = new ContextExporter();
|
|
893
|
+
if (mode === 'universal') {
|
|
894
|
+
const content = await exporter.generateUniversalExport();
|
|
895
|
+
const exportPath = path.join(process.cwd(), '.ccode', 'context-export.md');
|
|
896
|
+
await FileUtils.writeFile(exportPath, content);
|
|
897
|
+
showSuccess('Contexto exportado a .ccode/context-export.md');
|
|
898
|
+
showInfo('Copia el contenido y pegalo en cualquier chat de IA.');
|
|
899
|
+
console.log('');
|
|
900
|
+
return;
|
|
901
|
+
}
|
|
902
|
+
if (mode === 'pick') {
|
|
903
|
+
const formats = Object.entries(ContextExporter.FORMATS);
|
|
904
|
+
const existing = await exporter.detectExisting();
|
|
905
|
+
const { selected } = await inquirer.prompt([{
|
|
906
|
+
type: 'checkbox',
|
|
907
|
+
name: 'selected',
|
|
908
|
+
message: 'Selecciona los formatos:',
|
|
909
|
+
choices: formats.map(([key, info]) => ({
|
|
910
|
+
name: ` ${info.label}${existing.includes(key) ? c.dim(' (existe)') : ''}`,
|
|
911
|
+
value: key,
|
|
912
|
+
checked: true,
|
|
913
|
+
})),
|
|
914
|
+
}]);
|
|
915
|
+
if (selected.length === 0) {
|
|
916
|
+
showWarning('No se selecciono ningun formato.');
|
|
917
|
+
return;
|
|
918
|
+
}
|
|
919
|
+
const spinner = ora({ text: 'Generando exports...', color: 'cyan', spinner: 'dots' }).start();
|
|
920
|
+
for (const format of selected) {
|
|
921
|
+
await exporter.exportFormat(format);
|
|
922
|
+
}
|
|
923
|
+
spinner.succeed(c.success(`${selected.length} formato${selected.length > 1 ? 's' : ''} exportado${selected.length > 1 ? 's' : ''}`));
|
|
924
|
+
for (const format of selected) {
|
|
925
|
+
const info = ContextExporter.FORMATS[format];
|
|
926
|
+
console.log(c.dim(` ${info.file}`));
|
|
927
|
+
}
|
|
928
|
+
console.log('');
|
|
929
|
+
return;
|
|
930
|
+
}
|
|
931
|
+
// mode === 'all'
|
|
932
|
+
const spinner = ora({ text: 'Sincronizando con todas las herramientas AI...', color: 'cyan', spinner: 'dots' }).start();
|
|
933
|
+
const exported = await exporter.exportAll();
|
|
934
|
+
spinner.succeed(c.success('Contexto sincronizado'));
|
|
935
|
+
console.log(c.dim('\n Archivos generados:\n'));
|
|
936
|
+
showFileTree(exported.map(e => ({ name: e.file, desc: e.label.split('(')[1]?.replace(')', '') || '' })));
|
|
801
937
|
console.log('');
|
|
802
938
|
}
|
|
803
939
|
// ─── EXPLAIN ────────────────────────────────────────────────────────
|
|
@@ -877,14 +1013,15 @@ async function handleDoctor() {
|
|
|
877
1013
|
const config = await AIManager.loadConfig();
|
|
878
1014
|
if (config) {
|
|
879
1015
|
console.log(c.success(` ✓ Configurado: ${config.provider} (${config.model || 'default'})`));
|
|
880
|
-
// Test de
|
|
881
|
-
const spinner = ora({ text: ' Probando
|
|
882
|
-
const
|
|
883
|
-
if (
|
|
884
|
-
spinner.succeed(c.success('
|
|
1016
|
+
// Test de conexion
|
|
1017
|
+
const spinner = ora({ text: ' Probando conexion...', color: 'cyan', spinner: 'dots' }).start();
|
|
1018
|
+
const connResult = await AIManager.testConnection(config);
|
|
1019
|
+
if (connResult.ok) {
|
|
1020
|
+
spinner.succeed(c.success('Conexion activa'));
|
|
885
1021
|
}
|
|
886
1022
|
else {
|
|
887
1023
|
spinner.fail(c.error('No se pudo conectar'));
|
|
1024
|
+
showError(` ${connResult.error}`);
|
|
888
1025
|
issues++;
|
|
889
1026
|
}
|
|
890
1027
|
}
|
|
@@ -915,7 +1052,26 @@ async function handleDoctor() {
|
|
|
915
1052
|
issues++;
|
|
916
1053
|
}
|
|
917
1054
|
}
|
|
918
|
-
// 4.
|
|
1055
|
+
// 4. Exports de contexto
|
|
1056
|
+
console.log('');
|
|
1057
|
+
console.log(c.bold(' Exports de contexto'));
|
|
1058
|
+
const exporter = new ContextExporter();
|
|
1059
|
+
const existingExports = await exporter.detectExisting();
|
|
1060
|
+
if (existingExports.length > 0) {
|
|
1061
|
+
for (const format of existingExports) {
|
|
1062
|
+
const info = ContextExporter.FORMATS[format];
|
|
1063
|
+
console.log(c.success(` ✓ ${info.file}`));
|
|
1064
|
+
}
|
|
1065
|
+
const missing = Object.keys(ContextExporter.FORMATS).length - existingExports.length;
|
|
1066
|
+
if (missing > 0) {
|
|
1067
|
+
console.log(c.dim(` ${missing} formato${missing > 1 ? 's' : ''} disponible${missing > 1 ? 's' : ''} sin generar`));
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
else {
|
|
1071
|
+
console.log(c.warning(' ⚠ Sin exports — ejecuta "Sincronizar contexto" para generarlos'));
|
|
1072
|
+
issues++;
|
|
1073
|
+
}
|
|
1074
|
+
// 5. Archivos del proyecto
|
|
919
1075
|
console.log('');
|
|
920
1076
|
console.log(c.bold(' Proyecto'));
|
|
921
1077
|
const projectFiles = listProjectFiles(process.cwd());
|
|
@@ -935,11 +1091,15 @@ async function handleDoctor() {
|
|
|
935
1091
|
}
|
|
936
1092
|
// ─── CLI Setup ──────────────────────────────────────────────────────
|
|
937
1093
|
async function main() {
|
|
1094
|
+
// Notificar si hay una version nueva disponible
|
|
1095
|
+
const pkg = JSON.parse(fs.readFileSync(new URL('../../package.json', import.meta.url), 'utf-8'));
|
|
1096
|
+
const notifier = updateNotifier({ pkg, updateCheckInterval: 1000 * 60 * 60 * 4 }); // cada 4 horas
|
|
1097
|
+
notifier.notify({ isGlobal: true });
|
|
938
1098
|
const program = new Command();
|
|
939
1099
|
program
|
|
940
1100
|
.name('ccode')
|
|
941
1101
|
.description('CCODE: Contexto Persistente para Desarrollo con IA')
|
|
942
|
-
.version(
|
|
1102
|
+
.version(pkg.version);
|
|
943
1103
|
// Comandos individuales (para uso rápido sin sesión)
|
|
944
1104
|
program.command('init').description('Inicializa el contexto del proyecto').action(async () => {
|
|
945
1105
|
await handleInit();
|
|
@@ -954,6 +1114,7 @@ async function main() {
|
|
|
954
1114
|
program.command('status').description('Estado del proyecto').action(handleStatus);
|
|
955
1115
|
program.command('context').description('Ver contexto generado').action(handleContext);
|
|
956
1116
|
program.command('update').description('Re-analiza y actualiza el contexto').action(handleUpdate);
|
|
1117
|
+
program.command('sync').description('Sincroniza contexto con todas las herramientas AI').action(handleSync);
|
|
957
1118
|
program.command('export').description('Exporta contexto como .md para cualquier IA').action(handleExport);
|
|
958
1119
|
program.command('explain').description('Resumen rápido del proyecto').action(handleExplain);
|
|
959
1120
|
program.command('doctor').description('Diagnóstico de salud del proyecto').action(handleDoctor);
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ContextExporter: genera archivos de contexto para cada herramienta de IA.
|
|
3
|
+
*
|
|
4
|
+
* Lee la fuente de verdad (.ccode/) y produce exports específicos:
|
|
5
|
+
* - AGENTS.md → Estándar abierto (Linux Foundation)
|
|
6
|
+
* - CLAUDE.md → Claude Code
|
|
7
|
+
* - GEMINI.md → Gemini CLI
|
|
8
|
+
* - .cursorrules → Cursor
|
|
9
|
+
* - copilot-instructions.md → GitHub Copilot
|
|
10
|
+
*
|
|
11
|
+
* Cada export se adapta al formato y expectativas de la herramienta destino.
|
|
12
|
+
*/
|
|
13
|
+
export declare class ContextExporter {
|
|
14
|
+
private readonly ccodePath;
|
|
15
|
+
private readonly projectRoot;
|
|
16
|
+
static readonly FORMATS: Record<string, {
|
|
17
|
+
file: string;
|
|
18
|
+
label: string;
|
|
19
|
+
}>;
|
|
20
|
+
constructor(projectRoot?: string);
|
|
21
|
+
/**
|
|
22
|
+
* Lee todos los archivos de contexto de .ccode/
|
|
23
|
+
*/
|
|
24
|
+
private readSources;
|
|
25
|
+
/**
|
|
26
|
+
* Formatea las tareas como lista legible.
|
|
27
|
+
*/
|
|
28
|
+
private formatTasks;
|
|
29
|
+
/**
|
|
30
|
+
* Genera AGENTS.md — Estándar abierto compatible con múltiples herramientas.
|
|
31
|
+
* Sigue la convención de AGENTS.md (Linux Foundation / AAIF).
|
|
32
|
+
*/
|
|
33
|
+
generateAgentsMd(): Promise<string>;
|
|
34
|
+
/**
|
|
35
|
+
* Genera CLAUDE.md — Optimizado para Claude Code.
|
|
36
|
+
* Claude Code lee este archivo automáticamente del root del proyecto.
|
|
37
|
+
*/
|
|
38
|
+
generateClaudeMd(): Promise<string>;
|
|
39
|
+
/**
|
|
40
|
+
* Genera GEMINI.md — Para Gemini CLI.
|
|
41
|
+
*/
|
|
42
|
+
generateGeminiMd(): Promise<string>;
|
|
43
|
+
/**
|
|
44
|
+
* Genera .cursorrules — Instrucciones para Cursor IDE.
|
|
45
|
+
* Cursor lee este archivo del root del proyecto automáticamente.
|
|
46
|
+
*/
|
|
47
|
+
generateCursorRules(): Promise<string>;
|
|
48
|
+
/**
|
|
49
|
+
* Genera copilot-instructions.md — Para GitHub Copilot.
|
|
50
|
+
* Se ubica en .github/copilot-instructions.md
|
|
51
|
+
*/
|
|
52
|
+
generateCopilotInstructions(): Promise<string>;
|
|
53
|
+
/**
|
|
54
|
+
* Genera el export universal (.ccode/context-export.md).
|
|
55
|
+
*/
|
|
56
|
+
generateUniversalExport(): Promise<string>;
|
|
57
|
+
/**
|
|
58
|
+
* Genera y escribe un formato específico.
|
|
59
|
+
*/
|
|
60
|
+
exportFormat(format: string): Promise<string>;
|
|
61
|
+
/**
|
|
62
|
+
* Exporta todos los formatos de una vez.
|
|
63
|
+
* Retorna la lista de archivos generados.
|
|
64
|
+
*/
|
|
65
|
+
exportAll(): Promise<Array<{
|
|
66
|
+
format: string;
|
|
67
|
+
file: string;
|
|
68
|
+
label: string;
|
|
69
|
+
}>>;
|
|
70
|
+
/**
|
|
71
|
+
* Verifica qué formatos ya existen en el proyecto.
|
|
72
|
+
*/
|
|
73
|
+
detectExisting(): Promise<string[]>;
|
|
74
|
+
}
|