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.
- package/README.md +86 -93
- package/package.json +1 -1
- package/src/cli/commands.js +151 -48
- package/src/cli/print.js +4 -2
- package/src/cli/runtime.js +3 -0
- package/src/config.js +7 -1
- package/src/core/agent.js +15 -4
- package/src/core/prompts.js +141 -32
- package/src/tools/index.js +140 -121
- package/src/tui/app.mjs +72 -21
- package/src/web/server.js +10 -5
package/src/tools/index.js
CHANGED
|
@@ -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
|
|
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
|
-
'
|
|
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('
|
|
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
|
|
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
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
976
|
-
if (
|
|
977
|
-
const
|
|
978
|
-
if (
|
|
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
|
|
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
|
|
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:
|
|
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
|
|
302
|
+
app.listen(PORT, HOST, () => {
|
|
303
|
+
console.log(`\n \u25cf Zyn Web \u2192 http://${HOST}:${PORT}\n`);
|
|
299
304
|
});
|