@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.
@@ -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
- * Gestiona la configuración y conexión con proveedores de IA.
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 únicamente con la palabra "ok".');
47
- return true;
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
- return false;
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 v2.0'));
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
- const { provider } = await inquirer.prompt([{
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: 'provider',
39
- message: 'Proveedor de IA:',
98
+ name: 'method',
99
+ message: 'Como quieres configurar?',
40
100
  choices: [
41
- { name: ' Claude (Anthropic) — Recomendado', value: 'claude' },
42
- { name: ' OpenAI (ChatGPT)', value: 'openai' },
43
- { name: ' Google Gemini', value: 'gemini' },
44
- { name: ' DeepSeek', value: 'deepseek' },
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 config = { provider };
50
- // Modelos por proveedor
51
- const modelChoices = {
52
- claude: [
53
- { name: 'Claude Sonnet 4 (recomendado)', value: 'claude-sonnet-4-20250514' },
54
- { name: 'Claude Haiku 3.5 (rápido)', value: 'claude-haiku-4-5-20251001' },
55
- { name: 'Claude Opus 4 (máxima calidad)', value: 'claude-opus-4-20250514' },
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
- else {
104
- const { model } = await inquirer.prompt([{
105
- type: 'select',
106
- name: 'model',
107
- message: 'Modelo:',
108
- choices: modelChoices[provider],
109
- }]);
110
- config.model = model;
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
- return config;
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
- const spinner = ora({ text: 'Verificando conexión...', color: 'cyan', spinner: 'dots' }).start();
388
- const success = await AIManager.testConnection(config);
389
- if (success) {
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 pb = new PromptBuilder();
796
- const fullContext = await pb.buildContextPrompt();
797
- const exportPath = path.join(process.cwd(), '.ccode', 'context-export.md');
798
- await FileUtils.writeFile(exportPath, `# CCODE — Project Context Export\n\n${fullContext}`);
799
- showSuccess('Contexto exportado a .ccode/context-export.md');
800
- showInfo('Copia el contenido y pégalo en cualquier chat de IA (ChatGPT, Claude, Gemini, etc.).');
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 conexión
881
- const spinner = ora({ text: ' Probando conexión...', color: 'cyan', spinner: 'dots' }).start();
882
- const connected = await AIManager.testConnection(config);
883
- if (connected) {
884
- spinner.succeed(c.success('Conexión activa'));
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. Archivos del proyecto
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('2.2.0');
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
+ }