zyn-ai 1.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/package.json +43 -11
- package/src/cli/commands.js +2 -0
- package/src/cli/runtime.js +3 -2
- package/src/config.js +3 -3
- package/src/core/agent.js +4 -2
- package/src/core/prompts.js +12 -4
- package/src/i18n.js +6 -0
- package/src/providers/catalog.js +302 -0
- package/src/tools/index.js +276 -0
- package/src/tui/app.mjs +51 -28
- package/src/utils/secretStorage.js +223 -0
- package/src/utils/taskStorage.js +192 -0
- package/src/web/webAgent.js +2 -2
- package/.github/workflows/publish.yml +0 -23
- package/data/models.example.json +0 -15
- package/data/skills/code-style.md +0 -79
- package/data/skills/completion.md +0 -20
- package/data/skills/core.md +0 -35
- package/data/skills/debugging.md +0 -279
- package/data/skills/domains.md +0 -83
- package/data/skills/frontend_design.md +0 -33
- package/data/skills/methodology.md +0 -84
- package/data/skills/reasoning.md +0 -62
- package/data/skills/testing.md +0 -24
- package/data/skills/thinking.md +0 -146
- package/data/skills/tools.md +0 -102
- package/data/skills/web-agent.md +0 -67
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,
|
|
@@ -28,7 +38,9 @@ const TOOL_DEFINITIONS = [
|
|
|
28
38
|
{ name: 'fetch_url', usage: '{ url, selector?, attribute?, limit?, headers? }' },
|
|
29
39
|
{ name: 'web_search', usage: '{ query }' },
|
|
30
40
|
{ name: 'web_read', usage: '{ url }' },
|
|
41
|
+
{ name: 'create_canvas_image', usage: '{ width, height, background?, elements?, format?, outputPath? }' },
|
|
31
42
|
];
|
|
43
|
+
const REGISTERED_TOOLS = new Set(TOOL_DEFINITIONS.map(tool => tool.name));
|
|
32
44
|
|
|
33
45
|
function getToolPromptText() {
|
|
34
46
|
return [
|
|
@@ -94,6 +106,17 @@ function getToolPromptText() {
|
|
|
94
106
|
' Descarga una pagina web y la convierte a texto legible (sin HTML).',
|
|
95
107
|
' Ideal para leer articulos, documentacion o contenido de paginas.',
|
|
96
108
|
' Ejemplo: {"type":"tool","tool":"web_read","args":{"url":"https://docs.example.com/guide"}}',
|
|
109
|
+
'',
|
|
110
|
+
'## Imagen profesional con Jimp',
|
|
111
|
+
'',
|
|
112
|
+
'create_canvas_image { width, height, background?, elements?, format?, outputPath? }',
|
|
113
|
+
' Crea imagenes desde cero usando Jimp con composicion por elementos.',
|
|
114
|
+
' width/height son obligatorios. background puede ser color HEX (#RRGGBB o #RRGGBBAA).',
|
|
115
|
+
' elements permite combinar rect, circle/ellipse, line, text e image.',
|
|
116
|
+
' Usa este flujo profesional: definir lienzo -> capas base -> tipografia -> detalles -> exportacion.',
|
|
117
|
+
' Ejemplo:',
|
|
118
|
+
' {"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"}]}}',
|
|
119
|
+
'',
|
|
97
120
|
].join('\n');
|
|
98
121
|
}
|
|
99
122
|
|
|
@@ -137,6 +160,8 @@ function describeToolCall(call) {
|
|
|
137
160
|
const readUrl = cleanUrl(call.args.url || '');
|
|
138
161
|
return `Leyendo ${shortText(readUrl, 60)}`;
|
|
139
162
|
}
|
|
163
|
+
case 'create_canvas_image':
|
|
164
|
+
return `Creando imagen ${call.args.width || '?'}x${call.args.height || '?'}`;
|
|
140
165
|
default:
|
|
141
166
|
return call.tool;
|
|
142
167
|
}
|
|
@@ -751,6 +776,9 @@ async function webReadTool(args, state, paint) {
|
|
|
751
776
|
}
|
|
752
777
|
|
|
753
778
|
async function executeToolCall(call, state, ui) {
|
|
779
|
+
if (!call || typeof call.tool !== 'string' || !REGISTERED_TOOLS.has(call.tool)) {
|
|
780
|
+
throw new Error(`Herramienta no registrada: ${call?.tool || 'desconocida'}`);
|
|
781
|
+
}
|
|
754
782
|
ui.logEvent(state, 'tool', describeToolCall(call));
|
|
755
783
|
|
|
756
784
|
const startTime = Date.now();
|
|
@@ -796,6 +824,9 @@ async function executeToolCall(call, state, ui) {
|
|
|
796
824
|
case 'web_read':
|
|
797
825
|
result = await webReadTool(call.args, state, ui.paint);
|
|
798
826
|
break;
|
|
827
|
+
case 'create_canvas_image':
|
|
828
|
+
result = await createCanvasImageTool(call.args, state, ui.paint);
|
|
829
|
+
break;
|
|
799
830
|
default:
|
|
800
831
|
throw new Error(`Herramienta no soportada: ${call.tool}`);
|
|
801
832
|
}
|
|
@@ -819,6 +850,9 @@ function buildOllamaInstallCommand() {
|
|
|
819
850
|
|
|
820
851
|
function parseDirectAction(input) {
|
|
821
852
|
const text = input.trim();
|
|
853
|
+
if (/^(git|npm|node|pnpm|yarn)\s+/.test(text)) {
|
|
854
|
+
return { tool: 'run_command', args: { command: text } };
|
|
855
|
+
}
|
|
822
856
|
|
|
823
857
|
const runMatch = text.match(/^(?:ejecuta|corre)\s+(?:el\s+)?comando\s+([\s\S]+)$/i);
|
|
824
858
|
if (runMatch) {
|
|
@@ -1002,3 +1036,245 @@ module.exports = {
|
|
|
1002
1036
|
parseDirectAction,
|
|
1003
1037
|
printTools,
|
|
1004
1038
|
};
|
|
1039
|
+
|
|
1040
|
+
|
|
1041
|
+
function getGitSecretLabel(provider, name = '') {
|
|
1042
|
+
const key = normalizeProfileName(provider);
|
|
1043
|
+
if (key === 'custom') return name ? `custom:${name}` : 'custom';
|
|
1044
|
+
return key;
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
function parseColor(value, fallback = 0x000000ff) {
|
|
1048
|
+
if (typeof value !== 'string' || !value.trim()) return fallback;
|
|
1049
|
+
const raw = value.trim().replace('#', '');
|
|
1050
|
+
if (/^[0-9a-f]{6}$/i.test(raw)) return Number.parseInt(`${raw}ff`, 16) >>> 0;
|
|
1051
|
+
if (/^[0-9a-f]{8}$/i.test(raw)) return Number.parseInt(raw, 16) >>> 0;
|
|
1052
|
+
return fallback;
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
function drawRect(image, x, y, w, h, color) {
|
|
1056
|
+
const left = Math.max(0, Math.floor(Number(x) || 0));
|
|
1057
|
+
const top = Math.max(0, Math.floor(Number(y) || 0));
|
|
1058
|
+
const width = Math.max(1, Math.floor(Number(w) || 0));
|
|
1059
|
+
const height = Math.max(1, Math.floor(Number(h) || 0));
|
|
1060
|
+
image.scan(left, top, width, height, function (px, py, idx) {
|
|
1061
|
+
this.bitmap.data.writeUInt32BE(color >>> 0, idx);
|
|
1062
|
+
});
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
function drawRoundRect(image, x, y, w, h, radius, color) {
|
|
1066
|
+
const left = Math.max(0, Math.floor(Number(x) || 0));
|
|
1067
|
+
const top = Math.max(0, Math.floor(Number(y) || 0));
|
|
1068
|
+
const width = Math.max(1, Math.floor(Number(w) || 0));
|
|
1069
|
+
const height = Math.max(1, Math.floor(Number(h) || 0));
|
|
1070
|
+
const r = Math.max(0, Math.min(Number(radius) || 0, Math.floor(width / 2), Math.floor(height / 2)));
|
|
1071
|
+
if (r <= 0) return drawRect(image, left, top, width, height, color);
|
|
1072
|
+
|
|
1073
|
+
image.scan(left, top, width, height, function (px, py, idx) {
|
|
1074
|
+
const cx = px < left + r ? left + r : px >= left + width - r ? left + width - r - 1 : px;
|
|
1075
|
+
const cy = py < top + r ? top + r : py >= top + height - r ? top + height - r - 1 : py;
|
|
1076
|
+
const dx = px - cx;
|
|
1077
|
+
const dy = py - cy;
|
|
1078
|
+
if ((dx * dx) + (dy * dy) <= r * r) {
|
|
1079
|
+
this.bitmap.data.writeUInt32BE(color >>> 0, idx);
|
|
1080
|
+
}
|
|
1081
|
+
});
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
function drawLine(image, x1, y1, x2, y2, color) {
|
|
1085
|
+
const sx = Number(x1 || 0);
|
|
1086
|
+
const sy = Number(y1 || 0);
|
|
1087
|
+
const tx = Number(x2 || 0);
|
|
1088
|
+
const ty = Number(y2 || 0);
|
|
1089
|
+
const steps = Math.max(1, Math.ceil(Math.max(Math.abs(tx - sx), Math.abs(ty - sy))));
|
|
1090
|
+
for (let i = 0; i <= steps; i++) {
|
|
1091
|
+
const x = Math.round(sx + ((tx - sx) * i / steps));
|
|
1092
|
+
const y = Math.round(sy + ((ty - sy) * i / steps));
|
|
1093
|
+
if (x >= 0 && y >= 0 && x < image.bitmap.width && y < image.bitmap.height) {
|
|
1094
|
+
image.setPixelColor(color, x, y);
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
async function createCanvasImageTool(args, state, paint) {
|
|
1100
|
+
const { Jimp, loadFont } = require('jimp');
|
|
1101
|
+
const pluginPrintMain = require.resolve('@jimp/plugin-print');
|
|
1102
|
+
const fontsPath = path.join(path.dirname(pluginPrintMain), 'fonts.js');
|
|
1103
|
+
const fonts = require(fontsPath);
|
|
1104
|
+
const width = Math.max(1, Number(args.width || 0));
|
|
1105
|
+
const height = Math.max(1, Number(args.height || 0));
|
|
1106
|
+
if (!width || !height) {
|
|
1107
|
+
throw new Error('create_canvas_image requiere width y height');
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
const format = String(args.format || 'png').toLowerCase();
|
|
1111
|
+
const safeFormat = ['png', 'jpg', 'jpeg', 'webp', 'bmp', 'gif', 'tiff'].includes(format) ? format : 'png';
|
|
1112
|
+
const fileExt = safeFormat === 'jpeg' ? 'jpg' : safeFormat;
|
|
1113
|
+
const outputPath = resolveInputPath(args.outputPath || path.join('generated', `image-${Date.now()}.${fileExt}`), state.cwd);
|
|
1114
|
+
|
|
1115
|
+
const allowed = await askConfirmation(state.rl, 'Crear imagen', `${width}x${height}\nFormato: ${safeFormat}\nSalida: ${outputPath}`, paint, state);
|
|
1116
|
+
if (!allowed) return 'Creacion de imagen cancelada por el usuario.';
|
|
1117
|
+
|
|
1118
|
+
const bg = args.background && typeof args.background === 'object'
|
|
1119
|
+
? args.background.color || args.background.fill || '#ffffff'
|
|
1120
|
+
: args.background || '#ffffff';
|
|
1121
|
+
const image = new Jimp({ width, height, color: parseColor(bg, 0xffffffff) });
|
|
1122
|
+
const elements = Array.isArray(args.elements) ? args.elements.filter(Boolean) : [];
|
|
1123
|
+
|
|
1124
|
+
for (const element of elements) {
|
|
1125
|
+
if (!element || typeof element !== 'object') continue;
|
|
1126
|
+
const type = String(element.type || 'text').toLowerCase();
|
|
1127
|
+
if (type === 'rect') {
|
|
1128
|
+
const color = parseColor(element.fill || element.color || '#000000', 0x000000ff);
|
|
1129
|
+
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);
|
|
1130
|
+
else drawRect(image, element.x || 0, element.y || 0, element.w || element.width || 0, element.h || element.height || 0, color);
|
|
1131
|
+
continue;
|
|
1132
|
+
}
|
|
1133
|
+
if (type === 'line') {
|
|
1134
|
+
drawLine(image, element.x1 || 0, element.y1 || 0, element.x2 || 0, element.y2 || 0, parseColor(element.stroke || '#000000', 0x000000ff));
|
|
1135
|
+
continue;
|
|
1136
|
+
}
|
|
1137
|
+
if (type === 'circle' || type === 'ellipse') {
|
|
1138
|
+
const color = parseColor(element.fill || element.color || '#000000', 0x000000ff);
|
|
1139
|
+
const cx = Number(element.x || 0);
|
|
1140
|
+
const cy = Number(element.y || 0);
|
|
1141
|
+
const rx = Math.max(1, Number(element.rx || element.r || element.radius || element.width || 0));
|
|
1142
|
+
const ry = Math.max(1, Number(element.ry || element.r || element.radius || element.height || rx));
|
|
1143
|
+
image.scan(0, 0, image.bitmap.width, image.bitmap.height, function (px, py, idx) {
|
|
1144
|
+
const dx = (px - cx) / rx;
|
|
1145
|
+
const dy = (py - cy) / ry;
|
|
1146
|
+
if ((dx * dx) + (dy * dy) <= 1) {
|
|
1147
|
+
this.bitmap.data.writeUInt32BE(color >>> 0, idx);
|
|
1148
|
+
}
|
|
1149
|
+
});
|
|
1150
|
+
continue;
|
|
1151
|
+
}
|
|
1152
|
+
if (type === 'image') {
|
|
1153
|
+
const src = element.src || element.url || element.path;
|
|
1154
|
+
if (!src) continue;
|
|
1155
|
+
const loaded = await Jimp.read(src.startsWith('http') || src.startsWith('data:') ? src : resolveInputPath(src, state.cwd));
|
|
1156
|
+
const x = Number(element.x || 0);
|
|
1157
|
+
const y = Number(element.y || 0);
|
|
1158
|
+
const w = Math.max(1, Number(element.w || element.width || loaded.bitmap.width));
|
|
1159
|
+
const h = Math.max(1, Number(element.h || element.height || loaded.bitmap.height));
|
|
1160
|
+
const clone = loaded.clone().resize({ w, h });
|
|
1161
|
+
image.composite(clone, x, y);
|
|
1162
|
+
continue;
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
const text = String(element.text || '');
|
|
1166
|
+
if (!text) continue;
|
|
1167
|
+
const size = Math.max(8, Math.min(64, Number(element.fontSize || 32)));
|
|
1168
|
+
const font = await loadFont(
|
|
1169
|
+
size <= 8 ? fonts.SANS_8_BLACK :
|
|
1170
|
+
size <= 16 ? fonts.SANS_16_BLACK :
|
|
1171
|
+
size <= 32 ? fonts.SANS_32_BLACK :
|
|
1172
|
+
fonts.SANS_64_BLACK,
|
|
1173
|
+
);
|
|
1174
|
+
image.print({ font, x: Number(element.x || 0), y: Number(element.y || 0), maxWidth: element.maxWidth ? Number(element.maxWidth) : undefined }, text);
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
await fs.promises.mkdir(path.dirname(outputPath), { recursive: true });
|
|
1178
|
+
await image.write(outputPath);
|
|
1179
|
+
return [`Imagen creada: ${outputPath}`, `Formato: ${safeFormat}`, `Tamano: ${width}x${height}`, `Elementos: ${elements.length}`].join('\\n');
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
|
|
1183
|
+
async function gitSecretSetTool(args) {
|
|
1184
|
+
const provider = normalizeProfileName(args.provider || '');
|
|
1185
|
+
if (!provider) throw new Error('git_secret_set requiere provider');
|
|
1186
|
+
if (!args.token || typeof args.token !== 'string') throw new Error('git_secret_set requiere token');
|
|
1187
|
+
const saved = upsertGitSecret(provider, args);
|
|
1188
|
+
return `Credencial guardada: ${getGitSecretLabel(provider, saved?.name || args.name || '')}`;
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
async function gitSecretListTool(args) {
|
|
1192
|
+
const provider = normalizeProfileName(args.provider || '');
|
|
1193
|
+
const name = String(args.name || '').trim();
|
|
1194
|
+
const secrets = listGitSecrets();
|
|
1195
|
+
const filtered = secrets.filter(secret => {
|
|
1196
|
+
if (!provider) return true;
|
|
1197
|
+
if (provider === 'custom') return !name || secret.key === `custom:${name}`;
|
|
1198
|
+
return secret.key === provider;
|
|
1199
|
+
});
|
|
1200
|
+
if (!filtered.length) return 'No hay credenciales Git guardadas.';
|
|
1201
|
+
return filtered.map(secret => [
|
|
1202
|
+
`${secret.key}`,
|
|
1203
|
+
` username: ${secret.username || '-'}`,
|
|
1204
|
+
` apiBaseUrl: ${secret.apiBaseUrl || '-'}`,
|
|
1205
|
+
` cloneBaseUrl: ${secret.cloneBaseUrl || '-'}`,
|
|
1206
|
+
` authHeader: ${secret.authHeader || '-'}`,
|
|
1207
|
+
` token: ${secret.token}`,
|
|
1208
|
+
].join('\n')).join('\n\n');
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
async function gitSecretRemoveTool(args) {
|
|
1212
|
+
const provider = normalizeProfileName(args.provider || '');
|
|
1213
|
+
if (!provider) throw new Error('git_secret_remove requiere provider');
|
|
1214
|
+
const name = String(args.name || '').trim();
|
|
1215
|
+
const removed = removeGitSecret(provider, name);
|
|
1216
|
+
return removed ? `Credencial eliminada: ${getGitSecretLabel(provider, name)}` : 'No encontre esa credencial.';
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
async function gitCloneRepoTool(args, state, paint) {
|
|
1220
|
+
const repoUrl = String(args.repoUrl || '').trim();
|
|
1221
|
+
if (!repoUrl) throw new Error('git_clone_repo requiere repoUrl');
|
|
1222
|
+
const provider = normalizeProfileName(args.provider || '');
|
|
1223
|
+
const name = String(args.name || '').trim();
|
|
1224
|
+
const profile = provider ? resolveGitProfile(provider, name) : null;
|
|
1225
|
+
const finalUrl = buildCloneUrl(repoUrl, profile || {});
|
|
1226
|
+
const destination = args.destination ? resolveInputPath(args.destination, state.cwd) : '';
|
|
1227
|
+
const timeoutMs = Math.max(1000, Number.isFinite(Number(args.timeoutMs)) ? Number(args.timeoutMs) : 10 * 60 * 1000);
|
|
1228
|
+
|
|
1229
|
+
const allowed = await askConfirmation(state.rl, 'Clonar repositorio', [
|
|
1230
|
+
repoUrl,
|
|
1231
|
+
profile ? `Provider: ${provider}${name ? ` (${name})` : ''}` : 'Provider: direct',
|
|
1232
|
+
destination ? `Destino: ${destination}` : null,
|
|
1233
|
+
`Timeout: ${timeoutMs}ms`,
|
|
1234
|
+
].filter(Boolean).join('\n'), paint, state);
|
|
1235
|
+
if (!allowed) return 'Clonado cancelado por el usuario.';
|
|
1236
|
+
|
|
1237
|
+
const result = await runProcess('git', ['clone', ...(args.branch ? ['--branch', String(args.branch)] : []), finalUrl, ...(destination ? [destination] : [])], { cwd: state.cwd, timeoutMs });
|
|
1238
|
+
const lines = [`Exit code: ${result.code}`];
|
|
1239
|
+
if (result.timedOut) lines.push('Timeout: el clon fue detenido por tiempo.');
|
|
1240
|
+
if (result.stdout.trim()) lines.push(`STDOUT:\n${result.stdout.trim()}`);
|
|
1241
|
+
if (result.stderr.trim()) lines.push(`STDERR:\n${result.stderr.trim()}`);
|
|
1242
|
+
return lines.join('\n\n');
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
async function gitApiRequestTool(args, state, paint) {
|
|
1246
|
+
const provider = normalizeProfileName(args.provider || '');
|
|
1247
|
+
if (!provider) throw new Error('git_api_request requiere provider');
|
|
1248
|
+
const pathValue = String(args.path || '').trim();
|
|
1249
|
+
if (!pathValue) throw new Error('git_api_request requiere path');
|
|
1250
|
+
const name = String(args.name || '').trim();
|
|
1251
|
+
const profile = resolveGitProfile(provider, name);
|
|
1252
|
+
if (!profile) throw new Error(`No hay credenciales para ${provider}${provider === 'custom' && name ? `:${name}` : ''}`);
|
|
1253
|
+
const baseUrl = getApiBaseUrl(provider, profile);
|
|
1254
|
+
if (!baseUrl) throw new Error(`No hay apiBaseUrl para ${provider}`);
|
|
1255
|
+
const url = `${baseUrl.replace(/\/+$/, '')}/${pathValue.replace(/^\/+/, '')}`;
|
|
1256
|
+
const timeoutMs = Math.max(1000, Number.isFinite(Number(args.timeoutMs)) ? Number(args.timeoutMs) : 15000);
|
|
1257
|
+
const method = String(args.method || 'GET').toUpperCase();
|
|
1258
|
+
const headers = buildApiHeaders(provider, profile, args.headers && typeof args.headers === 'object' ? args.headers : {});
|
|
1259
|
+
|
|
1260
|
+
const allowed = await askConfirmation(state.rl, 'Git API request', `${method} ${url}\nTimeout: ${timeoutMs}ms`, paint, state);
|
|
1261
|
+
if (!allowed) return 'Request cancelado por el usuario.';
|
|
1262
|
+
|
|
1263
|
+
const axios = require('axios');
|
|
1264
|
+
const response = await axios({
|
|
1265
|
+
url,
|
|
1266
|
+
method,
|
|
1267
|
+
headers: {
|
|
1268
|
+
'User-Agent': 'Zyn/1.0',
|
|
1269
|
+
Accept: 'application/json, text/plain, */*',
|
|
1270
|
+
...headers,
|
|
1271
|
+
},
|
|
1272
|
+
data: args.body && typeof args.body === 'object' ? args.body : args.body,
|
|
1273
|
+
timeout: timeoutMs,
|
|
1274
|
+
responseType: 'text',
|
|
1275
|
+
validateStatus: () => true,
|
|
1276
|
+
});
|
|
1277
|
+
|
|
1278
|
+
const text = typeof response.data === 'string' ? response.data : JSON.stringify(response.data, null, 2);
|
|
1279
|
+
return `Status: ${response.status}\n\n${text}`;
|
|
1280
|
+
}
|
package/src/tui/app.mjs
CHANGED
|
@@ -17,9 +17,17 @@ const {
|
|
|
17
17
|
DEFAULT_MODEL_KEY,
|
|
18
18
|
MODELS,
|
|
19
19
|
} = require('../config');
|
|
20
|
+
const { normalizeLanguage } = require('../i18n');
|
|
20
21
|
|
|
21
22
|
const h = React.createElement;
|
|
23
|
+
function getTuiLang() {
|
|
24
|
+
return normalizeLanguage(global.__zynCurrentLanguage || 'en');
|
|
25
|
+
}
|
|
26
|
+
function uiText(en, es) {
|
|
27
|
+
return getTuiLang() === 'es' ? es : en;
|
|
28
|
+
}
|
|
22
29
|
const MAX_THINKING_LINES = 20;
|
|
30
|
+
const MAX_PASTE_PREVIEW = 200;
|
|
23
31
|
const SPIN_MS = 80;
|
|
24
32
|
|
|
25
33
|
const SPIN_FRAMES = ['\u280b', '\u2819', '\u2839', '\u2838', '\u283c', '\u2834', '\u2826', '\u2827', '\u2807', '\u280f'];
|
|
@@ -372,7 +380,7 @@ function Banner({ model, resumed, width, cwd }) {
|
|
|
372
380
|
return s + ' '.repeat(Math.max(0, inner - s.length));
|
|
373
381
|
};
|
|
374
382
|
|
|
375
|
-
const sessionLabel = resumed ? 'sesion reanudada' : 'sesion nueva';
|
|
383
|
+
const sessionLabel = resumed ? uiText('session resumed', 'sesion reanudada') : uiText('new session', 'sesion nueva');
|
|
376
384
|
const cwdShort = cwd && cwd.length > inner - 6 ? '...' + cwd.slice(-(inner - 9)) : (cwd || '.');
|
|
377
385
|
|
|
378
386
|
return h(Box, { flexDirection: 'column', paddingTop: 1, paddingBottom: 0 },
|
|
@@ -429,10 +437,15 @@ function EventLine({ kind, title, detail }) {
|
|
|
429
437
|
};
|
|
430
438
|
const { sym, color } = cfg[kind] || cfg.info;
|
|
431
439
|
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
440
|
+
const compactTitle = String(title || '').replace(/\s{2,}/g, ' ').trim();
|
|
441
|
+
const compactDetail = String(detail || '').replace(/\s{2,}/g, ' ').trim();
|
|
442
|
+
|
|
443
|
+
return h(Box, { paddingLeft: 3, flexDirection: 'column' },
|
|
444
|
+
h(Box, { gap: 1 },
|
|
445
|
+
h(Text, { color }, sym),
|
|
446
|
+
h(Text, { color: kind === 'tool' ? T.textDim : T.textMuted, wrap: 'wrap' }, compactTitle),
|
|
447
|
+
),
|
|
448
|
+
compactDetail ? h(Box, { paddingLeft: 2 }, h(Text, { color: T.textGhost, wrap: 'wrap' }, compactDetail)) : null,
|
|
436
449
|
);
|
|
437
450
|
}
|
|
438
451
|
|
|
@@ -441,7 +454,7 @@ function UserMessage({ text }) {
|
|
|
441
454
|
h(Box, { flexDirection: 'column' },
|
|
442
455
|
h(Box, { gap: 1, marginBottom: 0 },
|
|
443
456
|
h(Text, { color: T.accent, bold: true }, '\u29bf'),
|
|
444
|
-
h(Text, { color: T.textDim, bold: true }, 'You'),
|
|
457
|
+
h(Text, { color: T.textDim, bold: true }, uiText('You', 'Tú')),
|
|
445
458
|
),
|
|
446
459
|
h(Box, { paddingLeft: 2 },
|
|
447
460
|
h(Text, { color: T.text, wrap: 'wrap' }, text),
|
|
@@ -465,17 +478,17 @@ function ThinkingBlock({ text, elapsed, live, width }) {
|
|
|
465
478
|
const pulseChar = live ? SPIN_FRAMES[Math.floor(Date.now() / SPIN_MS) % SPIN_FRAMES.length] : '\u25d0';
|
|
466
479
|
|
|
467
480
|
const label = live
|
|
468
|
-
? pulseChar + ' Pensando...'
|
|
469
|
-
: '\u25d0
|
|
481
|
+
? pulseChar + ' ' + uiText('Thinking...', 'Pensando...')
|
|
482
|
+
: '\u25d0 ' + uiText('Thought for ', 'Pensó durante ') + elapsed + 's';
|
|
470
483
|
|
|
471
|
-
return h(Box, { flexDirection: 'column', paddingLeft:
|
|
484
|
+
return h(Box, { flexDirection: 'column', paddingLeft: 3, marginTop: 1 },
|
|
472
485
|
h(Text, { color: T.textGhost }, label),
|
|
473
486
|
lines.length > 0
|
|
474
|
-
? h(Box, { flexDirection: 'column', paddingLeft:
|
|
487
|
+
? h(Box, { flexDirection: 'column', paddingLeft: 1 },
|
|
475
488
|
...lines.map((line, i) =>
|
|
476
489
|
h(Text, { key: String(i), color: T.textInvis, wrap: 'wrap' }, line),
|
|
477
490
|
),
|
|
478
|
-
more > 0 ? h(Text, { color: T.textGhost }, '\u00b7\u00b7\u00b7 ' + more + '
|
|
491
|
+
more > 0 ? h(Text, { color: T.textGhost }, '\u00b7\u00b7\u00b7 ' + more + ' ' + uiText('more lines', 'líneas más')) : null,
|
|
479
492
|
)
|
|
480
493
|
: null,
|
|
481
494
|
);
|
|
@@ -534,10 +547,10 @@ function ConfirmBar({ title, detail }) {
|
|
|
534
547
|
: null,
|
|
535
548
|
h(Box, { marginTop: 0, paddingLeft: 2, gap: 2 },
|
|
536
549
|
h(Text, { color: T.green, bold: true }, '[y]'),
|
|
537
|
-
h(Text, { color: T.textMuted }, 'permitir'),
|
|
550
|
+
h(Text, { color: T.textMuted }, uiText('allow', 'permitir')),
|
|
538
551
|
h(Text, { color: T.textInvis }, '\u00b7'),
|
|
539
552
|
h(Text, { color: T.red, bold: true }, '[n]'),
|
|
540
|
-
h(Text, { color: T.textMuted }, 'denegar'),
|
|
553
|
+
h(Text, { color: T.textMuted }, uiText('deny', 'denegar')),
|
|
541
554
|
),
|
|
542
555
|
);
|
|
543
556
|
}
|
|
@@ -566,13 +579,13 @@ function StatusBar({ model, processing, width, turnCount }) {
|
|
|
566
579
|
? h(Text, { color: T.accentSoft }, SPIN_FRAMES[frame])
|
|
567
580
|
: null,
|
|
568
581
|
turnCount > 0
|
|
569
|
-
? h(Text, { color: T.textInvis }, '\u00b7 ' + turnCount + (turnCount === 1 ? ' turno' : '
|
|
582
|
+
? h(Text, { color: T.textInvis }, '\u00b7 ' + turnCount + ' ' + uiText(turnCount === 1 ? 'turn' : 'turns', turnCount === 1 ? 'turno' : 'turnos'))
|
|
570
583
|
: null,
|
|
571
584
|
),
|
|
572
585
|
h(Box, { gap: 1 },
|
|
573
586
|
h(Text, { color: T.textInvis }, '/help'),
|
|
574
587
|
h(Text, { color: T.textInvis }, '\u00b7'),
|
|
575
|
-
h(Text, { color: T.textInvis }, 'esc salir'),
|
|
588
|
+
h(Text, { color: T.textInvis }, uiText('esc exit', 'esc salir')),
|
|
576
589
|
),
|
|
577
590
|
),
|
|
578
591
|
);
|
|
@@ -709,8 +722,10 @@ function InputBar({ onSubmit, processing }) {
|
|
|
709
722
|
}
|
|
710
723
|
|
|
711
724
|
if (input && !key.ctrl && !key.meta) {
|
|
712
|
-
|
|
713
|
-
|
|
725
|
+
const normalizedInput = input.replace(/\r\n/g, '\n');
|
|
726
|
+
const safeInput = normalizedInput.includes('\n') ? normalizedInput.replace(/\n/g, ' ') : normalizedInput;
|
|
727
|
+
setValue(v => v.slice(0, cursor) + safeInput + v.slice(cursor));
|
|
728
|
+
setCursor(c => c + safeInput.length);
|
|
714
729
|
setSuggestIdx(0);
|
|
715
730
|
}
|
|
716
731
|
});
|
|
@@ -721,7 +736,7 @@ function InputBar({ onSubmit, processing }) {
|
|
|
721
736
|
const after = value.slice(cursor + 1);
|
|
722
737
|
|
|
723
738
|
const promptColor = processing ? T.amber : T.accent;
|
|
724
|
-
const placeholder = processing ? ' En cola — escribe y se procesará después...' : ' Escribe un mensaje...';
|
|
739
|
+
const placeholder = processing ? uiText(' Queued — type and it will run later...', ' En cola — escribe y se procesará después...') : uiText(' Type a message...', ' Escribe un mensaje...');
|
|
725
740
|
|
|
726
741
|
const inputLine = h(Box, { paddingLeft: 3, paddingRight: 3, paddingTop: 0, paddingBottom: 0, marginTop: 1 },
|
|
727
742
|
h(Text, { color: promptColor }, processing ? '\u{1F4E9} ' : '\u276f '),
|
|
@@ -749,7 +764,7 @@ function InputBar({ onSubmit, processing }) {
|
|
|
749
764
|
inputLine,
|
|
750
765
|
h(Box, { flexDirection: 'column', paddingLeft: 5, marginTop: 0 },
|
|
751
766
|
hasMore && windowStart > 0
|
|
752
|
-
? h(Text, { color: T.textInvis }, ' \u2191
|
|
767
|
+
? h(Text, { color: T.textInvis }, ' \u2191 ' + uiText('more', 'más'))
|
|
753
768
|
: null,
|
|
754
769
|
...visible.map((cmd, i) => {
|
|
755
770
|
const realIdx = windowStart + i;
|
|
@@ -766,10 +781,10 @@ function InputBar({ onSubmit, processing }) {
|
|
|
766
781
|
);
|
|
767
782
|
}),
|
|
768
783
|
hasMore && windowStart + maxVisible < suggestions.length
|
|
769
|
-
? h(Text, { color: T.textInvis }, ' \u2193
|
|
784
|
+
? h(Text, { color: T.textInvis }, ' \u2193 ' + uiText('more', 'más'))
|
|
770
785
|
: null,
|
|
771
786
|
h(Box, { paddingTop: 0 },
|
|
772
|
-
h(Text, { color: T.textInvis }, 'Tab completar
|
|
787
|
+
h(Text, { color: T.textInvis }, uiText('Tab complete · ↑↓ navigate', 'Tab completar · ↑↓ navegar')),
|
|
773
788
|
),
|
|
774
789
|
),
|
|
775
790
|
);
|
|
@@ -777,19 +792,20 @@ function InputBar({ onSubmit, processing }) {
|
|
|
777
792
|
|
|
778
793
|
function App({ store, state, onSubmit }) {
|
|
779
794
|
useStore(store);
|
|
795
|
+
global.__zynCurrentLanguage = state?.language || 'en';
|
|
780
796
|
const { exit } = useApp();
|
|
781
797
|
const { width } = useDimensions();
|
|
782
798
|
|
|
783
799
|
const modelKey = state?.activeModel || DEFAULT_MODEL_KEY;
|
|
784
800
|
const modelLabel = state?.concuerdo
|
|
785
|
-
? 'Concuerdo · ' + Object.values(MODELS).map(m => m.label).join(', ')
|
|
801
|
+
? uiText('Concord · ', 'Concuerdo · ') + Object.values(MODELS).map(m => m.label).join(', ')
|
|
786
802
|
: (MODELS[modelKey]?.label || modelKey).toLowerCase();
|
|
787
803
|
|
|
788
804
|
const handleInput = useCallback((text) => {
|
|
789
805
|
if (text === '/exit' || text === '/quit') {
|
|
790
806
|
if (store.processing) {
|
|
791
807
|
store.pendingExit = true;
|
|
792
|
-
store.addEvent('info', 'saliendo al terminar el turno actual');
|
|
808
|
+
store.addEvent('info', uiText('exiting after current turn', 'saliendo al terminar el turno actual'));
|
|
793
809
|
return;
|
|
794
810
|
}
|
|
795
811
|
exit();
|
|
@@ -809,10 +825,10 @@ function App({ store, state, onSubmit }) {
|
|
|
809
825
|
state.abortCurrentTurn();
|
|
810
826
|
}
|
|
811
827
|
store.pendingExit = false;
|
|
812
|
-
store.addEvent('warn', 'agente detenido', 'Interrumpido con ESC x2');
|
|
828
|
+
store.addEvent('warn', uiText('agent stopped', 'agente detenido'), uiText('Interrupted with ESC x2', 'Interrumpido con ESC x2'));
|
|
813
829
|
} else {
|
|
814
830
|
store.lastEscapeAt = now;
|
|
815
|
-
store.addEvent('info', 'pulsa ESC otra vez', 'para detener el agente');
|
|
831
|
+
store.addEvent('info', uiText('press ESC again', 'pulsa ESC otra vez'), uiText('to stop the agent', 'para detener el agente'));
|
|
816
832
|
}
|
|
817
833
|
return;
|
|
818
834
|
}
|
|
@@ -914,7 +930,7 @@ export async function startTUI(options = {}) {
|
|
|
914
930
|
const processInput = async (input) => {
|
|
915
931
|
if (input === '/exit' || input === '/quit') {
|
|
916
932
|
store.pendingExit = true;
|
|
917
|
-
store.addEvent('info', 'hasta luego');
|
|
933
|
+
store.addEvent('info', uiText('bye', 'hasta luego'));
|
|
918
934
|
return;
|
|
919
935
|
}
|
|
920
936
|
|
|
@@ -942,9 +958,9 @@ export async function startTUI(options = {}) {
|
|
|
942
958
|
const clean = lines.filter(l => l.trim()).join('\n');
|
|
943
959
|
if (clean) store.addItem({ type: 'system', text: clean });
|
|
944
960
|
}
|
|
945
|
-
if (!handled) store.addEvent('warn', 'comando no reconocido', input);
|
|
961
|
+
if (!handled) store.addEvent('warn', uiText('command not recognized', 'comando no reconocido'), input);
|
|
946
962
|
} catch (err) {
|
|
947
|
-
store.addEvent('error', 'error', err.message);
|
|
963
|
+
store.addEvent('error', uiText('error', 'error'), err.message);
|
|
948
964
|
} finally {
|
|
949
965
|
console.log = origLog;
|
|
950
966
|
console.error = origError;
|
|
@@ -983,6 +999,13 @@ export async function startTUI(options = {}) {
|
|
|
983
999
|
let appInstance = null;
|
|
984
1000
|
|
|
985
1001
|
const handleSubmit = async (input) => {
|
|
1002
|
+
if (typeof input === 'string' && input.length > MAX_PASTE_PREVIEW) {
|
|
1003
|
+
store.addEvent(
|
|
1004
|
+
'info',
|
|
1005
|
+
`[ ${uiText('Pasted Text', 'Texto pegado')} of ${input.length} ${uiText('Characters', 'Caracteres')} ]`,
|
|
1006
|
+
input.slice(0, MAX_PASTE_PREVIEW) + '...'
|
|
1007
|
+
);
|
|
1008
|
+
}
|
|
986
1009
|
if (input.startsWith('/') && store.processing) {
|
|
987
1010
|
await processInput(input);
|
|
988
1011
|
return;
|