zyn-ai 1.3.2 → 1.3.3
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/config.js +7 -1
- package/src/core/agent.js +3 -4
- package/src/core/prompts.js +46 -9
- package/src/tools/index.js +140 -121
- package/src/tui/app.mjs +72 -21
- package/src/web/server.js +10 -5
package/src/core/prompts.js
CHANGED
|
@@ -3,20 +3,56 @@ const { buildSkillsPrompt } = require('./skills');
|
|
|
3
3
|
const { getToolPromptText, TOOL_DEFINITIONS } = require('../tools');
|
|
4
4
|
const { listProvidersFromModels, MODELS, DEFAULT_MODEL_KEY } = require('../config');
|
|
5
5
|
const { detectLanguage, normalizeLanguage, languageLabel } = require('../i18n');
|
|
6
|
+
const os = require('os');
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
|
|
9
|
+
function getPlatformInfo() {
|
|
10
|
+
// Detectar SO real primero
|
|
11
|
+
let osName = 'Unknown';
|
|
12
|
+
|
|
13
|
+
if (process.platform === 'linux') {
|
|
14
|
+
try {
|
|
15
|
+
const release = fs.readFileSync('/etc/os-release', 'utf8');
|
|
16
|
+
const nameMatch = release.match(/^PRETTY_NAME="?([^"\n]+)"?/m);
|
|
17
|
+
if (nameMatch) {
|
|
18
|
+
osName = nameMatch[1];
|
|
19
|
+
} else {
|
|
20
|
+
const idMatch = release.match(/^ID="?([^"\n]+)"?/m);
|
|
21
|
+
const verMatch = release.match(/^VERSION_ID="?([^"\n]+)"?/m);
|
|
22
|
+
osName = `Linux ${idMatch ? idMatch[1] : ''}${verMatch ? ' ' + verMatch[1] : ''}`.trim();
|
|
23
|
+
}
|
|
24
|
+
} catch {
|
|
25
|
+
try {
|
|
26
|
+
const { execSync } = require('child_process');
|
|
27
|
+
osName = execSync('uname -o -r -m', { encoding: 'utf8', timeout: 3000 }).trim();
|
|
28
|
+
} catch {
|
|
29
|
+
osName = `Linux ${os.release()} ${os.arch()}`;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
} else if (process.platform === 'darwin') {
|
|
33
|
+
try {
|
|
34
|
+
const { execSync } = require('child_process');
|
|
35
|
+
const ver = execSync('sw_vers -productVersion', { encoding: 'utf8', timeout: 3000 }).trim();
|
|
36
|
+
osName = `macOS ${ver} (${os.arch()})`;
|
|
37
|
+
} catch {
|
|
38
|
+
osName = `macOS ${os.release()} (${os.arch()})`;
|
|
39
|
+
}
|
|
40
|
+
} else if (process.platform === 'win32') {
|
|
41
|
+
osName = `Windows ${os.release()} (${os.arch()})`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return osName;
|
|
45
|
+
}
|
|
6
46
|
|
|
7
47
|
const KNOWN_TOOLS = new Set([
|
|
8
48
|
...TOOL_DEFINITIONS.map(tool => tool.name),
|
|
9
49
|
'task_create', 'task_list', 'task_update', 'task_complete', 'task_delete', 'task_clear',
|
|
10
|
-
'git_secret_set', 'git_secret_list', 'git_secret_remove',
|
|
11
|
-
'git_clone_repo', 'git_api_request',
|
|
12
50
|
]);
|
|
13
51
|
|
|
14
52
|
|
|
15
53
|
function buildSystemPrompt(cwd, state = {}, options = {}) {
|
|
16
54
|
const language = normalizeLanguage(options.language || state.language || detectLanguage(options.input || '', state.language));
|
|
17
|
-
const platform =
|
|
18
|
-
: process.platform === 'darwin' ? 'macOS'
|
|
19
|
-
: process.platform;
|
|
55
|
+
const platform = getPlatformInfo();
|
|
20
56
|
const date = new Date().toLocaleDateString(language === 'es' ? 'es-ES' : 'en-US', {
|
|
21
57
|
year: 'numeric',
|
|
22
58
|
month: 'long',
|
|
@@ -39,9 +75,9 @@ function buildSystemPrompt(cwd, state = {}, options = {}) {
|
|
|
39
75
|
'Si la tarea requiere comprobar algo, primero intenta una herramienta real y espera el resultado antes de concluir.',
|
|
40
76
|
'No cierres con una conclusion si todavia no has probado nada.',
|
|
41
77
|
'Si una tarea dura demasiado, usa run_command con un timeoutMs adecuado y confirma el resultado real.',
|
|
42
|
-
'Para
|
|
78
|
+
'Para operaciones de Git usa la herramienta git con action="api" o action="clone".',
|
|
43
79
|
'Usa exclusivamente tools registradas en "Tool use". No inventes nombres de tools ni aliases.',
|
|
44
|
-
'
|
|
80
|
+
'Mantente acotado y determinista: entradas claras, salidas claras, sin razonamiento creativo salvo que el usuario lo pida.',
|
|
45
81
|
'Cuando aplique, entrega resumen ejecutivo corto + riesgos/banderas rojas + siguiente accion concreta.',
|
|
46
82
|
'Para proyectos, usa combinaciones de tools según la fase: descubrir (list_dir/search_text), leer (read_file/fetch/webfetch), cambiar (write/replace), validar (run_command), documentar (final).',
|
|
47
83
|
'No te limites a una sola tool por costumbre; elige la mejor secuencia técnica para el objetivo.',
|
|
@@ -57,9 +93,9 @@ function buildSystemPrompt(cwd, state = {}, options = {}) {
|
|
|
57
93
|
'If the task requires verification, try a real tool first and wait for its result before concluding.',
|
|
58
94
|
'Do not end with a conclusion if you have not tested anything yet.',
|
|
59
95
|
'If a task takes long, use run_command with an appropriate timeoutMs and verify the real result.',
|
|
60
|
-
'For
|
|
96
|
+
'For Git operations use the git tool with action="api" or action="clone".',
|
|
61
97
|
'Use only tools listed under "Tool use". Never invent tool names or aliases.',
|
|
62
|
-
'
|
|
98
|
+
'Stay bounded and deterministic: clear inputs, clear outputs, no creative reasoning unless explicitly requested.',
|
|
63
99
|
'When relevant, provide a short executive summary + obvious red flags + next concrete action.',
|
|
64
100
|
'For project work, combine tools by phase: discover (list_dir/search_text), read (read_file/fetch/webfetch), change (write/replace), validate (run_command), then report.',
|
|
65
101
|
'Do not over-focus on a single tool by habit; choose the best technical sequence for the goal.',
|
|
@@ -206,6 +242,7 @@ const TOOL_ARG_KEYS = {
|
|
|
206
242
|
web_search: ['query', 'lang', 'limit'],
|
|
207
243
|
web_read: ['url'],
|
|
208
244
|
create_canvas_image: ['width', 'height', 'background', 'elements', 'format', 'outputPath'],
|
|
245
|
+
git: ['provider', 'action', 'method', 'path', 'body', 'headers', 'name', 'repoUrl', 'destination', 'branch', 'timeoutMs'],
|
|
209
246
|
};
|
|
210
247
|
|
|
211
248
|
const LONG_VALUE_ARG = {
|
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
|
}
|