zyn-ai 1.3.2 → 1.3.4

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.
@@ -43,6 +43,7 @@ const TOOL_DEFINITIONS = [
43
43
  { name: 'web_search', usage: '{ query, lang?, limit? }' },
44
44
  { name: 'web_read', usage: '{ url }' },
45
45
  { name: 'create_canvas_image', usage: '{ width, height, background?, elements?, format?, outputPath? }' },
46
+ { name: 'git', usage: '{ provider, action, method?, path?, body?, headers?, name?, repoUrl?, destination?, branch?, timeoutMs? }' },
46
47
  ];
47
48
  const REGISTERED_TOOLS = new Set(TOOL_DEFINITIONS.map(tool => tool.name));
48
49
 
@@ -54,7 +55,7 @@ function getToolPromptText() {
54
55
  ' Lista archivos y carpetas ordenados. Sin path usa directorio actual.',
55
56
  '',
56
57
  'read_file { path, startLine?, endLine? }',
57
- ' Lee contenido con numeros de linea. Max 500 lineas por llamada.',
58
+ ' Lee contenido con numeros de linea. Max 5000 lineas por llamada.',
58
59
  ' Para archivos grandes, usa startLine/endLine para leer por secciones.',
59
60
  '',
60
61
  'search_text { pattern, path?, glob? }',
@@ -92,6 +93,9 @@ function getToolPromptText() {
92
93
  ' Retorna exit code, stdout y stderr.',
93
94
  ' Ejecuta la accion directamente. No expliques pasos al usuario salvo que sea estrictamente necesario.',
94
95
  ' Usa flags no-interactivos: -y, --yes, --no-pager, DEBIAN_FRONTEND=noninteractive.',
96
+ ' Ejemplo instalar: {"type":"tool","tool":"run_command","args":{"command":"apt-get update && apt-get install -y curl"}}',
97
+ ' Ejemplo git: {"type":"tool","tool":"run_command","args":{"command":"git log --oneline -10"}}',
98
+ ' Ejemplo npm: {"type":"tool","tool":"run_command","args":{"command":"npm install express --save"}}',
95
99
  '',
96
100
  '## Web',
97
101
  '',
@@ -100,9 +104,14 @@ function getToolPromptText() {
100
104
  ' Con selector CSS (ej: "h1", ".price"): extrae texto de elementos.',
101
105
  ' Con selector + attribute (ej: "href", "src"): extrae atributo.',
102
106
  ' limit: max elementos a extraer (default: 20, max: 50).',
107
+ ' Ejemplo (extraer titulos): {"type":"tool","tool":"fetch_url","args":{"url":"https://news.ycombinator.com","selector":".titleline > a"}}',
108
+ ' Ejemplo (extraer links de imagenes): {"type":"tool","tool":"fetch_url","args":{"url":"https://example.com","selector":"img","attribute":"src","limit":10}}',
103
109
  '',
104
110
  'fetch_http { url, method?, headers?, query?, json?, data?, form?, files?, timeoutMs? }',
105
111
  ' Cliente HTTP avanzado: soporta headers custom, query params, body JSON/texto, form-data y adjuntar archivos.',
112
+ ' Ejemplo GET: {"type":"tool","tool":"fetch_http","args":{"url":"https://api.github.com/users/octocat","method":"GET"}}',
113
+ ' Ejemplo POST JSON: {"type":"tool","tool":"fetch_http","args":{"url":"https://api.example.com/items","method":"POST","json":true,"data":{"name":"test","value":42},"headers":{"Authorization":"Bearer token123"}}}',
114
+ ' Ejemplo con query params: {"type":"tool","tool":"fetch_http","args":{"url":"https://api.example.com/search","query":{"q":"hello","page":1}}}',
106
115
  '',
107
116
  'fetch { url, method?, headers?, query?, json?, data?, form?, files?, timeoutMs? }',
108
117
  ' Alias profesional recomendado para solicitudes HTTP avanzadas.',
@@ -112,6 +121,8 @@ function getToolPromptText() {
112
121
  '',
113
122
  'scrape_site { url, selectors, limit?, headers? }',
114
123
  ' Scraping avanzado con multiples selectores en una sola llamada.',
124
+ ' selectors: objeto con nombres y selectores CSS. Ejemplo:',
125
+ ' {"type":"tool","tool":"scrape_site","args":{"url":"https://news.ycombinator.com","selectors":{"titulos":".titleline > a","urls":".titleline > a","scores":".score"},"limit":10}}',
115
126
  '',
116
127
  'web_search { query, lang?, limit? }',
117
128
  ' Busca en la web via DuckDuckGo. Retorna titulo, URL y snippet de los primeros resultados.',
@@ -130,9 +141,70 @@ function getToolPromptText() {
130
141
  ' width/height son obligatorios. background puede ser color HEX (#RRGGBB o #RRGGBBAA).',
131
142
  ' elements permite combinar rect, circle/ellipse, line, text e image.',
132
143
  ' Usa este flujo profesional: definir lienzo -> capas base -> tipografia -> detalles -> exportacion.',
133
- ' Ejemplo:',
144
+ '',
145
+ ' Tipos de elementos soportados:',
146
+ ' rect: { type:"rect", x, y, w, h, fill?, radius?, stroke? } — rectangulos y tarjetas',
147
+ ' circle/ellipse: { type:"circle", x, y, r } o { type:"ellipse", x, y, rx, ry, fill }',
148
+ ' line: { type:"line", x1, y1, x2, y2, stroke } — lineas y separadores',
149
+ ' image: { type:"image", src, x, y, w?, h? } — insertar imagenes externas o locales',
150
+ ' text: { type:"text", x, y, text, fontSize?, maxWidth? } — texto libre',
151
+ '',
152
+ ' === TUTORIALES DE USO ===',
153
+ '',
154
+ ' POST DE NOTICIA (1200x630 — formato Facebook/LinkedIn):',
155
+ ' {"type":"tool","tool":"create_canvas_image","args":{"width":1200,"height":630,"background":"#1a1a2e","format":"jpg","outputPath":"generated/news-post.jpg","elements":[{"type":"rect","x":0,"y":0,"w":1200,"h":8,"fill":"#e94560"},{"type":"rect","x":60,"y":40,"w":1080,"h":550,"radius":16,"fill":"#16213e"},{"type":"text","x":100,"y":80,"fontSize":32,"text":"BREAKING NEWS"},{"type":"rect","x":100,"y":125,"w":200,"h":4,"fill":"#e94560"},{"type":"text","x":100,"y":160,"fontSize":64,"maxWidth":1000,"text":"Nuevo avance en inteligencia artificial revoluciona la industria"},{"type":"text","x":100,"y":400,"fontSize":24,"text":"Fuente: Tech Daily — Hace 2 horas"},{"type":"rect","x":0,"y":580,"w":1200,"h":50,"fill":"#0f3460"},{"type":"text","x":100,"y":590,"fontSize":16,"text":"@noticias_tech"}]}}',
156
+ '',
157
+ ' POST DE TWITTER/X (1600x900):',
158
+ ' {"type":"tool","tool":"create_canvas_image","args":{"width":1600,"height":900,"background":"#000000","format":"png","outputPath":"generated/twitter-post.png","elements":[{"type":"rect","x":80,"y":80,"w":1440,"h":740,"radius":32,"fill":"#111111"},{"type":"rect","x":120,"y":120,"w":60,"h":60,"radius":30,"fill":"#1da1f2"},{"type":"text","x":200,"y":130,"fontSize":32,"text":"@usuario"},{"type":"text","x":120,"y":240,"fontSize":48,"maxWidth":1360,"text":"Este es un ejemplo de tweet con imagen generada automaticamente"},{"type":"text","x":120,"y":600,"fontSize":24,"text":"10:30 AM · 5 May 2026"},{"type":"rect","x":120,"y":680,"w":200,"h":40,"radius":20,"fill":"#1da1f2"},{"type":"text","x":160,"y":688,"fontSize":16,"text":"♡ 1.2K ↗ 340"}]}}',
159
+ '',
160
+ ' INSTAGRAM POST CUADRADO (1080x1080):',
161
+ ' {"type":"tool","tool":"create_canvas_image","args":{"width":1080,"height":1080,"background":"#f8f0e3","format":"jpg","outputPath":"generated/instagram-post.jpg","elements":[{"type":"rect","x":40,"y":40,"w":1000,"h":1000,"radius":20,"fill":"#ffffff"},{"type":"rect","x":80,"y":80,"w":920,"h":600,"radius":12,"fill":"#e8d5b7"},{"type":"text","x":120,"y":140,"fontSize":56,"maxWidth":840,"text":"Tips de productividad para developers"},{"type":"text","x":120,"y":740,"fontSize":36,"maxWidth":840,"text":"1. Usa Pomodoro\\n2. Bloquea distracciones\\n3. Planifica tu dia"},{"type":"text","x":120,"y":960,"fontSize":24,"text":"@dev_tips"}]}}',
162
+ '',
163
+ ' THUMBNAIL DE YOUTUBE (1280x720):',
164
+ ' {"type":"tool","tool":"create_canvas_image","args":{"width":1280,"height":720,"background":"#ff0000","format":"jpg","outputPath":"generated/youtube-thumb.jpg","elements":[{"type":"rect","x":0,"y":0,"w":1280,"h":720,"fill":"#1a1a1a"},{"type":"rect","x":60,"y":60,"w":1160,"h":600,"radius":24,"fill":"#2a2a2a"},{"type":"text","x":100,"y":120,"fontSize":72,"maxWidth":1080,"text":"APRENDER JAVASCRIPT"},{"type":"text","x":100,"y":220,"fontSize":48,"maxWidth":1080,"text":"en 10 minutos"},{"type":"rect","x":100,"y":480,"w":300,"h":80,"radius":40,"fill":"#ff0000"},{"type":"text","x":160,"y":500,"fontSize":32,"text":"▶ PLAY"}]}}',
165
+ '',
166
+ ' BANNER DE GITHUB/REPO (1280x320):',
167
+ ' {"type":"tool","tool":"create_canvas_image","args":{"width":1280,"height":320,"background":"#24292e","format":"png","outputPath":"generated/repo-banner.png","elements":[{"type":"rect","x":0,"y":0,"w":1280,"h":4,"fill":"#0366d6"},{"type":"text","x":80,"y":80,"fontSize":64,"text":"mi-proyecto"},{"type":"text","x":80,"y":170,"fontSize":28,"text":"Una descripcion genial del proyecto en pocas palabras"},{"type":"rect","x":80,"y":230,"w":16,"h":16,"radius":8,"fill":"#2ea44f"},{"type":"text","x":106,"y":230,"fontSize":20,"text":"MIT License"}]}}',
168
+ '',
169
+ ' CARTEL DE EVENTO (800x1000):',
170
+ ' {"type":"tool","tool":"create_canvas_image","args":{"width":800,"height":1000,"background":"#0d1b2a","format":"png","outputPath":"generated/evento.png","elements":[{"type":"rect","x":40,"y":40,"w":720,"h":920,"radius":24,"fill":"#1b2838"},{"type":"text","x":80,"y":100,"fontSize":48,"text":"MEETUP TECH"},{"type":"rect","x":80,"y":170,"w":200,"h":4,"fill":"#00d4ff"},{"type":"text","x":80,"y":220,"fontSize":36,"text":"Inteligencia Artificial"},{"type":"text","x":80,"y":280,"fontSize":24,"text":"y Desarrollo Moderno"},{"type":"text","x":80,"y":500,"fontSize":28,"text":"20 de Mayo 2026"},{"type":"text","x":80,"y":560,"fontSize":24,"text":"18:00 hrs"},{"type":"text","x":80,"y":640,"fontSize":20,"text":"Centro de Innovacion"},{"type":"rect","x":80,"y":780,"w":640,"h":80,"radius":40,"fill":"#00d4ff"},{"type":"text","x":240,"y":800,"fontSize":28,"text":"REGISTRATE"}]}}',
171
+ '',
172
+ ' DIBUJO SIMPLE — PAISAJE GEOMETRICO (800x600):',
173
+ ' {"type":"tool","tool":"create_canvas_image","args":{"width":800,"height":600,"background":"#87CEEB","format":"png","outputPath":"generated/paisaje.png","elements":[{"type":"rect","x":0,"y":400,"w":800,"h":200,"fill":"#228B22"},{"type":"circle","x":650,"y":100,"r":60,"fill":"#FFD700"},{"type":"rect","x":100,"y":250,"w":80,"h":150,"fill":"#8B4513"},{"type":"circle","x":140,"y":230,"r":60,"fill":"#006400"},{"type":"rect","x":300,"y":200,"w":60,"h":200,"fill":"#8B4513"},{"type":"circle","x":330,"y":180,"r":70,"fill":"#006400"},{"type":"rect","x":500,"y":280,"w":70,"h":120,"fill":"#8B4513"},{"type":"circle","x":535,"y":260,"r":55,"fill":"#006400"},{"type":"rect","x":0,"y":450,"w":800,"h":60,"fill":"#4169E1"}]}}',
174
+ '',
175
+ ' TARJETA DE PERFIL (600x350):',
176
+ ' {"type":"tool","tool":"create_canvas_image","args":{"width":600,"height":350,"background":"#f0f0f0","format":"png","outputPath":"generated/profile-card.png","elements":[{"type":"rect","x":0,"y":0,"w":600,"h":120,"fill":"#4a90d9"},{"type":"circle","x":300,"y":120,"r":70,"fill":"#ffffff"},{"type":"text","x":300,"y":210,"fontSize":28,"text":"Nombre Apellido"},{"type":"text","x":300,"y":250,"fontSize":16,"text":"Software Developer"},{"type":"text","x":300,"y":280,"fontSize":14,"text":"username@email.com"}]}}',
177
+ '',
178
+ ' INFOGRAFIA SIMPLE (800x1200):',
179
+ ' {"type":"tool","tool":"create_canvas_image","args":{"width":800,"height":1200,"background":"#1e1e2e","format":"png","outputPath":"generated/infografia.png","elements":[{"type":"rect","x":40,"y":40,"w":720,"h":100,"radius":16,"fill":"#333355"},{"type":"text","x":80,"y":70,"fontSize":40,"text":"ESTADISTICAS 2026"},{"type":"rect","x":40,"y":180,"w":720,"h":120,"radius":12,"fill":"#2a2a4a"},{"type":"circle","x":100,"y":240,"r":30,"fill":"#4fc3f7"},{"type":"text","x":150,"y":220,"fontSize":36,"text":"85%"},{"type":"text","x":150,"y":260,"fontSize":18,"text":"desarrolladores usan IA"},{"type":"rect","x":40,"y":340,"w":720,"h":120,"radius":12,"fill":"#2a2a4a"},{"type":"circle","x":100,"y":400,"r":30,"fill":"#81c784"},{"type":"text","x":150,"y":380,"fontSize":36,"text":"3.2x"},{"type":"text","x":150,"y":420,"fontSize":18,"text":"mas rapido con herramientas"},{"type":"rect","x":40,"y":500,"w":720,"h":120,"radius":12,"fill":"#2a2a4a"},{"type":"circle","x":100,"y":560,"r":30,"fill":"#ffb74d"},{"type":"text","x":150,"y":540,"fontSize":36,"text":"120k"},{"type":"text","x":150,"y":580,"fontSize":18,"text":"proyectos creados este mes"}]}}',
180
+ '',
181
+ ' POST DE CITAS/FRASES (1080x1080):',
182
+ ' {"type":"tool","tool":"create_canvas_image","args":{"width":1080,"height":1080,"background":"#000000","format":"jpg","outputPath":"generated/quote.jpg","elements":[{"type":"rect","x":100,"y":100,"w":880,"h":880,"radius":0,"fill":"#111111"},{"type":"rect","x":200,"y":300,"w":680,"h":4,"fill":"#ffffff"},{"type":"text","x":150,"y":200,"fontSize":80,"text":"\\""},{"type":"text","x":150,"y":340,"fontSize":48,"maxWidth":780,"text":"El codigo es poesia que las maquinas pueden leer."},{"type":"text","x":150,"y":700,"fontSize":32,"text":"— Autor Desconocido"}]}}',
183
+ '',
184
+ ' Ejemplo basico:',
134
185
  ' {"type":"tool","tool":"create_canvas_image","args":{"width":1200,"height":628,"background":"#0f172a","format":"png","outputPath":"generated/cover.png","elements":[{"type":"rect","x":48,"y":48,"w":1104,"h":532,"radius":24,"fill":"#111827"},{"type":"text","x":96,"y":120,"fontSize":32,"text":"Quarterly Business Report"}]}}',
135
186
  '',
187
+ '## Git - Control total de API',
188
+ '',
189
+ 'git { provider, action, method?, path?, body?, headers?, name?, repoUrl?, destination?, branch?, timeoutMs? }',
190
+ ' Unica herramienta Git. Control total sobre la API del proveedor.',
191
+ ' provider: "github", "gitlab" o "custom". name: identificador para perfil custom.',
192
+ ' action: "api" | "clone"',
193
+ '',
194
+ ' action="api" — cualquier operacion HTTP sobre la API del proveedor:',
195
+ ' method: GET, POST, PATCH, PUT, DELETE. path: ruta de la API sin / inicial.',
196
+ ' body: objeto JSON para POST/PATCH/PUT. headers: headers adicionales opcionales.',
197
+ ' Ejemplo: {"type":"tool","tool":"git","args":{"provider":"github","action":"api","method":"POST","path":"user/repos","body":{"name":"mi-proyecto","private":true}}}',
198
+ ' Ejemplo: {"type":"tool","tool":"git","args":{"provider":"github","action":"api","method":"GET","path":"repos/owner/repo/issues?state=open"}}',
199
+ ' Ejemplo: {"type":"tool","tool":"git","args":{"provider":"custom","name":"empresa","action":"api","method":"POST","path":"projects","body":{"name":"nuevo"}}}',
200
+ '',
201
+ ' action="clone" — clonar repositorio con credenciales configuradas:',
202
+ ' repoUrl: URL del repositorio. destination: carpeta destino. branch: rama especifica.',
203
+ ' Ejemplo: {"type":"tool","tool":"git","args":{"provider":"github","action":"clone","repoUrl":"https://github.com/user/repo","destination":"./repo"}}',
204
+ '',
205
+ ' Control total: repos, issues, PRs, releases, webhooks, users, etc. Segun permisos del token.',
206
+ ' No hay acciones fijas. Elige method y path libremente.',
207
+ '',
136
208
  ].join('\n');
137
209
  }
138
210
 
@@ -186,6 +258,8 @@ function describeToolCall(call) {
186
258
  }
187
259
  case 'create_canvas_image':
188
260
  return `Creando imagen ${call.args.width || '?'}x${call.args.height || '?'}`;
261
+ case 'git':
262
+ return `Git ${call.args.action || '?'} ${call.args.provider || '?'}`;
189
263
  default:
190
264
  return call.tool;
191
265
  }
@@ -1084,6 +1158,9 @@ async function executeToolCall(call, state, ui) {
1084
1158
  case 'create_canvas_image':
1085
1159
  result = await createCanvasImageTool(call.args, state, ui.paint);
1086
1160
  break;
1161
+ case 'git':
1162
+ result = await gitUnifiedTool(call.args, state, ui.paint);
1163
+ break;
1087
1164
  default:
1088
1165
  throw new Error(`Herramienta no soportada: ${call.tool}`);
1089
1166
  }
@@ -1093,18 +1170,6 @@ async function executeToolCall(call, state, ui) {
1093
1170
  return result;
1094
1171
  }
1095
1172
 
1096
- function buildOllamaInstallCommand() {
1097
- const isTermux = Boolean(
1098
- process.env.TERMUX_VERSION
1099
- || process.env.TERMUX_APP_PACKAGE
1100
- || (process.env.PREFIX && process.env.PREFIX.includes('com.termux'))
1101
- );
1102
-
1103
- return isTermux
1104
- ? 'pkg update -y && pkg install -y ollama'
1105
- : 'curl -fsSL https://ollama.com/install.sh | sh';
1106
- }
1107
-
1108
1173
  function parseDirectAction(input) {
1109
1174
  const text = input.trim();
1110
1175
  if (/^(git|npm|node|pnpm|yarn)\s+/.test(text)) {
@@ -1119,13 +1184,6 @@ function parseDirectAction(input) {
1119
1184
  };
1120
1185
  }
1121
1186
 
1122
- if (/^(?:instala|installa|install)\s+ollama$/i.test(text)) {
1123
- return {
1124
- tool: 'run_command',
1125
- args: { command: buildOllamaInstallCommand() },
1126
- };
1127
- }
1128
-
1129
1187
  const createRepoMatch = text.match(/^(?:crea|crear|create)\s+(?:un\s+)?(?:repo|repositorio)\s+(?:en\s+)?github\s+([a-z0-9._-]+)$/i);
1130
1188
  if (createRepoMatch) {
1131
1189
  return {
@@ -1446,111 +1504,72 @@ async function createCanvasImageTool(args, state, paint) {
1446
1504
 
1447
1505
  await fs.promises.mkdir(path.dirname(outputPath), { recursive: true });
1448
1506
  await image.write(outputPath);
1449
- return [`Imagen creada: ${outputPath}`, `Formato: ${safeFormat}`, `Tamano: ${width}x${height}`, `Elementos: ${elements.length}`].join('\\n');
1507
+ return [`Imagen creada: ${outputPath}`, `Formato: ${safeFormat}`, `Tamano: ${width}x${height}`, `Elementos: ${elements.length}`].join('\n');
1450
1508
  }
1451
1509
 
1452
1510
 
1453
- async function gitSecretSetTool(args) {
1511
+ async function gitUnifiedTool(args, state, paint) {
1512
+ const action = String(args.action || '').trim().toLowerCase();
1454
1513
  const provider = normalizeProfileName(args.provider || '');
1455
- if (!provider) throw new Error('git_secret_set requiere provider');
1456
- if (!args.token || typeof args.token !== 'string') throw new Error('git_secret_set requiere token');
1457
- const saved = upsertGitSecret(provider, args);
1458
- return `Credencial guardada: ${getGitSecretLabel(provider, saved?.name || args.name || '')}`;
1459
- }
1460
-
1461
- async function gitSecretListTool(args) {
1462
- const provider = normalizeProfileName(args.provider || '');
1463
- const name = String(args.name || '').trim();
1464
- const secrets = listGitSecrets();
1465
- const filtered = secrets.filter(secret => {
1466
- if (!provider) return true;
1467
- if (provider === 'custom') return !name || secret.key === `custom:${name}`;
1468
- return secret.key === provider;
1469
- });
1470
- if (!filtered.length) return 'No hay credenciales Git guardadas.';
1471
- const maskToken = (token) => {
1472
- const value = String(token || '');
1473
- if (!value) return '-';
1474
- if (value.length <= 8) return `${value.slice(0, 2)}***`;
1475
- return `${value.slice(0, 4)}...${value.slice(-4)}`;
1476
- };
1477
- return filtered.map(secret => [
1478
- `${secret.key}`,
1479
- ` username: ${secret.username || '-'}`,
1480
- ` apiBaseUrl: ${secret.apiBaseUrl || '-'}`,
1481
- ` cloneBaseUrl: ${secret.cloneBaseUrl || '-'}`,
1482
- ` authHeader: ${secret.authHeader || '-'}`,
1483
- ` tokenMasked: ${maskToken(secret.token)}`,
1484
- ].join('\n')).join('\n\n');
1485
- }
1486
-
1487
- async function gitSecretRemoveTool(args) {
1488
- const provider = normalizeProfileName(args.provider || '');
1489
- if (!provider) throw new Error('git_secret_remove requiere provider');
1490
- const name = String(args.name || '').trim();
1491
- const removed = removeGitSecret(provider, name);
1492
- return removed ? `Credencial eliminada: ${getGitSecretLabel(provider, name)}` : 'No encontre esa credencial.';
1493
- }
1494
-
1495
- async function gitCloneRepoTool(args, state, paint) {
1496
- const repoUrl = String(args.repoUrl || '').trim();
1497
- if (!repoUrl) throw new Error('git_clone_repo requiere repoUrl');
1498
- const provider = normalizeProfileName(args.provider || '');
1499
- const name = String(args.name || '').trim();
1500
- const profile = provider ? resolveGitProfile(provider, name) : null;
1501
- const finalUrl = buildCloneUrl(repoUrl, profile || {});
1502
- const destination = args.destination ? resolveInputPath(args.destination, state.cwd) : '';
1503
- const timeoutMs = Math.max(1000, Number.isFinite(Number(args.timeoutMs)) ? Number(args.timeoutMs) : 10 * 60 * 1000);
1504
-
1505
- const allowed = await askConfirmation(state.rl, 'Clonar repositorio', [
1506
- repoUrl,
1507
- profile ? `Provider: ${provider}${name ? ` (${name})` : ''}` : 'Provider: direct',
1508
- destination ? `Destino: ${destination}` : null,
1509
- `Timeout: ${timeoutMs}ms`,
1510
- ].filter(Boolean).join('\n'), paint, state);
1511
- if (!allowed) return 'Clonado cancelado por el usuario.';
1512
-
1513
- const result = await runProcess('git', ['clone', ...(args.branch ? ['--branch', String(args.branch)] : []), finalUrl, ...(destination ? [destination] : [])], { cwd: state.cwd, timeoutMs });
1514
- const lines = [`Exit code: ${result.code}`];
1515
- if (result.timedOut) lines.push('Timeout: el clon fue detenido por tiempo.');
1516
- if (result.stdout.trim()) lines.push(`STDOUT:\n${result.stdout.trim()}`);
1517
- if (result.stderr.trim()) lines.push(`STDERR:\n${result.stderr.trim()}`);
1518
- return lines.join('\n\n');
1519
- }
1520
-
1521
- async function gitApiRequestTool(args, state, paint) {
1522
- const provider = normalizeProfileName(args.provider || '');
1523
- if (!provider) throw new Error('git_api_request requiere provider');
1524
- const pathValue = String(args.path || '').trim();
1525
- if (!pathValue) throw new Error('git_api_request requiere path');
1526
1514
  const name = String(args.name || '').trim();
1527
- const profile = resolveGitProfile(provider, name);
1528
- if (!profile) throw new Error(`No hay credenciales para ${provider}${provider === 'custom' && name ? `:${name}` : ''}`);
1529
- const baseUrl = getApiBaseUrl(provider, profile);
1530
- if (!baseUrl) throw new Error(`No hay apiBaseUrl para ${provider}`);
1531
- const url = `${baseUrl.replace(/\/+$/, '')}/${pathValue.replace(/^\/+/, '')}`;
1532
- const timeoutMs = Math.max(1000, Number.isFinite(Number(args.timeoutMs)) ? Number(args.timeoutMs) : 15000);
1533
- const method = String(args.method || 'GET').toUpperCase();
1534
- const headers = buildApiHeaders(provider, profile, args.headers && typeof args.headers === 'object' ? args.headers : {});
1535
-
1536
- const allowed = await askConfirmation(state.rl, 'Git API request', `${method} ${url}\nTimeout: ${timeoutMs}ms`, paint, state);
1537
- if (!allowed) return 'Request cancelado por el usuario.';
1538
1515
 
1539
- const axios = require('axios');
1540
- const response = await axios({
1541
- url,
1542
- method,
1543
- headers: {
1544
- 'User-Agent': 'Zyn/1.0',
1545
- Accept: 'application/json, text/plain, */*',
1546
- ...headers,
1547
- },
1548
- data: args.body && typeof args.body === 'object' ? args.body : args.body,
1549
- timeout: timeoutMs,
1550
- responseType: 'text',
1551
- validateStatus: () => true,
1552
- });
1516
+ if (!action) throw new Error('git requiere action: "api" | "clone"');
1517
+
1518
+ if (action === 'clone') {
1519
+ const repoUrl = String(args.repoUrl || '').trim();
1520
+ if (!repoUrl) throw new Error('git action="clone" requiere repoUrl');
1521
+ if (!provider) throw new Error('git action="clone" requiere provider');
1522
+ const profile = resolveGitProfile(provider, name);
1523
+ const finalUrl = buildCloneUrl(repoUrl, profile || {});
1524
+ const destination = args.destination ? resolveInputPath(args.destination, state.cwd) : '';
1525
+ const timeoutMs = Math.max(1000, Number.isFinite(Number(args.timeoutMs)) ? Number(args.timeoutMs) : 10 * 60 * 1000);
1526
+ const allowed = await askConfirmation(state.rl, 'Clonar repositorio', [
1527
+ repoUrl,
1528
+ profile ? `Provider: ${provider}${name ? ` (${name})` : ''}` : 'Provider: direct',
1529
+ destination ? `Destino: ${destination}` : null,
1530
+ `Timeout: ${timeoutMs}ms`,
1531
+ ].filter(Boolean).join('\n'), paint, state);
1532
+ if (!allowed) return 'Clonado cancelado por el usuario.';
1533
+ const result = await runProcess('git', ['clone', ...(args.branch ? ['--branch', String(args.branch)] : []), finalUrl, ...(destination ? [destination] : [])], { cwd: state.cwd, timeoutMs });
1534
+ const lines = [`Exit code: ${result.code}`];
1535
+ if (result.timedOut) lines.push('Timeout: el clon fue detenido por tiempo.');
1536
+ if (result.stdout.trim()) lines.push(`STDOUT:\n${result.stdout.trim()}`);
1537
+ if (result.stderr.trim()) lines.push(`STDERR:\n${result.stderr.trim()}`);
1538
+ return lines.join('\n\n');
1539
+ }
1540
+
1541
+ if (action === 'api') {
1542
+ if (!provider) throw new Error('git action="api" requiere provider');
1543
+ const pathValue = String(args.path || '').trim();
1544
+ if (!pathValue) throw new Error('git action="api" requiere path');
1545
+ const profile = resolveGitProfile(provider, name);
1546
+ if (!profile) throw new Error(`Proveedor ${provider}${provider === 'custom' && name ? `:${name}` : ''} no configurado.`);
1547
+ const baseUrl = getApiBaseUrl(provider, profile);
1548
+ if (!baseUrl) throw new Error(`apiBaseUrl no configurada para ${provider}.`);
1549
+ const url = `${baseUrl.replace(/\/+$/, '')}/${pathValue.replace(/^\/+/, '')}`;
1550
+ const timeoutMs = Math.max(1000, Number.isFinite(Number(args.timeoutMs)) ? Number(args.timeoutMs) : 30000);
1551
+ const method = String(args.method || 'GET').toUpperCase();
1552
+ const headers = buildApiHeaders(provider, profile, args.headers && typeof args.headers === 'object' ? args.headers : {});
1553
+ const bodyPreview = args.body && typeof args.body === 'object' ? JSON.stringify(args.body).slice(0, 200) : '';
1554
+ const allowed = await askConfirmation(state.rl, `Git API ${method}`, `${url}${bodyPreview ? `\nBody: ${bodyPreview}` : ''}\nTimeout: ${timeoutMs}ms`, paint, state);
1555
+ if (!allowed) return 'Request cancelado por el usuario.';
1556
+ const axios = require('axios');
1557
+ const response = await axios({
1558
+ url,
1559
+ method,
1560
+ headers: {
1561
+ 'User-Agent': 'Zyn/1.0',
1562
+ Accept: 'application/json, text/plain, */*',
1563
+ ...headers,
1564
+ },
1565
+ data: args.body && typeof args.body === 'object' ? args.body : args.body,
1566
+ timeout: timeoutMs,
1567
+ responseType: 'text',
1568
+ validateStatus: () => true,
1569
+ });
1570
+ const text = typeof response.data === 'string' ? response.data : JSON.stringify(response.data, null, 2);
1571
+ return `Status: ${response.status}\n\n${text}`;
1572
+ }
1553
1573
 
1554
- const text = typeof response.data === 'string' ? response.data : JSON.stringify(response.data, null, 2);
1555
- return `Status: ${response.status}\n\n${text}`;
1574
+ throw new Error(`git action "${action}" no reconocida. Usa: "api" | "clone"`);
1556
1575
  }
package/src/tui/app.mjs CHANGED
@@ -71,6 +71,8 @@ class UIStore extends EventEmitter {
71
71
  this.spinner = null;
72
72
  this.processing = false;
73
73
  this.confirmRequest = null;
74
+ this.lastUserMessage = '';
75
+ this.inputDraft = '';
74
76
  this.turnCount = 0;
75
77
  this.messageQueue = [];
76
78
  this.pendingExit = false;
@@ -129,6 +131,13 @@ class UIStore extends EventEmitter {
129
131
  this.addItem({ type: 'event', kind, title, detail: detail || '' });
130
132
  }
131
133
 
134
+ setInputDraft(text) {
135
+ const nextDraft = String(text || '');
136
+ if (nextDraft === this.inputDraft) return;
137
+ this.inputDraft = nextDraft;
138
+ this._emit();
139
+ }
140
+
132
141
  requestConfirm(title, detail) {
133
142
  return new Promise(resolve => {
134
143
  this.confirmRequest = { title, detail, resolve };
@@ -163,6 +172,11 @@ function stripAnsi(str) {
163
172
  return str.replace(/\x1b\[[0-9;]*m/g, '');
164
173
  }
165
174
 
175
+ function shortTextPreview(str, maxLen) {
176
+ const s = String(str || '').replace(/\n/g, ' ').trim();
177
+ return s.length > maxLen ? s.slice(0, maxLen - 3) + '...' : s;
178
+ }
179
+
166
180
  function formatElapsed(ms) {
167
181
  const s = Math.floor(ms / 1000);
168
182
  if (s < 60) return s + 's';
@@ -527,7 +541,7 @@ function QueuedMessage({ text }) {
527
541
  );
528
542
  }
529
543
 
530
- function ConfirmBar({ title, detail }) {
544
+ function ConfirmBar({ title, detail, lastMessage, draft }) {
531
545
  const detailLines = (detail || '').split('\n').filter(l => l.trim()).slice(0, 10);
532
546
 
533
547
  return h(Box, { flexDirection: 'column', paddingLeft: 3, marginTop: 1 },
@@ -549,6 +563,18 @@ function ConfirmBar({ title, detail }) {
549
563
  ),
550
564
  )
551
565
  : null,
566
+ lastMessage
567
+ ? h(Box, { marginTop: 0, paddingLeft: 2 },
568
+ h(Text, { color: T.textGhost }, uiText('Your message: ', 'Tu mensaje: ')),
569
+ h(Text, { color: T.textDim, wrap: 'wrap' }, shortTextPreview(lastMessage, 80)),
570
+ )
571
+ : null,
572
+ draft
573
+ ? h(Box, { marginTop: 0, paddingLeft: 2 },
574
+ h(Text, { color: T.textGhost }, uiText('Saved draft: ', 'Borrador guardado: ')),
575
+ h(Text, { color: T.textDim, wrap: 'wrap' }, shortTextPreview(draft, 80)),
576
+ )
577
+ : null,
552
578
  h(Box, { marginTop: 0, paddingLeft: 2, gap: 2 },
553
579
  h(Text, { color: T.green, bold: true }, '[y]'),
554
580
  h(Text, { color: T.textMuted }, uiText('allow', 'permitir')),
@@ -609,15 +635,29 @@ function StaticItem({ item, width }) {
609
635
  }
610
636
  }
611
637
 
612
- function InputBar({ onSubmit, processing, width = 100 }) {
613
- const [value, setValue] = useState('');
614
- const [cursor, setCursor] = useState(0);
638
+ function InputBar({ onSubmit, processing, width = 100, draft = '', onDraftChange }) {
639
+ const [value, setValue] = useState(draft || '');
640
+ const [cursor, setCursor] = useState((draft || '').length);
615
641
  const [histIdx, setHistIdx] = useState(-1);
616
642
  const [suggestIdx, setSuggestIdx] = useState(0);
617
643
  const historyRef = useRef([]);
618
644
  const savedRef = useRef('');
619
645
  const lastPasteMetaRef = useRef(null);
620
646
 
647
+ useEffect(() => {
648
+ if (draft !== value) {
649
+ setValue(draft || '');
650
+ setCursor((draft || '').length);
651
+ setHistIdx(-1);
652
+ setSuggestIdx(0);
653
+ }
654
+ }, [draft]);
655
+
656
+ const updateValue = useCallback((next) => {
657
+ setValue(next);
658
+ if (onDraftChange) onDraftChange(next);
659
+ }, [onDraftChange]);
660
+
621
661
  const showSuggestions = value.startsWith('/') && !value.includes(' ') && value.length > 0;
622
662
  const suggestions = showSuggestions
623
663
  ? SLASH_COMMANDS.filter(c => ('/' + c.name).startsWith(value.toLowerCase()))
@@ -634,7 +674,7 @@ function InputBar({ onSubmit, processing, width = 100 }) {
634
674
  }
635
675
  historyRef.current.unshift(text);
636
676
  if (historyRef.current.length > 100) historyRef.current.pop();
637
- setValue('');
677
+ updateValue('');
638
678
  setCursor(0);
639
679
  setHistIdx(-1);
640
680
  setSuggestIdx(0);
@@ -647,7 +687,7 @@ function InputBar({ onSubmit, processing, width = 100 }) {
647
687
  const cmd = suggestions[suggestIdx] || suggestions[0];
648
688
  if (cmd) {
649
689
  const completed = `/${cmd.name} `;
650
- setValue(completed);
690
+ updateValue(completed);
651
691
  setCursor(completed.length);
652
692
  setSuggestIdx(0);
653
693
  }
@@ -671,7 +711,7 @@ function InputBar({ onSubmit, processing, width = 100 }) {
671
711
  if (histIdx === -1) savedRef.current = value;
672
712
  const next = Math.min(histIdx + 1, hist.length - 1);
673
713
  setHistIdx(next);
674
- setValue(hist[next]);
714
+ updateValue(hist[next]);
675
715
  setCursor(hist[next].length);
676
716
  return;
677
717
  }
@@ -679,13 +719,13 @@ function InputBar({ onSubmit, processing, width = 100 }) {
679
719
  if (key.downArrow) {
680
720
  if (histIdx <= 0) {
681
721
  setHistIdx(-1);
682
- setValue(savedRef.current);
722
+ updateValue(savedRef.current);
683
723
  setCursor(savedRef.current.length);
684
724
  return;
685
725
  }
686
726
  const next = histIdx - 1;
687
727
  setHistIdx(next);
688
- setValue(historyRef.current[next]);
728
+ updateValue(historyRef.current[next]);
689
729
  setCursor(historyRef.current[next].length);
690
730
  return;
691
731
  }
@@ -705,7 +745,7 @@ function InputBar({ onSubmit, processing, width = 100 }) {
705
745
 
706
746
  if (key.ctrl && input === 'u') {
707
747
  const after = value.slice(cursor);
708
- setValue(after);
748
+ updateValue(after);
709
749
  setCursor(0);
710
750
  return;
711
751
  }
@@ -714,14 +754,14 @@ function InputBar({ onSubmit, processing, width = 100 }) {
714
754
  const before = value.slice(0, cursor);
715
755
  const after = value.slice(cursor);
716
756
  const trimmed = before.replace(/\S+\s*$/, '');
717
- setValue(trimmed + after);
757
+ updateValue(trimmed + after);
718
758
  setCursor(trimmed.length);
719
759
  return;
720
760
  }
721
761
 
722
762
  if (key.backspace || key.delete) {
723
763
  if (cursor === 0) return;
724
- setValue(v => v.slice(0, cursor - 1) + v.slice(cursor));
764
+ updateValue(value.slice(0, cursor - 1) + value.slice(cursor));
725
765
  setCursor(c => Math.max(0, c - 1));
726
766
  setSuggestIdx(0);
727
767
  return;
@@ -730,7 +770,7 @@ function InputBar({ onSubmit, processing, width = 100 }) {
730
770
  if (input && !key.ctrl && !key.meta) {
731
771
  const normalizedInput = input.replace(/\r\n/g, '\n');
732
772
  const safeInput = normalizedInput.includes('\n') ? normalizedInput.replace(/\n/g, ' ') : normalizedInput;
733
- setValue(v => v.slice(0, cursor) + safeInput + v.slice(cursor));
773
+ updateValue(value.slice(0, cursor) + safeInput + value.slice(cursor));
734
774
  setCursor(c => c + safeInput.length);
735
775
  setSuggestIdx(0);
736
776
  }
@@ -790,7 +830,7 @@ function InputBar({ onSubmit, processing, width = 100 }) {
790
830
  h(Text, {
791
831
  color: selected ? T.accent : T.textMuted,
792
832
  }, `/${cmd.name}`),
793
- h(Text, { color: T.textGhost }, ` ${cmd.desc}`),
833
+ h(Text, { color: T.textGhost }, ` ${getTuiLang() === 'es' ? (cmd.descEs || cmd.desc) : cmd.desc}`),
794
834
  );
795
835
  }),
796
836
  hasMore && windowStart + maxVisible < suggestions.length
@@ -877,13 +917,13 @@ function App({ store, state, onSubmit }) {
877
917
 
878
918
  if (showConfirm) {
879
919
  dynamicArea.push(
880
- h(ConfirmBar, { key: 'confirm', title: store.confirmRequest.title, detail: store.confirmRequest.detail })
920
+ h(ConfirmBar, { key: 'confirm', title: store.confirmRequest.title, detail: store.confirmRequest.detail, lastMessage: store.lastUserMessage, draft: store.inputDraft })
881
921
  );
882
922
  }
883
923
 
884
924
  if (showInput) {
885
925
  dynamicArea.push(
886
- h(InputBar, { key: 'input', onSubmit: handleInput, processing: store.processing, width })
926
+ h(InputBar, { key: 'input', onSubmit: handleInput, processing: store.processing, width, draft: store.inputDraft, onDraftChange: (text) => store.setInputDraft(text) })
887
927
  );
888
928
  }
889
929
 
@@ -954,6 +994,7 @@ export async function startTUI(options = {}) {
954
994
  }
955
995
 
956
996
  if (input.startsWith('/')) {
997
+ const commandName = input.split(' ')[0].slice(1).toLowerCase();
957
998
  const lines = [];
958
999
  const origLog = console.log;
959
1000
  const origError = console.error;
@@ -972,12 +1013,21 @@ export async function startTUI(options = {}) {
972
1013
  printSessions: printMod.printSessions,
973
1014
  printStatus: printMod.printStatus,
974
1015
  };
975
- const handled = await handleLocalCommand(input, state, deps);
976
- if (handled && lines.length > 0) {
977
- const clean = lines.filter(l => l.trim()).join('\n');
978
- if (clean) store.addItem({ type: 'system', text: clean });
1016
+
1017
+ if (commandName === 'persona') {
1018
+ const handled = await handleLocalCommand(input, state, deps);
1019
+ if (handled && lines.length > 0) {
1020
+ const clean = lines.filter(l => l.trim()).join('\n');
1021
+ if (clean) store.addItem({ type: 'system', text: clean });
1022
+ }
1023
+ } else {
1024
+ const handled = await handleLocalCommand(input, state, deps);
1025
+ if (handled && lines.length > 0) {
1026
+ const clean = lines.filter(l => l.trim()).join('\n');
1027
+ if (clean) store.addItem({ type: 'system', text: clean });
1028
+ }
1029
+ if (!handled) store.addEvent('warn', uiText('command not recognized', 'comando no reconocido'), input);
979
1030
  }
980
- if (!handled) store.addEvent('warn', uiText('command not recognized', 'comando no reconocido'), input);
981
1031
  } catch (err) {
982
1032
  store.addEvent('error', uiText('error', 'error'), err.message);
983
1033
  } finally {
@@ -989,6 +1039,7 @@ export async function startTUI(options = {}) {
989
1039
 
990
1040
  store.addItem({ type: 'divider' });
991
1041
  store.addItem({ type: 'user', text: input });
1042
+ store.lastUserMessage = input;
992
1043
 
993
1044
  const origError = console.error;
994
1045
  console.error = () => {};
package/src/web/server.js CHANGED
@@ -11,7 +11,8 @@ const { runWebAgent } = require('./webAgent');
11
11
  const { MODELS, DEFAULT_MODEL_KEY, listProvidersFromModels, DEFAULT_LANGUAGE } = require('../config');
12
12
 
13
13
  const app = express();
14
- const PORT = process.env.PORT || 3000;
14
+ const HOST = process.env.HOST || process.env.ZYN_WEB_HOST || '127.0.0.1';
15
+ const PORT = Number(process.env.PORT || process.env.ZYN_WEB_PORT || 3000);
15
16
 
16
17
  // Evitar crashes silenciosos
17
18
  process.on('uncaughtException', (err) => {
@@ -31,7 +32,7 @@ app.use((req, res, next) => {
31
32
  next();
32
33
  });
33
34
  app.use(express.static(path.join(__dirname, 'public')));
34
- // Persistir secreto de sesión en disco
35
+ // Persistir secreto de sesion en disco
35
36
  const SECRET_FILE = path.join(__dirname, 'data', '.session-secret');
36
37
  let sessionSecret;
37
38
  try {
@@ -42,9 +43,13 @@ try {
42
43
  fs.writeFileSync(SECRET_FILE, sessionSecret);
43
44
  }
44
45
 
46
+ // Asegurar que el directorio de sesiones existe
47
+ const SESSION_DIR = path.join(__dirname, 'data', 'sessions');
48
+ fs.mkdirSync(SESSION_DIR, { recursive: true });
49
+
45
50
  app.use(session({
46
51
  store: new FileStore({
47
- path: path.join(__dirname, 'data', 'sessions'),
52
+ path: SESSION_DIR,
48
53
  ttl: 30 * 24 * 60 * 60,
49
54
  retries: 0,
50
55
  logFn: () => {},
@@ -294,6 +299,6 @@ app.get('/{*splat}', (req, res) => {
294
299
  res.sendFile(path.join(__dirname, 'public', 'index.html'));
295
300
  });
296
301
 
297
- app.listen(PORT, () => {
298
- console.log(`\n Zyn Web http://localhost:${PORT}\n`);
302
+ app.listen(PORT, HOST, () => {
303
+ console.log(`\n \u25cf Zyn Web \u2192 http://${HOST}:${PORT}\n`);
299
304
  });