zyn-ai 1.2.1 → 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 +6 -4
- 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/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/README.md
CHANGED
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zyn-ai",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "Production-ready AI agent for CLI and web with real tool execution and automation",
|
|
5
|
-
"author": "Maycol
|
|
5
|
+
"author": "Maycol",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"ai-agent",
|
|
8
8
|
"cli-ai",
|
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
"zyn"
|
|
19
19
|
],
|
|
20
20
|
"bin": {
|
|
21
|
-
"zyn": "./zyn.js"
|
|
21
|
+
"zyn": "./zyn.js",
|
|
22
|
+
"Zyn": "./zyn.js"
|
|
22
23
|
},
|
|
23
24
|
"type": "commonjs",
|
|
24
25
|
"scripts": {
|
|
@@ -36,7 +37,8 @@
|
|
|
36
37
|
"ink": "^6.8.0",
|
|
37
38
|
"keypress": "^0.2.1",
|
|
38
39
|
"react": "^19.0.0",
|
|
39
|
-
"session-file-store": "^1.5.0"
|
|
40
|
+
"session-file-store": "^1.5.0",
|
|
41
|
+
"jimp": "^1.6.1"
|
|
40
42
|
},
|
|
41
43
|
"repository": {
|
|
42
44
|
"type": "git",
|
package/src/cli/commands.js
CHANGED
|
@@ -7,6 +7,7 @@ const { listSkills, SKILLS_DIR } = require('../core/skills');
|
|
|
7
7
|
const { DEFAULT_LANGUAGE, DEFAULT_MODEL_KEY, MODELS, listProvidersFromModels } = require('../config');
|
|
8
8
|
const { languageLabel, normalizeLanguage, t } = require('../i18n');
|
|
9
9
|
const { createNewSessionState, listSessions, loadSessionState, saveState } = require('../utils/sessionStorage');
|
|
10
|
+
const { listGitSecrets, removeGitSecret, upsertGitSecret } = require('../utils/secretStorage');
|
|
10
11
|
const { exportTranscriptText, formatTranscriptPreview } = require('../utils/transcriptStorage');
|
|
11
12
|
const { resolveInputPath } = require('../utils/pathUtils');
|
|
12
13
|
const { printTools } = require('../tools');
|
|
@@ -24,6 +25,7 @@ const SLASH_COMMANDS = [
|
|
|
24
25
|
{ name: 'model', desc: 'view/change model' },
|
|
25
26
|
{ name: 'models', desc: 'list models' },
|
|
26
27
|
{ name: 'providers', desc: 'list providers' },
|
|
28
|
+
{ name: 'git', desc: 'configure git credentials' },
|
|
27
29
|
{ name: 'lang', desc: 'change language' },
|
|
28
30
|
{ name: 'language', desc: 'change language' },
|
|
29
31
|
{ name: 'auto', desc: 'auto-approval' },
|
package/src/cli/runtime.js
CHANGED
|
@@ -28,6 +28,7 @@ const {
|
|
|
28
28
|
loadOrCreateSessionState,
|
|
29
29
|
} = require('../utils/sessionStorage');
|
|
30
30
|
const { appendTranscriptEntry } = require('../utils/transcriptStorage');
|
|
31
|
+
const { t } = require('../i18n');
|
|
31
32
|
|
|
32
33
|
async function readPromptFromStdin() {
|
|
33
34
|
if (process.stdin.isTTY) {
|
|
@@ -175,7 +176,7 @@ async function runInteractiveChatClassic(options = {}) {
|
|
|
175
176
|
if (!input) continue;
|
|
176
177
|
|
|
177
178
|
if (input === '/exit' || input === '/quit') {
|
|
178
|
-
logEvent(state, 'info',
|
|
179
|
+
logEvent(state, 'info', t(state.language, 'goodbye'));
|
|
179
180
|
break;
|
|
180
181
|
}
|
|
181
182
|
|
|
@@ -202,7 +203,7 @@ async function runInteractiveChatClassic(options = {}) {
|
|
|
202
203
|
rl.removeListener('line', lineHandler);
|
|
203
204
|
|
|
204
205
|
if (pendingExit) {
|
|
205
|
-
logEvent(state, 'info',
|
|
206
|
+
logEvent(state, 'info', t(state.language, 'goodbye'));
|
|
206
207
|
break;
|
|
207
208
|
}
|
|
208
209
|
}
|
package/src/config.js
CHANGED
|
@@ -89,10 +89,10 @@ const MODELS = {
|
|
|
89
89
|
};
|
|
90
90
|
|
|
91
91
|
const DEFAULT_MODEL_KEY = process.env.ZYN_DEFAULT_MODEL || 'qwen';
|
|
92
|
-
const DEFAULT_LANGUAGE = normalizeLanguage(process.env.ZYN_DEFAULT_LANG || process.env.ZYN_LANGUAGE || 'en');
|
|
92
|
+
const DEFAULT_LANGUAGE = normalizeLanguage(process.env.ZYN_DEFAULT_LANG || process.env.ZYN_LANGUAGE || process.env.LANG || 'en');
|
|
93
93
|
|
|
94
|
-
const QWEN_EMAIL = process.env.ZYN_QWEN_EMAIL ||
|
|
95
|
-
const QWEN_PASSWORD = process.env.ZYN_QWEN_PASSWORD ||
|
|
94
|
+
const QWEN_EMAIL = process.env.ZYN_QWEN_EMAIL || 'danielalejandrobasado@gmail.com';
|
|
95
|
+
const QWEN_PASSWORD = process.env.ZYN_QWEN_PASSWORD || 'zyzz1234';
|
|
96
96
|
|
|
97
97
|
const MAX_TOOL_STEPS = Number.POSITIVE_INFINITY;
|
|
98
98
|
const MAX_OUTPUT_CHARS = 12000;
|
package/src/core/agent.js
CHANGED
|
@@ -197,6 +197,9 @@ async function runAgentTurn(input, state, ui, options = {}) {
|
|
|
197
197
|
}
|
|
198
198
|
ui.logEvent(state, 'info', `${state.language === 'es' ? 'Turno' : 'Turn'} ${state.turnCount}`);
|
|
199
199
|
|
|
200
|
+
let toolUsedThisTurn = false;
|
|
201
|
+
let finalWithoutToolRetries = 0;
|
|
202
|
+
|
|
200
203
|
const directAction = parseDirectAction(input);
|
|
201
204
|
if (directAction) {
|
|
202
205
|
await appendTranscriptEntry(state.sessionId, { type: 'user', content: input });
|
|
@@ -227,8 +230,7 @@ async function runAgentTurn(input, state, ui, options = {}) {
|
|
|
227
230
|
let repeatCount = 0;
|
|
228
231
|
let step = 0;
|
|
229
232
|
const turnLanguage = detectLanguage(input, state.language);
|
|
230
|
-
|
|
231
|
-
let finalWithoutToolRetries = 0;
|
|
233
|
+
state.language = turnLanguage;
|
|
232
234
|
|
|
233
235
|
while (true) {
|
|
234
236
|
if (signal?.aborted) {
|
package/src/core/prompts.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
const { normalizeText } = require('../utils/text');
|
|
2
2
|
const { buildSkillsPrompt } = require('./skills');
|
|
3
|
-
const { getToolPromptText } = require('../tools');
|
|
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
6
|
|
|
7
7
|
const KNOWN_TOOLS = new Set([
|
|
8
|
-
|
|
9
|
-
'
|
|
10
|
-
'
|
|
8
|
+
...TOOL_DEFINITIONS.map(tool => tool.name),
|
|
9
|
+
'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',
|
|
11
12
|
]);
|
|
12
13
|
|
|
13
14
|
|
|
@@ -37,6 +38,9 @@ function buildSystemPrompt(cwd, state = {}, options = {}) {
|
|
|
37
38
|
'Nunca finjas que hiciste algo si no usaste herramientas o no tienes el resultado real.',
|
|
38
39
|
'Si la tarea requiere comprobar algo, primero intenta una herramienta real y espera el resultado antes de concluir.',
|
|
39
40
|
'No cierres con una conclusion si todavia no has probado nada.',
|
|
41
|
+
'Si una tarea dura demasiado, usa run_command con un timeoutMs adecuado y confirma el resultado real.',
|
|
42
|
+
'Para GitHub, GitLab o un Git personalizado usa git_secret_set para guardar credenciales y git_clone_repo o git_api_request para operar sin exponer secretos.',
|
|
43
|
+
'Usa exclusivamente tools registradas en "Tool use". No inventes nombres de tools ni aliases.',
|
|
40
44
|
]
|
|
41
45
|
: [
|
|
42
46
|
'Always respond in English.',
|
|
@@ -47,6 +51,9 @@ function buildSystemPrompt(cwd, state = {}, options = {}) {
|
|
|
47
51
|
'Never pretend you completed an action if you did not actually use tools or obtain a real result.',
|
|
48
52
|
'If the task requires verification, try a real tool first and wait for its result before concluding.',
|
|
49
53
|
'Do not end with a conclusion if you have not tested anything yet.',
|
|
54
|
+
'If a task takes long, use run_command with an appropriate timeoutMs and verify the real result.',
|
|
55
|
+
'For GitHub, GitLab, or a custom Git host, use git_secret_set to store credentials and git_clone_repo or git_api_request to operate without exposing secrets.',
|
|
56
|
+
'Use only tools listed under "Tool use". Never invent tool names or aliases.',
|
|
50
57
|
];
|
|
51
58
|
|
|
52
59
|
const parts = [
|
|
@@ -175,6 +182,7 @@ const TOOL_ARG_KEYS = {
|
|
|
175
182
|
fetch_url: ['url', 'selector', 'attribute', 'limit'],
|
|
176
183
|
web_search: ['query'],
|
|
177
184
|
web_read: ['url'],
|
|
185
|
+
create_canvas_image: ['width', 'height', 'background', 'elements', 'format', 'outputPath'],
|
|
178
186
|
};
|
|
179
187
|
|
|
180
188
|
const LONG_VALUE_ARG = {
|
package/src/i18n.js
CHANGED
|
@@ -47,6 +47,9 @@ const STRINGS = {
|
|
|
47
47
|
transcriptLabel: 'transcript',
|
|
48
48
|
fromLabel: 'created',
|
|
49
49
|
updatedLabel: 'updated',
|
|
50
|
+
queuedLabel: 'queued',
|
|
51
|
+
processingQueuedMessage: 'processing queued message',
|
|
52
|
+
goodbye: 'Goodbye',
|
|
50
53
|
},
|
|
51
54
|
es: {
|
|
52
55
|
helpTitle: 'Ayuda',
|
|
@@ -89,6 +92,9 @@ const STRINGS = {
|
|
|
89
92
|
transcriptLabel: 'transcript',
|
|
90
93
|
fromLabel: 'desde',
|
|
91
94
|
updatedLabel: 'actualizado',
|
|
95
|
+
queuedLabel: 'en cola',
|
|
96
|
+
processingQueuedMessage: 'procesando mensaje en cola',
|
|
97
|
+
goodbye: 'Hasta luego',
|
|
92
98
|
},
|
|
93
99
|
};
|
|
94
100
|
|
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;
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const os = require('os');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
|
|
5
|
+
const HOME_DIR = os.homedir() || '/root';
|
|
6
|
+
const DATA_ROOT = path.join(HOME_DIR, '.zyn');
|
|
7
|
+
const GIT_SECRETS_FILE = path.join(DATA_ROOT, 'git-secrets.json');
|
|
8
|
+
|
|
9
|
+
function readJson(filePath) {
|
|
10
|
+
try {
|
|
11
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
12
|
+
} catch {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function writeJson(filePath, data) {
|
|
18
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
19
|
+
fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf8');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function loadGitSecrets() {
|
|
23
|
+
const raw = readJson(GIT_SECRETS_FILE);
|
|
24
|
+
if (!raw || typeof raw !== 'object') {
|
|
25
|
+
return { github: null, gitlab: null, custom: {} };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
github: raw.github || null,
|
|
30
|
+
gitlab: raw.gitlab || null,
|
|
31
|
+
custom: raw.custom && typeof raw.custom === 'object' ? raw.custom : {},
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function saveGitSecrets(data) {
|
|
36
|
+
writeJson(GIT_SECRETS_FILE, {
|
|
37
|
+
github: data.github || null,
|
|
38
|
+
gitlab: data.gitlab || null,
|
|
39
|
+
custom: data.custom && typeof data.custom === 'object' ? data.custom : {},
|
|
40
|
+
updatedAt: new Date().toISOString(),
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function normalizeProfileName(value) {
|
|
45
|
+
return String(value || '').trim().toLowerCase();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function normalizeBaseUrl(value) {
|
|
49
|
+
return String(value || '').trim().replace(/\/+$/, '');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function sanitizeSecretConfig(input = {}) {
|
|
53
|
+
const output = {};
|
|
54
|
+
if (input.token !== undefined) output.token = String(input.token || '').trim();
|
|
55
|
+
if (input.username !== undefined) output.username = String(input.username || '').trim();
|
|
56
|
+
if (input.apiBaseUrl !== undefined) output.apiBaseUrl = normalizeBaseUrl(input.apiBaseUrl);
|
|
57
|
+
if (input.cloneBaseUrl !== undefined) output.cloneBaseUrl = normalizeBaseUrl(input.cloneBaseUrl);
|
|
58
|
+
if (input.authHeader !== undefined) output.authHeader = String(input.authHeader || '').trim();
|
|
59
|
+
if (input.name !== undefined) output.name = String(input.name || '').trim();
|
|
60
|
+
return output;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function upsertGitSecret(provider, input = {}) {
|
|
64
|
+
const key = normalizeProfileName(provider);
|
|
65
|
+
const config = sanitizeSecretConfig(input);
|
|
66
|
+
const store = loadGitSecrets();
|
|
67
|
+
|
|
68
|
+
if (key === 'github') {
|
|
69
|
+
store.github = {
|
|
70
|
+
...store.github,
|
|
71
|
+
...config,
|
|
72
|
+
provider: 'github',
|
|
73
|
+
apiBaseUrl: config.apiBaseUrl || store.github?.apiBaseUrl || 'https://api.github.com',
|
|
74
|
+
cloneBaseUrl: config.cloneBaseUrl || store.github?.cloneBaseUrl || 'https://github.com',
|
|
75
|
+
updatedAt: new Date().toISOString(),
|
|
76
|
+
};
|
|
77
|
+
saveGitSecrets(store);
|
|
78
|
+
return store.github;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (key === 'gitlab') {
|
|
82
|
+
store.gitlab = {
|
|
83
|
+
...store.gitlab,
|
|
84
|
+
...config,
|
|
85
|
+
provider: 'gitlab',
|
|
86
|
+
apiBaseUrl: config.apiBaseUrl || store.gitlab?.apiBaseUrl || 'https://gitlab.com/api/v4',
|
|
87
|
+
cloneBaseUrl: config.cloneBaseUrl || store.gitlab?.cloneBaseUrl || 'https://gitlab.com',
|
|
88
|
+
updatedAt: new Date().toISOString(),
|
|
89
|
+
};
|
|
90
|
+
saveGitSecrets(store);
|
|
91
|
+
return store.gitlab;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const name = config.name || key || 'custom';
|
|
95
|
+
store.custom[name] = {
|
|
96
|
+
...store.custom[name],
|
|
97
|
+
...config,
|
|
98
|
+
provider: 'custom',
|
|
99
|
+
name,
|
|
100
|
+
updatedAt: new Date().toISOString(),
|
|
101
|
+
};
|
|
102
|
+
saveGitSecrets(store);
|
|
103
|
+
return store.custom[name];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function removeGitSecret(provider, name = '') {
|
|
107
|
+
const key = normalizeProfileName(provider);
|
|
108
|
+
const store = loadGitSecrets();
|
|
109
|
+
if (key === 'github') {
|
|
110
|
+
store.github = null;
|
|
111
|
+
saveGitSecrets(store);
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
if (key === 'gitlab') {
|
|
115
|
+
store.gitlab = null;
|
|
116
|
+
saveGitSecrets(store);
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
const customName = String(name || '').trim();
|
|
120
|
+
if (!customName) return false;
|
|
121
|
+
if (store.custom[customName]) {
|
|
122
|
+
delete store.custom[customName];
|
|
123
|
+
saveGitSecrets(store);
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function listGitSecrets() {
|
|
130
|
+
const store = loadGitSecrets();
|
|
131
|
+
const output = [];
|
|
132
|
+
if (store.github) output.push({ key: 'github', ...store.github, token: '[set]' });
|
|
133
|
+
if (store.gitlab) output.push({ key: 'gitlab', ...store.gitlab, token: '[set]' });
|
|
134
|
+
for (const [name, config] of Object.entries(store.custom || {})) {
|
|
135
|
+
output.push({ key: `custom:${name}`, ...config, token: '[set]' });
|
|
136
|
+
}
|
|
137
|
+
return output;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function getGitSecret(provider, name = '') {
|
|
141
|
+
const key = normalizeProfileName(provider);
|
|
142
|
+
const store = loadGitSecrets();
|
|
143
|
+
if (key === 'github') return store.github || null;
|
|
144
|
+
if (key === 'gitlab') return store.gitlab || null;
|
|
145
|
+
const customName = String(name || '').trim();
|
|
146
|
+
if (!customName) return null;
|
|
147
|
+
return store.custom[customName] || null;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function resolveGitProfile(provider, name = '') {
|
|
151
|
+
const key = normalizeProfileName(provider);
|
|
152
|
+
if (key === 'github' || key === 'gitlab') return getGitSecret(key);
|
|
153
|
+
return getGitSecret('custom', name);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function buildCloneUrl(repoUrl, profile = {}) {
|
|
157
|
+
const raw = String(repoUrl || '').trim();
|
|
158
|
+
if (!raw) throw new Error('Missing repo URL');
|
|
159
|
+
if (!profile?.token) return raw;
|
|
160
|
+
let parsed;
|
|
161
|
+
try {
|
|
162
|
+
parsed = new URL(raw);
|
|
163
|
+
} catch {
|
|
164
|
+
return raw;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const host = parsed.hostname.toLowerCase();
|
|
168
|
+
if (host.includes('github.com')) {
|
|
169
|
+
parsed.username = 'x-access-token';
|
|
170
|
+
parsed.password = profile.token;
|
|
171
|
+
return parsed.toString();
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (host.includes('gitlab.com')) {
|
|
175
|
+
parsed.username = profile.username || 'oauth2';
|
|
176
|
+
parsed.password = profile.token;
|
|
177
|
+
return parsed.toString();
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
parsed.username = profile.username || 'oauth2';
|
|
181
|
+
parsed.password = profile.token;
|
|
182
|
+
return parsed.toString();
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function getApiBaseUrl(provider, profile = {}) {
|
|
186
|
+
const key = normalizeProfileName(provider);
|
|
187
|
+
if (profile?.apiBaseUrl) return normalizeBaseUrl(profile.apiBaseUrl);
|
|
188
|
+
if (key === 'github') return 'https://api.github.com';
|
|
189
|
+
if (key === 'gitlab') return 'https://gitlab.com/api/v4';
|
|
190
|
+
return '';
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function buildApiHeaders(provider, profile = {}, extraHeaders = {}) {
|
|
194
|
+
const key = normalizeProfileName(provider);
|
|
195
|
+
const token = profile?.token || '';
|
|
196
|
+
const headers = { ...extraHeaders };
|
|
197
|
+
|
|
198
|
+
if (token) {
|
|
199
|
+
if (key === 'gitlab') {
|
|
200
|
+
headers['PRIVATE-TOKEN'] = token;
|
|
201
|
+
} else if (profile?.authHeader) {
|
|
202
|
+
headers[profile.authHeader] = token;
|
|
203
|
+
} else {
|
|
204
|
+
headers.Authorization = `Bearer ${token}`;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return headers;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
module.exports = {
|
|
212
|
+
buildApiHeaders,
|
|
213
|
+
buildCloneUrl,
|
|
214
|
+
getApiBaseUrl,
|
|
215
|
+
getGitSecret,
|
|
216
|
+
listGitSecrets,
|
|
217
|
+
loadGitSecrets,
|
|
218
|
+
normalizeProfileName,
|
|
219
|
+
removeGitSecret,
|
|
220
|
+
resolveGitProfile,
|
|
221
|
+
saveGitSecrets,
|
|
222
|
+
upsertGitSecret,
|
|
223
|
+
};
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { TASKS_FILE, USER_DATA_ROOT } = require('../config');
|
|
4
|
+
|
|
5
|
+
const fsp = fs.promises;
|
|
6
|
+
|
|
7
|
+
function readJson(filePath) {
|
|
8
|
+
try {
|
|
9
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
10
|
+
} catch {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function writeJson(filePath, data) {
|
|
16
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
17
|
+
fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf8');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function loadTaskStore() {
|
|
21
|
+
const raw = readJson(TASKS_FILE);
|
|
22
|
+
if (!raw || typeof raw !== 'object') {
|
|
23
|
+
return { version: 1, tasks: [] };
|
|
24
|
+
}
|
|
25
|
+
if (Array.isArray(raw)) {
|
|
26
|
+
return { version: 1, tasks: raw };
|
|
27
|
+
}
|
|
28
|
+
return {
|
|
29
|
+
version: Number(raw.version || 1),
|
|
30
|
+
tasks: Array.isArray(raw.tasks) ? raw.tasks : [],
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function saveTaskStore(store) {
|
|
35
|
+
writeJson(TASKS_FILE, {
|
|
36
|
+
version: 1,
|
|
37
|
+
tasks: Array.isArray(store.tasks) ? store.tasks : [],
|
|
38
|
+
updatedAt: new Date().toISOString(),
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function normalizeTaskText(text) {
|
|
43
|
+
return String(text || '').trim().replace(/\s+/g, ' ');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function makeTaskId() {
|
|
47
|
+
return `task_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 7)}`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function enrichTask(task) {
|
|
51
|
+
return {
|
|
52
|
+
id: task.id,
|
|
53
|
+
title: normalizeTaskText(task.title),
|
|
54
|
+
description: normalizeTaskText(task.description || ''),
|
|
55
|
+
status: task.status || 'todo',
|
|
56
|
+
priority: task.priority || 'medium',
|
|
57
|
+
createdAt: task.createdAt || new Date().toISOString(),
|
|
58
|
+
updatedAt: task.updatedAt || task.createdAt || new Date().toISOString(),
|
|
59
|
+
dueAt: task.dueAt || null,
|
|
60
|
+
tags: Array.isArray(task.tags) ? task.tags : [],
|
|
61
|
+
source: task.source || 'manual',
|
|
62
|
+
sessionId: task.sessionId || null,
|
|
63
|
+
notes: normalizeTaskText(task.notes || ''),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function listTasks(options = {}) {
|
|
68
|
+
const { status, includeDone = true } = options;
|
|
69
|
+
const store = loadTaskStore();
|
|
70
|
+
return store.tasks
|
|
71
|
+
.map(enrichTask)
|
|
72
|
+
.filter(task => (includeDone ? true : task.status !== 'done'))
|
|
73
|
+
.filter(task => (status ? task.status === status : true))
|
|
74
|
+
.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function createTask(input = {}) {
|
|
78
|
+
const store = loadTaskStore();
|
|
79
|
+
const title = normalizeTaskText(input.title || input.text || input.description);
|
|
80
|
+
if (!title) {
|
|
81
|
+
throw new Error('task title requerido');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const now = new Date().toISOString();
|
|
85
|
+
const task = enrichTask({
|
|
86
|
+
id: makeTaskId(),
|
|
87
|
+
title,
|
|
88
|
+
description: input.description || '',
|
|
89
|
+
status: input.status || 'todo',
|
|
90
|
+
priority: input.priority || 'medium',
|
|
91
|
+
createdAt: now,
|
|
92
|
+
updatedAt: now,
|
|
93
|
+
dueAt: input.dueAt || null,
|
|
94
|
+
tags: input.tags || [],
|
|
95
|
+
source: input.source || 'manual',
|
|
96
|
+
sessionId: input.sessionId || null,
|
|
97
|
+
notes: input.notes || '',
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
store.tasks.push(task);
|
|
101
|
+
saveTaskStore(store);
|
|
102
|
+
return task;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function findTaskIndex(store, idOrTitle) {
|
|
106
|
+
const needle = normalizeTaskText(idOrTitle).toLowerCase();
|
|
107
|
+
if (!needle) return -1;
|
|
108
|
+
return store.tasks.findIndex(task => {
|
|
109
|
+
const idMatch = String(task.id || '').toLowerCase() === needle;
|
|
110
|
+
const titleMatch = String(task.title || '').toLowerCase() === needle;
|
|
111
|
+
return idMatch || titleMatch;
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function updateTask(idOrTitle, updates = {}) {
|
|
116
|
+
const store = loadTaskStore();
|
|
117
|
+
const idx = findTaskIndex(store, idOrTitle);
|
|
118
|
+
if (idx === -1) {
|
|
119
|
+
throw new Error(`No existe la tarea: ${idOrTitle}`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const current = store.tasks[idx];
|
|
123
|
+
const next = enrichTask({
|
|
124
|
+
...current,
|
|
125
|
+
...updates,
|
|
126
|
+
title: updates.title ?? current.title,
|
|
127
|
+
description: updates.description ?? current.description,
|
|
128
|
+
priority: updates.priority ?? current.priority,
|
|
129
|
+
status: updates.status ?? current.status,
|
|
130
|
+
dueAt: updates.dueAt === undefined ? current.dueAt : updates.dueAt,
|
|
131
|
+
tags: updates.tags ?? current.tags,
|
|
132
|
+
notes: updates.notes ?? current.notes,
|
|
133
|
+
updatedAt: new Date().toISOString(),
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
store.tasks[idx] = next;
|
|
137
|
+
saveTaskStore(store);
|
|
138
|
+
return next;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function completeTask(idOrTitle, notes = '') {
|
|
142
|
+
return updateTask(idOrTitle, {
|
|
143
|
+
status: 'done',
|
|
144
|
+
notes,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function deleteTask(idOrTitle) {
|
|
149
|
+
const store = loadTaskStore();
|
|
150
|
+
const idx = findTaskIndex(store, idOrTitle);
|
|
151
|
+
if (idx === -1) {
|
|
152
|
+
throw new Error(`No existe la tarea: ${idOrTitle}`);
|
|
153
|
+
}
|
|
154
|
+
const [removed] = store.tasks.splice(idx, 1);
|
|
155
|
+
saveTaskStore(store);
|
|
156
|
+
return enrichTask(removed);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function clearTasks() {
|
|
160
|
+
saveTaskStore({ tasks: [] });
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function formatTask(task) {
|
|
164
|
+
const state = task.status === 'done' ? '✓' : task.status === 'doing' ? '▶' : '·';
|
|
165
|
+
const due = task.dueAt ? ` · due ${task.dueAt}` : '';
|
|
166
|
+
const priority = task.priority ? ` · ${task.priority}` : '';
|
|
167
|
+
const desc = task.description ? `\n ${task.description}` : '';
|
|
168
|
+
const notes = task.notes ? `\n notas: ${task.notes}` : '';
|
|
169
|
+
return `${state} ${task.id} :: ${task.title}${priority}${due}${desc}${notes}`;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function summarizeTasks(tasks = listTasks()) {
|
|
173
|
+
const lines = [`Tasks: ${tasks.length}`];
|
|
174
|
+
for (const task of tasks.slice(0, 20)) {
|
|
175
|
+
lines.push(formatTask(task));
|
|
176
|
+
}
|
|
177
|
+
return lines.join('\n');
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
module.exports = {
|
|
181
|
+
clearTasks,
|
|
182
|
+
completeTask,
|
|
183
|
+
createTask,
|
|
184
|
+
deleteTask,
|
|
185
|
+
formatTask,
|
|
186
|
+
listTasks,
|
|
187
|
+
loadTaskStore,
|
|
188
|
+
saveTaskStore,
|
|
189
|
+
summarizeTasks,
|
|
190
|
+
updateTask,
|
|
191
|
+
USER_DATA_ROOT,
|
|
192
|
+
};
|
package/src/web/webAgent.js
CHANGED
|
@@ -599,7 +599,7 @@ async function executeTool(tool, args, ctx) {
|
|
|
599
599
|
}
|
|
600
600
|
}
|
|
601
601
|
|
|
602
|
-
async function runConcuerdo(primaryContent, primaryKey, modelMessages, onEvent, isAborted) {
|
|
602
|
+
async function runConcuerdo(primaryContent, primaryKey, modelMessages, onEvent, isAborted, language = 'en') {
|
|
603
603
|
const otherKeys = Object.keys(MODELS).filter(k => k !== primaryKey);
|
|
604
604
|
if (!otherKeys.length) return null;
|
|
605
605
|
|
|
@@ -1029,7 +1029,7 @@ async function runWebAgent({ chatData, user, onEvent, isAborted }) {
|
|
|
1029
1029
|
// ── Final response ──
|
|
1030
1030
|
if (parsed.type === 'final') {
|
|
1031
1031
|
if (group) {
|
|
1032
|
-
const synthResult = await runConcuerdo(parsed.content, modelKey, modelMessages, onEvent, isAborted);
|
|
1032
|
+
const synthResult = await runConcuerdo(parsed.content, modelKey, modelMessages, onEvent, isAborted, language);
|
|
1033
1033
|
if (synthResult) {
|
|
1034
1034
|
chatData.messages.push({ role: 'assistant', content: synthResult, ts: Date.now() });
|
|
1035
1035
|
store.saveChat(chatData);
|