zyn-ai 1.2.1 → 1.3.1
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/LICENSE +17 -11
- package/README.md +8 -4
- package/package.json +6 -4
- package/src/cli/commands.js +57 -0
- package/src/cli/runtime.js +4 -2
- package/src/config.js +5 -3
- package/src/core/agent.js +102 -59
- package/src/core/prompts.js +36 -5
- package/src/i18n.js +6 -0
- package/src/tools/index.js +593 -41
- package/src/tui/app.mjs +83 -39
- package/src/utils/secretStorage.js +223 -0
- package/src/utils/sessionStorage.js +42 -4
- package/src/utils/taskStorage.js +192 -0
- package/src/web/webAgent.js +2 -2
package/src/tools/index.js
CHANGED
|
@@ -7,6 +7,16 @@ const fsp = fs.promises;
|
|
|
7
7
|
const {
|
|
8
8
|
MAX_FILE_LINES,
|
|
9
9
|
} = require('../config');
|
|
10
|
+
const {
|
|
11
|
+
buildApiHeaders,
|
|
12
|
+
buildCloneUrl,
|
|
13
|
+
getApiBaseUrl,
|
|
14
|
+
listGitSecrets,
|
|
15
|
+
normalizeProfileName,
|
|
16
|
+
removeGitSecret,
|
|
17
|
+
resolveGitProfile,
|
|
18
|
+
upsertGitSecret,
|
|
19
|
+
} = require('../utils/secretStorage');
|
|
10
20
|
const { resolveInputPath } = require('../utils/pathUtils');
|
|
11
21
|
const {
|
|
12
22
|
formatLineRange,
|
|
@@ -26,9 +36,15 @@ const TOOL_DEFINITIONS = [
|
|
|
26
36
|
{ name: 'append_file', usage: '{ path, content }' },
|
|
27
37
|
{ name: 'replace_in_file', usage: '{ path, search, replace, all? }' },
|
|
28
38
|
{ name: 'fetch_url', usage: '{ url, selector?, attribute?, limit?, headers? }' },
|
|
29
|
-
{ name: '
|
|
39
|
+
{ name: 'fetch', usage: '{ url, method?, headers?, query?, json?, data?, form?, files?, timeoutMs? }' },
|
|
40
|
+
{ name: 'fetch_http', usage: '{ url, method?, headers?, query?, json?, data?, form?, files?, timeoutMs? }' },
|
|
41
|
+
{ name: 'webfetch', usage: '{ url, headers?, timeoutMs? }' },
|
|
42
|
+
{ name: 'scrape_site', usage: '{ url, selectors, limit?, headers? }' },
|
|
43
|
+
{ name: 'web_search', usage: '{ query, lang?, limit? }' },
|
|
30
44
|
{ name: 'web_read', usage: '{ url }' },
|
|
45
|
+
{ name: 'create_canvas_image', usage: '{ width, height, background?, elements?, format?, outputPath? }' },
|
|
31
46
|
];
|
|
47
|
+
const REGISTERED_TOOLS = new Set(TOOL_DEFINITIONS.map(tool => tool.name));
|
|
32
48
|
|
|
33
49
|
function getToolPromptText() {
|
|
34
50
|
return [
|
|
@@ -85,7 +101,19 @@ function getToolPromptText() {
|
|
|
85
101
|
' Con selector + attribute (ej: "href", "src"): extrae atributo.',
|
|
86
102
|
' limit: max elementos a extraer (default: 20, max: 50).',
|
|
87
103
|
'',
|
|
88
|
-
'
|
|
104
|
+
'fetch_http { url, method?, headers?, query?, json?, data?, form?, files?, timeoutMs? }',
|
|
105
|
+
' Cliente HTTP avanzado: soporta headers custom, query params, body JSON/texto, form-data y adjuntar archivos.',
|
|
106
|
+
'',
|
|
107
|
+
'fetch { url, method?, headers?, query?, json?, data?, form?, files?, timeoutMs? }',
|
|
108
|
+
' Alias profesional recomendado para solicitudes HTTP avanzadas.',
|
|
109
|
+
'',
|
|
110
|
+
'webfetch { url, headers?, timeoutMs? }',
|
|
111
|
+
' Descarga una pagina web y la convierte a Markdown estructurado (enlaces, botones, imagenes, texto).',
|
|
112
|
+
'',
|
|
113
|
+
'scrape_site { url, selectors, limit?, headers? }',
|
|
114
|
+
' Scraping avanzado con multiples selectores en una sola llamada.',
|
|
115
|
+
'',
|
|
116
|
+
'web_search { query, lang?, limit? }',
|
|
89
117
|
' Busca en la web via DuckDuckGo. Retorna titulo, URL y snippet de los primeros resultados.',
|
|
90
118
|
' Si el usuario pide investigar algo, realiza la busqueda en lugar de explicar como hacerlo.',
|
|
91
119
|
' Ejemplo: {"type":"tool","tool":"web_search","args":{"query":"como usar puppeteer node"}}',
|
|
@@ -94,6 +122,17 @@ function getToolPromptText() {
|
|
|
94
122
|
' Descarga una pagina web y la convierte a texto legible (sin HTML).',
|
|
95
123
|
' Ideal para leer articulos, documentacion o contenido de paginas.',
|
|
96
124
|
' Ejemplo: {"type":"tool","tool":"web_read","args":{"url":"https://docs.example.com/guide"}}',
|
|
125
|
+
'',
|
|
126
|
+
'## Imagen profesional con Jimp',
|
|
127
|
+
'',
|
|
128
|
+
'create_canvas_image { width, height, background?, elements?, format?, outputPath? }',
|
|
129
|
+
' Crea imagenes desde cero usando Jimp con composicion por elementos.',
|
|
130
|
+
' width/height son obligatorios. background puede ser color HEX (#RRGGBB o #RRGGBBAA).',
|
|
131
|
+
' elements permite combinar rect, circle/ellipse, line, text e image.',
|
|
132
|
+
' Usa este flujo profesional: definir lienzo -> capas base -> tipografia -> detalles -> exportacion.',
|
|
133
|
+
' Ejemplo:',
|
|
134
|
+
' {"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
|
+
'',
|
|
97
136
|
].join('\n');
|
|
98
137
|
}
|
|
99
138
|
|
|
@@ -131,12 +170,22 @@ function describeToolCall(call) {
|
|
|
131
170
|
const sel = call.args.selector ? ` → ${shortText(call.args.selector, 30)}` : '';
|
|
132
171
|
return `Fetch ${shortText(cleanedUrl, 50)}${sel}`;
|
|
133
172
|
}
|
|
173
|
+
case 'fetch_http':
|
|
174
|
+
return `HTTP ${String(call.args.method || 'GET').toUpperCase()} ${shortText(cleanUrl(call.args.url || ''), 50)}`;
|
|
175
|
+
case 'fetch':
|
|
176
|
+
return `Fetch ${String(call.args.method || 'GET').toUpperCase()} ${shortText(cleanUrl(call.args.url || ''), 50)}`;
|
|
177
|
+
case 'webfetch':
|
|
178
|
+
return `WebFetch ${shortText(cleanUrl(call.args.url || ''), 50)}`;
|
|
179
|
+
case 'scrape_site':
|
|
180
|
+
return `Scraping ${shortText(cleanUrl(call.args.url || ''), 50)}`;
|
|
134
181
|
case 'web_search':
|
|
135
182
|
return `Buscando "${shortText(call.args.query || '', 50)}"`;
|
|
136
183
|
case 'web_read': {
|
|
137
184
|
const readUrl = cleanUrl(call.args.url || '');
|
|
138
185
|
return `Leyendo ${shortText(readUrl, 60)}`;
|
|
139
186
|
}
|
|
187
|
+
case 'create_canvas_image':
|
|
188
|
+
return `Creando imagen ${call.args.width || '?'}x${call.args.height || '?'}`;
|
|
140
189
|
default:
|
|
141
190
|
return call.tool;
|
|
142
191
|
}
|
|
@@ -252,6 +301,35 @@ async function askConfirmation(rl, title, detail, paint, state) {
|
|
|
252
301
|
return answer === 's' || answer === 'si' || answer === 'y' || answer === 'yes';
|
|
253
302
|
}
|
|
254
303
|
|
|
304
|
+
function isIpHost(hostname = '') {
|
|
305
|
+
return /^(\d{1,3}\.){3}\d{1,3}$/.test(hostname)
|
|
306
|
+
|| /^\[[0-9a-f:]+\]$/i.test(hostname)
|
|
307
|
+
|| /^[0-9a-f:]+$/i.test(hostname);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
async function requireIpConsent(urlValue, state, paint) {
|
|
311
|
+
let parsed;
|
|
312
|
+
try {
|
|
313
|
+
parsed = new URL(urlValue);
|
|
314
|
+
} catch {
|
|
315
|
+
return true;
|
|
316
|
+
}
|
|
317
|
+
if (!isIpHost(parsed.hostname)) return true;
|
|
318
|
+
|
|
319
|
+
if (state?.tuiConfirm) {
|
|
320
|
+
const answer = await state.tuiConfirm('Permiso obligatorio para IP', `Destino IP detectado: ${urlValue}\nConfirma acceso de red explícitamente.`);
|
|
321
|
+
return answer === 's' || answer === 'si' || answer === 'y' || answer === 'yes';
|
|
322
|
+
}
|
|
323
|
+
if (!state?.rl) return false;
|
|
324
|
+
console.error('');
|
|
325
|
+
console.error(` ${paint('!', 'yellow')} Permiso obligatorio para IP`);
|
|
326
|
+
console.error(` ${paint(`Destino IP detectado: ${urlValue}`, 'dim')}`);
|
|
327
|
+
const answer = (await state.rl.question(` ${paint('s/N', 'yellow')} ${paint('\u276F', 'yellow')} `))
|
|
328
|
+
.trim()
|
|
329
|
+
.toLowerCase();
|
|
330
|
+
return answer === 's' || answer === 'si' || answer === 'y' || answer === 'yes';
|
|
331
|
+
}
|
|
332
|
+
|
|
255
333
|
async function listDirTool(args, state) {
|
|
256
334
|
const targetPath = resolveInputPath(args.path ?? '.', state.cwd);
|
|
257
335
|
const entries = await fsp.readdir(targetPath, { withFileTypes: true });
|
|
@@ -370,6 +448,7 @@ async function runCommandTool(args, state, paint) {
|
|
|
370
448
|
return 'Comando cancelado por el usuario.';
|
|
371
449
|
}
|
|
372
450
|
|
|
451
|
+
|
|
373
452
|
const result = await runProcess('bash', ['-lc', command], {
|
|
374
453
|
cwd: state.cwd,
|
|
375
454
|
timeoutMs: 120000,
|
|
@@ -597,23 +676,37 @@ async function fetchUrlTool(args, state, paint) {
|
|
|
597
676
|
if (!allowed) {
|
|
598
677
|
return 'Fetch cancelado por el usuario.';
|
|
599
678
|
}
|
|
679
|
+
if (!(await requireIpConsent(url, state, paint))) {
|
|
680
|
+
return 'Fetch a IP cancelado por falta de consentimiento explícito.';
|
|
681
|
+
}
|
|
600
682
|
|
|
601
683
|
const axios = require('axios');
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
684
|
+
let response;
|
|
685
|
+
let lastErr;
|
|
686
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
687
|
+
try {
|
|
688
|
+
response = await axios({
|
|
689
|
+
url,
|
|
690
|
+
method: 'GET',
|
|
691
|
+
headers: {
|
|
692
|
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36',
|
|
693
|
+
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
|
694
|
+
'Accept-Language': 'es-ES,es;q=0.9,en;q=0.8',
|
|
695
|
+
...(args.headers || {}),
|
|
696
|
+
},
|
|
697
|
+
timeout: 15000,
|
|
698
|
+
maxContentLength: 512000,
|
|
699
|
+
maxRedirects: 5,
|
|
700
|
+
responseType: 'text',
|
|
701
|
+
validateStatus: () => true,
|
|
702
|
+
});
|
|
703
|
+
break;
|
|
704
|
+
} catch (err) {
|
|
705
|
+
lastErr = err;
|
|
706
|
+
if (attempt === 0) continue;
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
if (!response) throw lastErr || new Error('fetch_url fallo');
|
|
617
710
|
|
|
618
711
|
const body = typeof response.data === 'string'
|
|
619
712
|
? response.data
|
|
@@ -660,45 +753,219 @@ async function fetchUrlTool(args, state, paint) {
|
|
|
660
753
|
return truncateText(parts.join('\n'));
|
|
661
754
|
}
|
|
662
755
|
|
|
663
|
-
async function
|
|
664
|
-
|
|
665
|
-
|
|
756
|
+
async function fetchHttpTool(args, state, paint) {
|
|
757
|
+
if (!args.url || typeof args.url !== 'string') throw new Error('fetch_http requiere url');
|
|
758
|
+
const method = String(args.method || 'GET').toUpperCase();
|
|
759
|
+
const url = cleanUrl(args.url);
|
|
760
|
+
const detail = `${method} ${url}`;
|
|
761
|
+
const allowed = await askConfirmation(state.rl, 'HTTP avanzado', detail, paint, state);
|
|
762
|
+
if (!allowed) return 'Solicitud cancelada.';
|
|
763
|
+
if (!(await requireIpConsent(url, state, paint))) {
|
|
764
|
+
return 'Solicitud a IP cancelada por falta de consentimiento explícito.';
|
|
765
|
+
}
|
|
666
766
|
|
|
667
|
-
const
|
|
668
|
-
|
|
669
|
-
);
|
|
670
|
-
if (
|
|
767
|
+
const headers = { ...(args.headers || {}) };
|
|
768
|
+
let body;
|
|
769
|
+
const isCatbox = /catbox\.moe/i.test(url);
|
|
770
|
+
if (args.json && typeof args.json === 'object') {
|
|
771
|
+
body = JSON.stringify(args.json);
|
|
772
|
+
headers['Content-Type'] = headers['Content-Type'] || 'application/json';
|
|
773
|
+
} else if (args.data !== undefined) {
|
|
774
|
+
body = String(args.data);
|
|
775
|
+
} else if (args.form && typeof args.form === 'object') {
|
|
776
|
+
const formPayload = { ...args.form };
|
|
777
|
+
if (isCatbox && !formPayload.reqtype) formPayload.reqtype = 'fileupload';
|
|
778
|
+
const form = new FormData();
|
|
779
|
+
for (const [k, v] of Object.entries(formPayload)) form.append(k, String(v));
|
|
780
|
+
if (Array.isArray(args.files)) {
|
|
781
|
+
for (const file of args.files) {
|
|
782
|
+
if (!file || !file.path || !file.field) continue;
|
|
783
|
+
const filePath = resolveInputPath(file.path, state.cwd);
|
|
784
|
+
const buffer = await fs.promises.readFile(filePath);
|
|
785
|
+
const blob = new Blob([buffer], { type: file.type || 'application/octet-stream' });
|
|
786
|
+
const fieldName = isCatbox ? 'fileToUpload' : String(file.field);
|
|
787
|
+
form.append(fieldName, blob, file.name || path.basename(file.path));
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
body = form;
|
|
791
|
+
} else if (Array.isArray(args.files) && args.files.length > 0) {
|
|
792
|
+
const form = new FormData();
|
|
793
|
+
if (isCatbox) form.append('reqtype', 'fileupload');
|
|
794
|
+
for (const file of args.files) {
|
|
795
|
+
if (!file || !file.path) continue;
|
|
796
|
+
const filePath = resolveInputPath(file.path, state.cwd);
|
|
797
|
+
const buffer = await fs.promises.readFile(filePath);
|
|
798
|
+
const blob = new Blob([buffer], { type: file.type || 'application/octet-stream' });
|
|
799
|
+
const fieldName = isCatbox ? 'fileToUpload' : String(file.field || 'file');
|
|
800
|
+
form.append(fieldName, blob, file.name || path.basename(file.path));
|
|
801
|
+
}
|
|
802
|
+
body = form;
|
|
803
|
+
}
|
|
804
|
+
if (body instanceof FormData) {
|
|
805
|
+
delete headers['Content-Type'];
|
|
806
|
+
delete headers['content-type'];
|
|
807
|
+
}
|
|
808
|
+
const finalUrl = new URL(url);
|
|
809
|
+
if (args.query && typeof args.query === 'object') {
|
|
810
|
+
for (const [k, v] of Object.entries(args.query)) finalUrl.searchParams.set(k, String(v));
|
|
811
|
+
}
|
|
812
|
+
let res;
|
|
813
|
+
let lastErr;
|
|
814
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
815
|
+
const controller = new AbortController();
|
|
816
|
+
const timeout = setTimeout(() => controller.abort(), Math.max(1000, Number(args.timeoutMs || 20000)));
|
|
817
|
+
try {
|
|
818
|
+
res = await fetch(finalUrl.toString(), { method, headers, body, signal: controller.signal });
|
|
819
|
+
clearTimeout(timeout);
|
|
820
|
+
break;
|
|
821
|
+
} catch (err) {
|
|
822
|
+
clearTimeout(timeout);
|
|
823
|
+
lastErr = err;
|
|
824
|
+
if (attempt === 0) continue;
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
if (!res) throw lastErr || new Error('fetch fallo');
|
|
828
|
+
const text = await res.text();
|
|
829
|
+
return truncateText(`Status: ${res.status}\nContent-Type: ${res.headers.get('content-type') || '-'}\n\n${text}`);
|
|
830
|
+
}
|
|
671
831
|
|
|
832
|
+
async function scrapeSiteTool(args, state, paint) {
|
|
833
|
+
if (!args.url || typeof args.url !== 'string') throw new Error('scrape_site requiere url');
|
|
834
|
+
if (!args.selectors || typeof args.selectors !== 'object') throw new Error('scrape_site requiere selectors objeto');
|
|
835
|
+
const url = cleanUrl(args.url);
|
|
836
|
+
const allowed = await askConfirmation(state.rl, 'Scrape site', `GET ${url}`, paint, state);
|
|
837
|
+
if (!allowed) return 'Scraping cancelado.';
|
|
838
|
+
if (!(await requireIpConsent(url, state, paint))) return 'Scraping a IP cancelado por falta de consentimiento explícito.';
|
|
672
839
|
const axios = require('axios');
|
|
840
|
+
const res = await axios({
|
|
841
|
+
url,
|
|
842
|
+
method: 'GET',
|
|
843
|
+
headers: { 'User-Agent': 'Mozilla/5.0', ...(args.headers || {}) },
|
|
844
|
+
timeout: 15000,
|
|
845
|
+
responseType: 'text',
|
|
846
|
+
validateStatus: () => true,
|
|
847
|
+
});
|
|
848
|
+
const body = typeof res.data === 'string' ? res.data : String(res.data || '');
|
|
673
849
|
const cheerio = require('cheerio');
|
|
674
|
-
const
|
|
850
|
+
const $ = cheerio.load(body);
|
|
851
|
+
const limit = Math.min(Number(args.limit) || 20, 100);
|
|
852
|
+
const out = {};
|
|
853
|
+
for (const [key, spec] of Object.entries(args.selectors)) {
|
|
854
|
+
const selector = typeof spec === 'string' ? spec : spec?.selector;
|
|
855
|
+
const attr = typeof spec === 'object' ? spec.attribute : null;
|
|
856
|
+
if (!selector) continue;
|
|
857
|
+
const arr = [];
|
|
858
|
+
$(selector).each((i, el) => {
|
|
859
|
+
if (i >= limit) return false;
|
|
860
|
+
const node = $(el);
|
|
861
|
+
const tag = String(el.tagName || '').toLowerCase();
|
|
862
|
+
let value = '';
|
|
863
|
+
if (attr) {
|
|
864
|
+
value = node.attr(attr) || '';
|
|
865
|
+
} else if (tag === 'meta') {
|
|
866
|
+
value = node.attr('content') || '';
|
|
867
|
+
} else if (tag === 'title') {
|
|
868
|
+
value = node.text().trim();
|
|
869
|
+
} else {
|
|
870
|
+
value = node.text().trim();
|
|
871
|
+
}
|
|
872
|
+
if (value) arr.push(value);
|
|
873
|
+
});
|
|
874
|
+
out[key] = arr;
|
|
875
|
+
}
|
|
876
|
+
return truncateText(JSON.stringify(out, null, 2));
|
|
877
|
+
}
|
|
675
878
|
|
|
879
|
+
function htmlToMarkdown(html) {
|
|
880
|
+
const cheerio = require('cheerio');
|
|
881
|
+
const $ = cheerio.load(html);
|
|
882
|
+
$('script,style,noscript').remove();
|
|
883
|
+
const lines = [];
|
|
884
|
+
const root = $('body').length ? $('body') : $.root();
|
|
885
|
+
root.find('h1,h2,h3,h4,h5,h6,p,li,pre,code,blockquote,a,img,button').each((_, el) => {
|
|
886
|
+
const tag = (el.tagName || '').toLowerCase();
|
|
887
|
+
const node = $(el);
|
|
888
|
+
const text = node.text().trim().replace(/\s+/g, ' ');
|
|
889
|
+
if (!text && !['img', 'a'].includes(tag)) return;
|
|
890
|
+
if (tag.startsWith('h')) lines.push(`${'#'.repeat(Number(tag[1]) || 1)} ${text}`);
|
|
891
|
+
else if (tag === 'li') lines.push(`- ${text}`);
|
|
892
|
+
else if (tag === 'a') lines.push(`[${text || 'link'}](${node.attr('href') || ''})`);
|
|
893
|
+
else if (tag === 'img') lines.push(` || ''})`);
|
|
894
|
+
else if (tag === 'button') lines.push(`**[Button]** ${text}`);
|
|
895
|
+
else if (tag === 'blockquote') lines.push(`> ${text}`);
|
|
896
|
+
else if (tag === 'pre' || tag === 'code') lines.push(`\`\`\`\n${node.text()}\n\`\`\``);
|
|
897
|
+
else lines.push(text);
|
|
898
|
+
});
|
|
899
|
+
return lines.join('\n\n').trim();
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
async function webfetchTool(args, state, paint) {
|
|
903
|
+
const rawUrl = String(args.url || '').trim();
|
|
904
|
+
if (!rawUrl) throw new Error('webfetch requiere url');
|
|
905
|
+
const url = cleanUrl(rawUrl);
|
|
906
|
+
const allowed = await askConfirmation(state.rl, 'WebFetch HTML → Markdown', `GET ${url}`, paint, state);
|
|
907
|
+
if (!allowed) return 'WebFetch cancelado por el usuario.';
|
|
908
|
+
if (!(await requireIpConsent(url, state, paint))) {
|
|
909
|
+
return 'WebFetch a IP cancelado por falta de consentimiento explícito.';
|
|
910
|
+
}
|
|
911
|
+
const axios = require('axios');
|
|
676
912
|
const res = await axios({
|
|
677
913
|
url,
|
|
678
914
|
method: 'GET',
|
|
679
915
|
headers: {
|
|
680
|
-
'User-Agent': 'Mozilla/5.0
|
|
681
|
-
'Accept
|
|
916
|
+
'User-Agent': 'Mozilla/5.0',
|
|
917
|
+
'Accept': 'text/html,application/xhtml+xml',
|
|
918
|
+
...(args.headers || {}),
|
|
682
919
|
},
|
|
683
|
-
timeout:
|
|
920
|
+
timeout: Math.max(1000, Number(args.timeoutMs || 20000)),
|
|
684
921
|
responseType: 'text',
|
|
922
|
+
validateStatus: () => true,
|
|
685
923
|
});
|
|
924
|
+
const ct = String(res.headers['content-type'] || '').toLowerCase();
|
|
925
|
+
if (!ct.includes('text/html')) {
|
|
926
|
+
throw new Error(`webfetch solo permite HTML. Content-Type recibido: ${ct || 'desconocido'}`);
|
|
927
|
+
}
|
|
928
|
+
const html = typeof res.data === 'string' ? res.data : String(res.data || '');
|
|
929
|
+
const markdown = htmlToMarkdown(html);
|
|
930
|
+
return truncateText(markdown || '[sin contenido markdown]');
|
|
931
|
+
}
|
|
686
932
|
|
|
687
|
-
|
|
688
|
-
const
|
|
933
|
+
async function webSearchTool(args, state, paint) {
|
|
934
|
+
const query = (args.query || '').trim();
|
|
935
|
+
if (!query) throw new Error('web_search requiere query');
|
|
689
936
|
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
const href = $(el).find('.result__url').attr('href')
|
|
695
|
-
|| $(el).find('.result__a').attr('href') || '';
|
|
696
|
-
if (title) results.push(`${i + 1}. ${title}\n ${href}\n ${snippet}`);
|
|
697
|
-
});
|
|
937
|
+
const allowed = await askConfirmation(
|
|
938
|
+
state.rl, 'Buscar en la web', query, paint, state,
|
|
939
|
+
);
|
|
940
|
+
if (!allowed) return 'Busqueda cancelada.';
|
|
698
941
|
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
942
|
+
const cheerio = require('cheerio');
|
|
943
|
+
const lang = String(args.lang || (state.language === 'es' ? 'es-es' : 'us-en')).toLowerCase();
|
|
944
|
+
const limit = Math.max(1, Math.min(Number(args.limit) || 5, 20));
|
|
945
|
+
const url = `https://html.duckduckgo.com/html/?q=${encodeURIComponent(query)}&kl=${encodeURIComponent(lang)}`;
|
|
946
|
+
const res = await fetch(url, {
|
|
947
|
+
headers: {
|
|
948
|
+
'User-Agent': 'Mozilla/5.0',
|
|
949
|
+
'Accept-Language': lang.startsWith('es') ? 'es-ES,es;q=0.9,en;q=0.7' : 'en-US,en;q=0.9',
|
|
950
|
+
},
|
|
951
|
+
});
|
|
952
|
+
const html = await res.text();
|
|
953
|
+
const $ = cheerio.load(html);
|
|
954
|
+
const results = [];
|
|
955
|
+
$('.result').each((_, el) => {
|
|
956
|
+
if (results.length >= limit) return false;
|
|
957
|
+
const titleEl = $(el).find('.result__a').first();
|
|
958
|
+
const snippetEl = $(el).find('.result__snippet').first();
|
|
959
|
+
let href = titleEl.attr('href') || '';
|
|
960
|
+
const match = href.match(/uddg=([^&]+)/);
|
|
961
|
+
if (match) href = decodeURIComponent(match[1]);
|
|
962
|
+
const title = titleEl.text().trim();
|
|
963
|
+
const snippet = snippetEl.text().trim();
|
|
964
|
+
if (title && href) results.push(`${results.length + 1}. ${title}\n ${href}\n ${snippet}`);
|
|
965
|
+
});
|
|
966
|
+
return results.length
|
|
967
|
+
? `Resultados para: ${query}\nIdioma: ${lang}\n\n${results.join('\n\n')}`
|
|
968
|
+
: 'Sin resultados para esa búsqueda.';
|
|
702
969
|
}
|
|
703
970
|
|
|
704
971
|
async function webReadTool(args, state, paint) {
|
|
@@ -716,6 +983,9 @@ async function webReadTool(args, state, paint) {
|
|
|
716
983
|
state.rl, 'Leer pagina web', url, paint, state,
|
|
717
984
|
);
|
|
718
985
|
if (!allowed) return 'Lectura cancelada.';
|
|
986
|
+
if (!(await requireIpConsent(url, state, paint))) {
|
|
987
|
+
return 'Lectura a IP cancelada por falta de consentimiento explícito.';
|
|
988
|
+
}
|
|
719
989
|
|
|
720
990
|
const axios = require('axios');
|
|
721
991
|
const res = await axios({
|
|
@@ -751,6 +1021,9 @@ async function webReadTool(args, state, paint) {
|
|
|
751
1021
|
}
|
|
752
1022
|
|
|
753
1023
|
async function executeToolCall(call, state, ui) {
|
|
1024
|
+
if (!call || typeof call.tool !== 'string' || !REGISTERED_TOOLS.has(call.tool)) {
|
|
1025
|
+
throw new Error(`Herramienta no registrada: ${call?.tool || 'desconocida'}`);
|
|
1026
|
+
}
|
|
754
1027
|
ui.logEvent(state, 'tool', describeToolCall(call));
|
|
755
1028
|
|
|
756
1029
|
const startTime = Date.now();
|
|
@@ -790,12 +1063,27 @@ async function executeToolCall(call, state, ui) {
|
|
|
790
1063
|
case 'fetch_url':
|
|
791
1064
|
result = await fetchUrlTool(call.args, state, ui.paint);
|
|
792
1065
|
break;
|
|
1066
|
+
case 'fetch_http':
|
|
1067
|
+
result = await fetchHttpTool(call.args, state, ui.paint);
|
|
1068
|
+
break;
|
|
1069
|
+
case 'fetch':
|
|
1070
|
+
result = await fetchHttpTool(call.args, state, ui.paint);
|
|
1071
|
+
break;
|
|
1072
|
+
case 'webfetch':
|
|
1073
|
+
result = await webfetchTool(call.args, state, ui.paint);
|
|
1074
|
+
break;
|
|
1075
|
+
case 'scrape_site':
|
|
1076
|
+
result = await scrapeSiteTool(call.args, state, ui.paint);
|
|
1077
|
+
break;
|
|
793
1078
|
case 'web_search':
|
|
794
1079
|
result = await webSearchTool(call.args, state, ui.paint);
|
|
795
1080
|
break;
|
|
796
1081
|
case 'web_read':
|
|
797
1082
|
result = await webReadTool(call.args, state, ui.paint);
|
|
798
1083
|
break;
|
|
1084
|
+
case 'create_canvas_image':
|
|
1085
|
+
result = await createCanvasImageTool(call.args, state, ui.paint);
|
|
1086
|
+
break;
|
|
799
1087
|
default:
|
|
800
1088
|
throw new Error(`Herramienta no soportada: ${call.tool}`);
|
|
801
1089
|
}
|
|
@@ -819,6 +1107,9 @@ function buildOllamaInstallCommand() {
|
|
|
819
1107
|
|
|
820
1108
|
function parseDirectAction(input) {
|
|
821
1109
|
const text = input.trim();
|
|
1110
|
+
if (/^(git|npm|node|pnpm|yarn)\s+/.test(text)) {
|
|
1111
|
+
return { tool: 'run_command', args: { command: text } };
|
|
1112
|
+
}
|
|
822
1113
|
|
|
823
1114
|
const runMatch = text.match(/^(?:ejecuta|corre)\s+(?:el\s+)?comando\s+([\s\S]+)$/i);
|
|
824
1115
|
if (runMatch) {
|
|
@@ -835,6 +1126,19 @@ function parseDirectAction(input) {
|
|
|
835
1126
|
};
|
|
836
1127
|
}
|
|
837
1128
|
|
|
1129
|
+
const createRepoMatch = text.match(/^(?:crea|crear|create)\s+(?:un\s+)?(?:repo|repositorio)\s+(?:en\s+)?github\s+([a-z0-9._-]+)$/i);
|
|
1130
|
+
if (createRepoMatch) {
|
|
1131
|
+
return {
|
|
1132
|
+
tool: 'git_api_request',
|
|
1133
|
+
args: {
|
|
1134
|
+
provider: 'github',
|
|
1135
|
+
method: 'POST',
|
|
1136
|
+
path: '/user/repos',
|
|
1137
|
+
body: { name: createRepoMatch[1] },
|
|
1138
|
+
},
|
|
1139
|
+
};
|
|
1140
|
+
}
|
|
1141
|
+
|
|
838
1142
|
const mkdirMatch = text.match(/^(?:crea|crear|haz)\s+(?:la\s+)?(?:carpeta|directorio)\s+([^\s]+)$/i);
|
|
839
1143
|
if (mkdirMatch) {
|
|
840
1144
|
return {
|
|
@@ -1002,3 +1306,251 @@ module.exports = {
|
|
|
1002
1306
|
parseDirectAction,
|
|
1003
1307
|
printTools,
|
|
1004
1308
|
};
|
|
1309
|
+
|
|
1310
|
+
|
|
1311
|
+
function getGitSecretLabel(provider, name = '') {
|
|
1312
|
+
const key = normalizeProfileName(provider);
|
|
1313
|
+
if (key === 'custom') return name ? `custom:${name}` : 'custom';
|
|
1314
|
+
return key;
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
function parseColor(value, fallback = 0x000000ff) {
|
|
1318
|
+
if (typeof value !== 'string' || !value.trim()) return fallback;
|
|
1319
|
+
const raw = value.trim().replace('#', '');
|
|
1320
|
+
if (/^[0-9a-f]{6}$/i.test(raw)) return Number.parseInt(`${raw}ff`, 16) >>> 0;
|
|
1321
|
+
if (/^[0-9a-f]{8}$/i.test(raw)) return Number.parseInt(raw, 16) >>> 0;
|
|
1322
|
+
return fallback;
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
function drawRect(image, x, y, w, h, color) {
|
|
1326
|
+
const left = Math.max(0, Math.floor(Number(x) || 0));
|
|
1327
|
+
const top = Math.max(0, Math.floor(Number(y) || 0));
|
|
1328
|
+
const width = Math.max(1, Math.floor(Number(w) || 0));
|
|
1329
|
+
const height = Math.max(1, Math.floor(Number(h) || 0));
|
|
1330
|
+
image.scan(left, top, width, height, function (px, py, idx) {
|
|
1331
|
+
this.bitmap.data.writeUInt32BE(color >>> 0, idx);
|
|
1332
|
+
});
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
function drawRoundRect(image, x, y, w, h, radius, color) {
|
|
1336
|
+
const left = Math.max(0, Math.floor(Number(x) || 0));
|
|
1337
|
+
const top = Math.max(0, Math.floor(Number(y) || 0));
|
|
1338
|
+
const width = Math.max(1, Math.floor(Number(w) || 0));
|
|
1339
|
+
const height = Math.max(1, Math.floor(Number(h) || 0));
|
|
1340
|
+
const r = Math.max(0, Math.min(Number(radius) || 0, Math.floor(width / 2), Math.floor(height / 2)));
|
|
1341
|
+
if (r <= 0) return drawRect(image, left, top, width, height, color);
|
|
1342
|
+
|
|
1343
|
+
image.scan(left, top, width, height, function (px, py, idx) {
|
|
1344
|
+
const cx = px < left + r ? left + r : px >= left + width - r ? left + width - r - 1 : px;
|
|
1345
|
+
const cy = py < top + r ? top + r : py >= top + height - r ? top + height - r - 1 : py;
|
|
1346
|
+
const dx = px - cx;
|
|
1347
|
+
const dy = py - cy;
|
|
1348
|
+
if ((dx * dx) + (dy * dy) <= r * r) {
|
|
1349
|
+
this.bitmap.data.writeUInt32BE(color >>> 0, idx);
|
|
1350
|
+
}
|
|
1351
|
+
});
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
function drawLine(image, x1, y1, x2, y2, color) {
|
|
1355
|
+
const sx = Number(x1 || 0);
|
|
1356
|
+
const sy = Number(y1 || 0);
|
|
1357
|
+
const tx = Number(x2 || 0);
|
|
1358
|
+
const ty = Number(y2 || 0);
|
|
1359
|
+
const steps = Math.max(1, Math.ceil(Math.max(Math.abs(tx - sx), Math.abs(ty - sy))));
|
|
1360
|
+
for (let i = 0; i <= steps; i++) {
|
|
1361
|
+
const x = Math.round(sx + ((tx - sx) * i / steps));
|
|
1362
|
+
const y = Math.round(sy + ((ty - sy) * i / steps));
|
|
1363
|
+
if (x >= 0 && y >= 0 && x < image.bitmap.width && y < image.bitmap.height) {
|
|
1364
|
+
image.setPixelColor(color, x, y);
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
async function createCanvasImageTool(args, state, paint) {
|
|
1370
|
+
const { Jimp, loadFont } = require('jimp');
|
|
1371
|
+
const pluginPrintMain = require.resolve('@jimp/plugin-print');
|
|
1372
|
+
const fontsPath = path.join(path.dirname(pluginPrintMain), 'fonts.js');
|
|
1373
|
+
const fonts = require(fontsPath);
|
|
1374
|
+
const width = Math.max(1, Number(args.width || 0));
|
|
1375
|
+
const height = Math.max(1, Number(args.height || 0));
|
|
1376
|
+
if (!width || !height) {
|
|
1377
|
+
throw new Error('create_canvas_image requiere width y height');
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
const format = String(args.format || 'png').toLowerCase();
|
|
1381
|
+
const safeFormat = ['png', 'jpg', 'jpeg', 'webp', 'bmp', 'gif', 'tiff'].includes(format) ? format : 'png';
|
|
1382
|
+
const fileExt = safeFormat === 'jpeg' ? 'jpg' : safeFormat;
|
|
1383
|
+
const outputPath = resolveInputPath(args.outputPath || path.join('generated', `image-${Date.now()}.${fileExt}`), state.cwd);
|
|
1384
|
+
|
|
1385
|
+
const allowed = await askConfirmation(state.rl, 'Crear imagen', `${width}x${height}\nFormato: ${safeFormat}\nSalida: ${outputPath}`, paint, state);
|
|
1386
|
+
if (!allowed) return 'Creacion de imagen cancelada por el usuario.';
|
|
1387
|
+
|
|
1388
|
+
const bg = args.background && typeof args.background === 'object'
|
|
1389
|
+
? args.background.color || args.background.fill || '#ffffff'
|
|
1390
|
+
: args.background || '#ffffff';
|
|
1391
|
+
const image = new Jimp({ width, height, color: parseColor(bg, 0xffffffff) });
|
|
1392
|
+
const elements = Array.isArray(args.elements) ? args.elements.filter(Boolean) : [];
|
|
1393
|
+
|
|
1394
|
+
for (const element of elements) {
|
|
1395
|
+
if (!element || typeof element !== 'object') continue;
|
|
1396
|
+
const type = String(element.type || 'text').toLowerCase();
|
|
1397
|
+
if (type === 'rect') {
|
|
1398
|
+
const color = parseColor(element.fill || element.color || '#000000', 0x000000ff);
|
|
1399
|
+
if (element.radius) drawRoundRect(image, element.x || 0, element.y || 0, element.w || element.width || 0, element.h || element.height || 0, element.radius || 0, color);
|
|
1400
|
+
else drawRect(image, element.x || 0, element.y || 0, element.w || element.width || 0, element.h || element.height || 0, color);
|
|
1401
|
+
continue;
|
|
1402
|
+
}
|
|
1403
|
+
if (type === 'line') {
|
|
1404
|
+
drawLine(image, element.x1 || 0, element.y1 || 0, element.x2 || 0, element.y2 || 0, parseColor(element.stroke || '#000000', 0x000000ff));
|
|
1405
|
+
continue;
|
|
1406
|
+
}
|
|
1407
|
+
if (type === 'circle' || type === 'ellipse') {
|
|
1408
|
+
const color = parseColor(element.fill || element.color || '#000000', 0x000000ff);
|
|
1409
|
+
const cx = Number(element.x || 0);
|
|
1410
|
+
const cy = Number(element.y || 0);
|
|
1411
|
+
const rx = Math.max(1, Number(element.rx || element.r || element.radius || element.width || 0));
|
|
1412
|
+
const ry = Math.max(1, Number(element.ry || element.r || element.radius || element.height || rx));
|
|
1413
|
+
image.scan(0, 0, image.bitmap.width, image.bitmap.height, function (px, py, idx) {
|
|
1414
|
+
const dx = (px - cx) / rx;
|
|
1415
|
+
const dy = (py - cy) / ry;
|
|
1416
|
+
if ((dx * dx) + (dy * dy) <= 1) {
|
|
1417
|
+
this.bitmap.data.writeUInt32BE(color >>> 0, idx);
|
|
1418
|
+
}
|
|
1419
|
+
});
|
|
1420
|
+
continue;
|
|
1421
|
+
}
|
|
1422
|
+
if (type === 'image') {
|
|
1423
|
+
const src = element.src || element.url || element.path;
|
|
1424
|
+
if (!src) continue;
|
|
1425
|
+
const loaded = await Jimp.read(src.startsWith('http') || src.startsWith('data:') ? src : resolveInputPath(src, state.cwd));
|
|
1426
|
+
const x = Number(element.x || 0);
|
|
1427
|
+
const y = Number(element.y || 0);
|
|
1428
|
+
const w = Math.max(1, Number(element.w || element.width || loaded.bitmap.width));
|
|
1429
|
+
const h = Math.max(1, Number(element.h || element.height || loaded.bitmap.height));
|
|
1430
|
+
const clone = loaded.clone().resize({ w, h });
|
|
1431
|
+
image.composite(clone, x, y);
|
|
1432
|
+
continue;
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
const text = String(element.text || '');
|
|
1436
|
+
if (!text) continue;
|
|
1437
|
+
const size = Math.max(8, Math.min(64, Number(element.fontSize || 32)));
|
|
1438
|
+
const font = await loadFont(
|
|
1439
|
+
size <= 8 ? fonts.SANS_8_BLACK :
|
|
1440
|
+
size <= 16 ? fonts.SANS_16_BLACK :
|
|
1441
|
+
size <= 32 ? fonts.SANS_32_BLACK :
|
|
1442
|
+
fonts.SANS_64_BLACK,
|
|
1443
|
+
);
|
|
1444
|
+
image.print({ font, x: Number(element.x || 0), y: Number(element.y || 0), maxWidth: element.maxWidth ? Number(element.maxWidth) : undefined }, text);
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
await fs.promises.mkdir(path.dirname(outputPath), { recursive: true });
|
|
1448
|
+
await image.write(outputPath);
|
|
1449
|
+
return [`Imagen creada: ${outputPath}`, `Formato: ${safeFormat}`, `Tamano: ${width}x${height}`, `Elementos: ${elements.length}`].join('\\n');
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
|
|
1453
|
+
async function gitSecretSetTool(args) {
|
|
1454
|
+
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
|
+
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
|
+
|
|
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
|
+
});
|
|
1553
|
+
|
|
1554
|
+
const text = typeof response.data === 'string' ? response.data : JSON.stringify(response.data, null, 2);
|
|
1555
|
+
return `Status: ${response.status}\n\n${text}`;
|
|
1556
|
+
}
|