zyn-ai 1.3.4 → 1.3.6

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 CHANGED
@@ -31,7 +31,6 @@ Zyn is a local AI agent designed for terminal and web usage. It supports persist
31
31
  - Node.js 18+
32
32
  - npm
33
33
  - Internet connection for remote providers
34
- - Optional: Ollama for local models
35
34
 
36
35
  ---
37
36
 
@@ -133,6 +132,9 @@ Commands:
133
132
  |---|---|
134
133
  | `/tools` | List tools |
135
134
  | `/skills` | List skills |
135
+ | `/gmail connect` | Connect Gmail with Google OAuth + PKCE |
136
+ | `/gmail status` | Show Gmail connection status |
137
+ | `/gmail disconnect` | Remove saved Gmail tokens |
136
138
  | `/cwd` | Show working directory |
137
139
 
138
140
  ### Web & Export
@@ -164,16 +166,10 @@ Example:
164
166
  ```json
165
167
  {
166
168
  "models": {
167
- "my-local-model": {
168
- "label": "My local model",
169
- "provider": "ollama",
170
- "ollamaModel": "llama3.1:8b"
171
- },
172
- "my-remote-model": {
173
- "label": "My remote model",
174
- "provider": "openai-compatible",
175
- "openaiModel": "gpt-4o-mini",
176
- "baseUrl": "https://api.example.com/v1"
169
+ "my-gemini-flash": {
170
+ "label": "Gemini Flash",
171
+ "provider": "gemini",
172
+ "geminiModel": "gemini-flash"
177
173
  }
178
174
  }
179
175
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zyn-ai",
3
- "version": "1.3.4",
3
+ "version": "1.3.6",
4
4
  "description": "Production-ready AI agent for CLI and web with real tool execution and automation",
5
5
  "author": "Maycol",
6
6
  "keywords": [
@@ -4,14 +4,35 @@ const { spawn } = require('child_process');
4
4
 
5
5
  const fsp = fs.promises;
6
6
  const { listSkills, SKILLS_DIR } = require('../core/skills');
7
- const { DEFAULT_LANGUAGE, DEFAULT_MODEL_KEY, MODELS, listProvidersFromModels } = require('../config');
7
+ const { DEFAULT_LANGUAGE, DEFAULT_MODEL_KEY, GEMINI_MODEL_WARNING, MODELS, listProvidersFromModels } = require('../config');
8
8
  const { languageLabel, normalizeLanguage, t } = require('../i18n');
9
9
  const { createNewSessionState, listSessions, loadSessionState, saveState } = require('../utils/sessionStorage');
10
10
  const { listGitSecrets, removeGitSecret, upsertGitSecret } = require('../utils/secretStorage');
11
+ const { clearGmailAuth, getGmailAuthStatus, startGmailOAuthFlow } = require('../utils/gmailAuth');
11
12
  const { exportTranscriptText, formatTranscriptPreview } = require('../utils/transcriptStorage');
12
13
  const { resolveInputPath } = require('../utils/pathUtils');
13
14
  const { printTools } = require('../tools');
14
15
 
16
+
17
+ function getModelWarning(key) {
18
+ const model = MODELS[key];
19
+ return model?.provider === 'gemini' ? GEMINI_MODEL_WARNING : '';
20
+ }
21
+
22
+ function printModelChanged(key) {
23
+ const warning = getModelWarning(key);
24
+ console.log(`Model: ${MODELS[key].label}`);
25
+ if (warning) console.log(`Warning: ${warning}`);
26
+ }
27
+
28
+ function printLanguageChanged(language) {
29
+ const normalized = normalizeLanguage(language);
30
+ const label = languageLabel(normalized);
31
+ console.log(normalized === 'es'
32
+ ? `Actualizado al idioma ${label} (${normalized})`
33
+ : `Updated to language ${label} (${normalized})`);
34
+ }
35
+
15
36
  const SLASH_COMMANDS = [
16
37
  { name: 'help', desc: 'full help', descEs: 'ayuda completa' },
17
38
  { name: 'status', desc: 'current status', descEs: 'estado actual' },
@@ -28,6 +49,7 @@ const SLASH_COMMANDS = [
28
49
  { name: 'models', desc: 'list models', descEs: 'listar modelos' },
29
50
  { name: 'providers', desc: 'list providers', descEs: 'listar proveedores' },
30
51
  { name: 'git', desc: 'configure git credentials', descEs: 'configurar credenciales git' },
52
+ { name: 'gmail', desc: 'connect Gmail account', descEs: 'conectar cuenta Gmail' },
31
53
  { name: 'persona', desc: 'set response tone/personality', descEs: 'definir tono/persona' },
32
54
  { name: 'lang', desc: 'change language', descEs: 'cambiar idioma' },
33
55
  { name: 'language', desc: 'change language', descEs: 'cambiar idioma' },
@@ -121,6 +143,9 @@ function printHelp(state = {}) {
121
143
  console.log(` ${b('/git set <provider> <token> [user] [apiBaseUrl:URL] [cloneBaseUrl:URL] [name:X]')}`);
122
144
  console.log(` ${b('/git list')} List configured git profiles`);
123
145
  console.log(` ${b('/git remove <provider> [name]')} Remove git credentials`);
146
+ console.log(` ${b('/gmail connect')} Connect Gmail with Google OAuth + PKCE`);
147
+ console.log(` ${b('/gmail status')} Show Gmail connection status`);
148
+ console.log(` ${b('/gmail disconnect')} Remove saved Gmail tokens`);
124
149
  console.log(` ${b('/cwd')} Show current working directory`);
125
150
  console.log(` ${b('/cwd <path>')} Change working directory`);
126
151
  console.log('');
@@ -261,7 +286,7 @@ async function handleLocalCommand(input, state, deps) {
261
286
 
262
287
  state.language = nextLanguage;
263
288
  await saveState(state);
264
- console.log(`${t(state.language, 'langChanged')}: ${languageLabel(nextLanguage)} (${nextLanguage})`);
289
+ printLanguageChanged(nextLanguage);
265
290
  return true;
266
291
  }
267
292
 
@@ -401,7 +426,7 @@ async function handleLocalCommand(input, state, deps) {
401
426
  }
402
427
  state.language = nextLanguage;
403
428
  await saveState(state);
404
- console.log(`${t(state.language, 'langChanged')}: ${languageLabel(nextLanguage)} (${nextLanguage})`);
429
+ printLanguageChanged(nextLanguage);
405
430
  return true;
406
431
  }
407
432
 
@@ -416,9 +441,9 @@ async function handleLocalCommand(input, state, deps) {
416
441
  await saveState(state);
417
442
  await appendTranscriptEntry(state.sessionId, {
418
443
  type: 'system',
419
- content: `Model switched to: ${MODELS[key].label}`,
444
+ content: `Model switched to: ${MODELS[key].label}${getModelWarning(key) ? `\nWarning: ${getModelWarning(key)}` : ''}`,
420
445
  });
421
- console.log(`Model: ${MODELS[key].label}`);
446
+ printModelChanged(key);
422
447
  return true;
423
448
  }
424
449
 
@@ -479,9 +504,9 @@ async function handleLocalCommand(input, state, deps) {
479
504
  await saveState(state);
480
505
  await appendTranscriptEntry(state.sessionId, {
481
506
  type: 'system',
482
- content: `Model switched to: ${MODELS[key].label}`,
507
+ content: `Model switched to: ${MODELS[key].label}${getModelWarning(key) ? `\nWarning: ${getModelWarning(key)}` : ''}`,
483
508
  });
484
- console.log(`Model: ${MODELS[key].label}`);
509
+ printModelChanged(key);
485
510
  return true;
486
511
  }
487
512
 
@@ -541,6 +566,51 @@ async function handleLocalCommand(input, state, deps) {
541
566
  return true;
542
567
  }
543
568
 
569
+
570
+ if (commandName === 'gmail') {
571
+ const [subRaw, ...rest] = String(args || 'status').trim().split(/\s+/).filter(Boolean);
572
+ const sub = (subRaw || 'status').toLowerCase();
573
+
574
+ if (sub === 'connect' || sub === 'login') {
575
+ const portArg = rest.find(part => /^\d{2,5}$/.test(part));
576
+ const flow = await startGmailOAuthFlow({ port: portArg ? Number(portArg) : 0, flow: 'code' });
577
+ console.log(flow.authUrl);
578
+ if (flow.flow === 'device') {
579
+ console.log(`Código: ${flow.userCode}`);
580
+ console.log('Abre el link, ingresa el código y autoriza Gmail.');
581
+ } else {
582
+ console.log('Abre el link, inicia sesión y vuelve aquí.');
583
+ }
584
+ flow.done
585
+ .then(auth => {
586
+ const email = auth?.profile?.email || 'cuenta conectada';
587
+ console.error(`Gmail conectado: ${email}`);
588
+ })
589
+ .catch(err => console.error(`Gmail OAuth fallo: ${err.message}`));
590
+ return true;
591
+ }
592
+
593
+ if (sub === 'status') {
594
+ const status = await getGmailAuthStatus();
595
+ if (!status.connected) {
596
+ console.log('Gmail: no conectado. Usa /gmail connect.');
597
+ } else {
598
+ console.log(`Gmail: conectado${status.email ? ` (${status.email})` : ''}`);
599
+ console.log(`Scopes: ${status.scopes.join(', ') || '-'}`);
600
+ console.log(`Expira: ${status.expiryDate ? new Date(status.expiryDate).toISOString() : '-'}`);
601
+ }
602
+ return true;
603
+ }
604
+
605
+ if (sub === 'disconnect' || sub === 'logout' || sub === 'remove') {
606
+ await clearGmailAuth();
607
+ console.log('Gmail desconectado.');
608
+ return true;
609
+ }
610
+
611
+ throw new Error('Use /gmail connect|status|disconnect');
612
+ }
613
+
544
614
  if (commandName === 'web') {
545
615
  let host = '127.0.0.1';
546
616
  let port = 3000;
package/src/cli/print.js CHANGED
@@ -112,11 +112,11 @@ function pushAction(state, kind, title, detail = '') {
112
112
  }
113
113
 
114
114
  const EVENT_SYMBOLS = {
115
- info: { sym: '·', color: C.gray },
115
+ info: { sym: '', color: C.gray },
116
116
  think: { sym: '○', color: C.gray },
117
- tool: { sym: '', color: C.purple },
117
+ tool: { sym: '', color: C.purple },
118
118
  ok: { sym: '✓', color: C.green },
119
- warn: { sym: '', color: C.yellow },
119
+ warn: { sym: '', color: C.yellow },
120
120
  error: { sym: '✗', color: C.red },
121
121
  };
122
122
 
package/src/config.js CHANGED
@@ -34,23 +34,16 @@ const BUILTIN_MODELS = {
34
34
  provider: 'zen',
35
35
  zenModel: 'trinity-large-preview-free',
36
36
  },
37
- 'ollama-qwen': {
38
- label: 'Ollama Qwen 2.5',
39
- provider: 'ollama',
40
- ollamaModel: 'qwen2.5:latest',
41
- },
42
- 'ollama-gemma': {
43
- label: 'Ollama Gemma 3',
44
- provider: 'ollama',
45
- ollamaModel: 'gemma3:latest',
46
- },
47
- 'openai-mini': {
48
- label: 'OpenAI Compatible Mini',
49
- provider: 'openai-compatible',
50
- openaiModel: 'gpt-4o-mini',
37
+ 'gemini-flash': {
38
+ label: 'Gemini Flash',
39
+ provider: 'gemini',
40
+ geminiModel: 'gemini-flash',
51
41
  },
52
42
  };
53
43
 
44
+ const SUPPORTED_MODEL_PROVIDERS = new Set(['qwen', 'zen', 'gemini']);
45
+ const GEMINI_MODEL_WARNING = 'It is not recommended for use in production; it is unstable and ineffective.';
46
+
54
47
  function readJsonFile(filePath) {
55
48
  try {
56
49
  return JSON.parse(fs.readFileSync(filePath, 'utf8'));
@@ -66,7 +59,7 @@ function loadExternalModels() {
66
59
  if (Array.isArray(raw)) {
67
60
  const output = {};
68
61
  for (const item of raw) {
69
- if (!item?.key || !item?.provider) continue;
62
+ if (!item?.key || !item?.provider || !SUPPORTED_MODEL_PROVIDERS.has(item.provider)) continue;
70
63
  output[item.key] = {
71
64
  label: item.label || item.key,
72
65
  provider: item.provider,
@@ -76,11 +69,13 @@ function loadExternalModels() {
76
69
  return output;
77
70
  }
78
71
 
79
- if (raw && typeof raw === 'object' && raw.models && typeof raw.models === 'object') {
80
- return raw.models;
81
- }
72
+ const rawModels = raw && typeof raw === 'object' && raw.models && typeof raw.models === 'object'
73
+ ? raw.models
74
+ : (raw && typeof raw === 'object' ? raw : {});
82
75
 
83
- return raw && typeof raw === 'object' ? raw : {};
76
+ return Object.fromEntries(
77
+ Object.entries(rawModels).filter(([, model]) => SUPPORTED_MODEL_PROVIDERS.has(model?.provider)),
78
+ );
84
79
  }
85
80
 
86
81
  const MODELS = {
@@ -99,8 +94,10 @@ const MAX_OUTPUT_CHARS = 12000;
99
94
  const MAX_FILE_LINES = 5000;
100
95
  const ACTION_LOG_LIMIT = 40;
101
96
  const REQUEST_TIMEOUT_MS = Number(process.env.ZYN_REQUEST_TIMEOUT_MS || 180000);
102
- const MAX_HISTORY_CHARS = 24000;
103
- const KEEP_RECENT_MESSAGES = 12;
97
+ const MAX_HISTORY_CHARS = 60000;
98
+ const KEEP_RECENT_MESSAGES = 50;
99
+ const PROVIDER_TIMEOUT_RETRY_DELAY_MS = Number(process.env.ZYN_PROVIDER_TIMEOUT_RETRY_DELAY_MS || 600000);
100
+ const PROVIDER_TIMEOUT_MAX_ATTEMPTS = 3;
104
101
  const SESSION_ROOT = path.join(DATA_ROOT, 'chat');
105
102
  const SESSIONS_DIR = path.join(SESSION_ROOT, 'sessions');
106
103
  const CURRENT_SESSION_FILE = path.join(SESSION_ROOT, 'current-session.json');
@@ -110,6 +107,9 @@ const EXPORTS_DIR = path.join(SESSION_ROOT, 'exports');
110
107
  const THINK_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
111
108
  const USER_DATA_ROOT = path.join(os.homedir(), '.zyn');
112
109
  const TASKS_FILE = path.join(USER_DATA_ROOT, 'tasks.json');
110
+ const GMAIL_CLIENT_ID = '871944347395-rnpsjsqgbnvlfb05hqk4dc9283olgnh2.apps.googleusercontent.com';
111
+ const GMAIL_CLIENT_SECRET = process.env.ZYN_GMAIL_CLIENT_SECRET || '';
112
+ const GMAIL_AUTH_FILE = path.join(USER_DATA_ROOT, 'gmail-auth.json');
113
113
  const PROVIDERS_FILE = path.join(DATA_ROOT, 'providers.json');
114
114
 
115
115
  function listProvidersFromModels(models = MODELS) {
@@ -142,6 +142,10 @@ module.exports = {
142
142
  DATA_ROOT,
143
143
  DEFAULT_LANGUAGE,
144
144
  DEFAULT_MODEL_KEY,
145
+ GEMINI_MODEL_WARNING,
146
+ GMAIL_AUTH_FILE,
147
+ GMAIL_CLIENT_ID,
148
+ GMAIL_CLIENT_SECRET,
145
149
  EXPORTS_DIR,
146
150
  HOME_DIR,
147
151
  KEEP_RECENT_MESSAGES,
@@ -152,6 +156,9 @@ module.exports = {
152
156
  MODELS,
153
157
  MODELS_FILE,
154
158
  PROVIDERS_FILE,
159
+ SUPPORTED_MODEL_PROVIDERS,
160
+ PROVIDER_TIMEOUT_MAX_ATTEMPTS,
161
+ PROVIDER_TIMEOUT_RETRY_DELAY_MS,
155
162
  QWEN_EMAIL,
156
163
  QWEN_PASSWORD,
157
164
  REQUEST_TIMEOUT_MS,
package/src/core/agent.js CHANGED
@@ -4,6 +4,8 @@ const {
4
4
  MAX_HISTORY_CHARS,
5
5
  MAX_TOOL_STEPS,
6
6
  MODELS,
7
+ PROVIDER_TIMEOUT_MAX_ATTEMPTS,
8
+ PROVIDER_TIMEOUT_RETRY_DELAY_MS,
7
9
  REQUEST_TIMEOUT_MS,
8
10
  } = require('../config');
9
11
  const { chat, chatSilent } = require('../providers/scraperClient');
@@ -25,6 +27,29 @@ const { normalizeText, shortText } = require('../utils/text');
25
27
  const { detectLanguage } = require('../i18n');
26
28
 
27
29
 
30
+
31
+ function waitForRetry(ms, signal) {
32
+ if (!ms || ms <= 0) return Promise.resolve();
33
+ if (signal?.aborted) return Promise.reject(new Error('aborted'));
34
+
35
+ return new Promise((resolve, reject) => {
36
+ const timeout = setTimeout(cleanupAndResolve, ms);
37
+ const onAbort = () => {
38
+ clearTimeout(timeout);
39
+ cleanup();
40
+ reject(new Error('aborted'));
41
+ };
42
+ function cleanup() {
43
+ if (signal) signal.removeEventListener('abort', onAbort);
44
+ }
45
+ function cleanupAndResolve() {
46
+ cleanup();
47
+ resolve();
48
+ }
49
+ if (signal) signal.addEventListener('abort', onAbort, { once: true });
50
+ });
51
+ }
52
+
28
53
  function looksLikeActionRequest(text) {
29
54
  const sample = normalizeText(String(text || '')).toLowerCase();
30
55
  if (!sample) return false;
@@ -38,7 +63,7 @@ async function requestModel(messages, state, ui, options = {}) {
38
63
  signal,
39
64
  } = options;
40
65
 
41
- for (let attempt = 0; attempt < 2; attempt += 1) {
66
+ for (let attempt = 0; attempt < PROVIDER_TIMEOUT_MAX_ATTEMPTS; attempt += 1) {
42
67
  if (signal?.aborted) {
43
68
  throw new Error(state.language === 'es' ? 'Agente detenido por el usuario (ESC x2)' : 'Agent stopped by the user (ESC x2)');
44
69
  }
@@ -96,8 +121,21 @@ async function requestModel(messages, state, ui, options = {}) {
96
121
  } catch (err) {
97
122
  const externalAbort = Boolean(signal?.aborted);
98
123
  const aborted = controller.signal.aborted || err?.name === 'AbortError';
99
- if (aborted && timedOut && !externalAbort && attempt === 0) {
100
- ui.logEvent(state, 'warn', state.language === 'es' ? 'Proveedor lento, reenviando mensaje' : 'Provider stalled, resending message');
124
+ if (aborted && timedOut && !externalAbort && attempt < PROVIDER_TIMEOUT_MAX_ATTEMPTS - 1) {
125
+ const waitMinutes = Math.round(PROVIDER_TIMEOUT_RETRY_DELAY_MS / 60000);
126
+ ui.logEvent(
127
+ state,
128
+ 'warn',
129
+ state.language === 'es' ? 'Tiempo agotado del proveedor' : 'Provider timeout',
130
+ state.language === 'es'
131
+ ? `Esperando ${waitMinutes} minutos antes de reenviar (${attempt + 2}/${PROVIDER_TIMEOUT_MAX_ATTEMPTS})`
132
+ : `Waiting ${waitMinutes} minutes before resending (${attempt + 2}/${PROVIDER_TIMEOUT_MAX_ATTEMPTS})`,
133
+ );
134
+ try {
135
+ await waitForRetry(PROVIDER_TIMEOUT_RETRY_DELAY_MS, signal);
136
+ } catch {
137
+ throw new Error(state.language === 'es' ? 'Agente detenido por el usuario (ESC x2)' : 'Agent stopped by the user (ESC x2)');
138
+ }
101
139
  continue;
102
140
  }
103
141
  if (aborted) {
@@ -106,7 +144,7 @@ async function requestModel(messages, state, ui, options = {}) {
106
144
  }
107
145
  throw new Error(state.language === 'es' ? 'Tiempo agotado del proveedor' : 'Provider timeout exceeded');
108
146
  }
109
- if (!externalAbort && attempt === 0) {
147
+ if (!externalAbort && attempt < PROVIDER_TIMEOUT_MAX_ATTEMPTS - 1) {
110
148
  ui.logEvent(state, 'warn', state.language === 'es' ? 'Error transitorio, reenviando contexto y skills' : 'Transient error, resending context and skills');
111
149
  continue;
112
150
  }
@@ -131,10 +169,13 @@ async function summarizeMessages(state, ui, messages) {
131
169
  {
132
170
  role: 'system',
133
171
  content: [
134
- state.language === 'es' ? 'Resume la conversacion para memoria persistente.' : 'Summarize the conversation for persistent memory.',
135
- state.language === 'es' ? 'Escribe en espanol.' : 'Write in English.',
136
- state.language === 'es' ? 'Incluye objetivos, decisiones, archivos, comandos, restricciones y pendientes importantes.' : 'Include goals, decisions, files, commands, constraints, and important pending items.',
137
- 'Max 12 lines.',
172
+ state.language === 'es' ? 'Compacta la conversacion para memoria persistente del agente.' : 'Compact the conversation for the agent persistent memory.',
173
+ state.language === 'es' ? 'Escribe en español y conserva los idiomas/preferencias indicados por el usuario.' : 'Write in English and preserve any languages/preferences requested by the user.',
174
+ state.language === 'es'
175
+ ? 'Resume sin perder detalles críticos: objetivo del proyecto, progreso exacto, decisiones, archivos/rutas tocadas, comandos ejecutados, resultados, errores, credenciales/configuraciones no secretas, restricciones, próximos pasos y preferencias de idioma.'
176
+ : 'Summarize without losing critical details: project goal, exact progress, decisions, touched files/paths, executed commands, results, errors, non-secret credentials/configuration, constraints, next steps, and language preferences.',
177
+ state.language === 'es' ? 'No inventes; si algo está pendiente márcalo como Pendiente. Mantén nombres propios, rutas, APIs, límites y valores numéricos intactos.' : 'Do not invent; if something is pending mark it as Pending. Keep proper nouns, paths, APIs, limits, and numeric values intact.',
178
+ 'Format: compact bullet list with sections Contexto/Progreso/Archivos/Comandos/Pendiente/Preferencias. Max 20 lines.',
138
179
  ].join('\n'),
139
180
  },
140
181
  {
@@ -205,9 +246,9 @@ async function answerFromToolResult(input, call, result, state, ui) {
205
246
 
206
247
  const output = await requestModel(messages, state, ui, {
207
248
  label: state.language === 'es' ? 'Resumiendo resultado' : 'Summarizing result',
208
- streamOutput: true,
209
249
  });
210
- return normalizeText(output);
250
+ const parsed = parseAgentResponse(output);
251
+ return parsed.type === 'final' ? normalizeText(parsed.content) : normalizeText(output);
211
252
  }
212
253
 
213
254
  async function runAgentTurn(input, state, ui, options = {}) {
@@ -241,7 +282,7 @@ async function runAgentTurn(input, state, ui, options = {}) {
241
282
  });
242
283
  ui.logEvent(state, 'ok', state.language === 'es' ? 'Respuesta lista' : 'Response ready');
243
284
  await persistSessionState(state, ui);
244
- return { content: finalAnswer, rendered: true };
285
+ return { content: finalAnswer, rendered: false };
245
286
  }
246
287
 
247
288
  const turnMessages = [{ role: 'user', content: input }];
@@ -44,6 +44,23 @@ function getPlatformInfo() {
44
44
  return osName;
45
45
  }
46
46
 
47
+
48
+ const TOOL_ALIASES = {
49
+ bash: 'run_command',
50
+ shell: 'run_command',
51
+ terminal: 'run_command',
52
+ execute_command: 'run_command',
53
+ command: 'run_command',
54
+ run_terminal_command: 'run_command',
55
+ };
56
+
57
+ function normalizeToolName(name) {
58
+ const raw = String(name || '').trim();
59
+ if (!raw) return raw;
60
+ const lower = raw.toLowerCase();
61
+ return TOOL_ALIASES[lower] || lower;
62
+ }
63
+
47
64
  const KNOWN_TOOLS = new Set([
48
65
  ...TOOL_DEFINITIONS.map(tool => tool.name),
49
66
  'task_create', 'task_list', 'task_update', 'task_complete', 'task_delete', 'task_clear',
@@ -66,7 +83,7 @@ function buildSystemPrompt(cwd, state = {}, options = {}) {
66
83
 
67
84
  const languageInstructions = language === 'es'
68
85
  ? [
69
- 'Responde siempre en español.',
86
+ 'Responde en el idioma del ultimo mensaje del usuario (si es ambiguo, usa la preferencia de sesion).',
70
87
  'Ejecuta la tarea directamente. No des tutoriales ni instrucciones al usuario cuando puedas actuar tú mismo.',
71
88
  'Si hace falta, usa herramientas sin pedir permiso extra.',
72
89
  'Responde solo con el resultado final o con la siguiente accion concreta.',
@@ -76,12 +93,13 @@ function buildSystemPrompt(cwd, state = {}, options = {}) {
76
93
  'No cierres con una conclusion si todavia no has probado nada.',
77
94
  'Si una tarea dura demasiado, usa run_command con un timeoutMs adecuado y confirma el resultado real.',
78
95
  'Para operaciones de Git usa la herramienta git con action="api" o action="clone".',
79
- 'Para proyectos, usa combinaciones de tools según la fase: descubrir (list_dir/search_text), leer (read_file/fetch/webfetch), cambiar (write/replace), validar (run_command), documentar (final).',
96
+ 'Para proyectos, usa combinaciones de tools según la fase: descubrir (list_dir/search_text), leer (read_file/fetch/webfetch), cambiar (write/replace), validar (run_command), subir artefactos si hace falta (upload_file), documentar (final).',
97
+ 'Antes de elegir una herramienta revisa su nombre exacto, argumentos requeridos y resultado esperado; usa read/search/list antes de editar y run_command para validar cambios.',
80
98
  'No te limites a una sola tool por costumbre; elige la mejor secuencia técnica para el objetivo.',
81
99
  'Si el usuario pide logos, mockups o piezas visuales para un proyecto/frontend, usa create_canvas_image cuando corresponda, junto al resto de tools del flujo.',
82
100
  ]
83
101
  : [
84
- 'Always respond in English.',
102
+ 'Respond in the language of the user\'s latest message (if ambiguous, use the session preference).',
85
103
  'Execute the task directly. Do not give tutorials or instructions when you can act yourself.',
86
104
  'Use tools when needed without asking for extra permission.',
87
105
  'Reply only with the final result or the next concrete action.',
@@ -91,9 +109,11 @@ function buildSystemPrompt(cwd, state = {}, options = {}) {
91
109
  'Do not end with a conclusion if you have not tested anything yet.',
92
110
  'If a task takes long, use run_command with an appropriate timeoutMs and verify the real result.',
93
111
  'For Git operations use the git tool with action="api" or action="clone".',
94
- 'For project work, combine tools by phase: discover (list_dir/search_text), read (read_file/fetch/webfetch), change (write/replace), validate (run_command), then report.',
112
+ 'For project work, combine tools by phase: discover (list_dir/search_text), read (read_file/fetch/webfetch), change (write/replace), validate (run_command), upload artifacts if needed (upload_file), then report.',
113
+ 'Before choosing a tool, verify its exact name, required args, and expected result; use read/search/list before editing and run_command to validate changes.',
95
114
  'Do not over-focus on a single tool by habit; choose the best technical sequence for the goal.',
96
115
  'If the user asks for logos, mockups, or visual assets for a project/frontend, use create_canvas_image when appropriate together with the rest of the workflow.',
116
+ 'Prioritize and reuse explicit user-provided context (requirements, constraints, repo instructions, preferences) across the full task; do not ignore it.',
97
117
  ];
98
118
 
99
119
  const toolUseEnforcement = language === 'es'
@@ -235,7 +255,7 @@ function extractXmlTool(text) {
235
255
  );
236
256
  if (!invokeMatch) return null;
237
257
 
238
- const tool = invokeMatch[1];
258
+ const tool = normalizeToolName(invokeMatch[1]);
239
259
  if (!KNOWN_TOOLS.has(tool)) return null;
240
260
 
241
261
  const rawArgs = invokeMatch[2].trim();
@@ -254,13 +274,15 @@ function extractXmlTool(text) {
254
274
 
255
275
  function classifyParsed(parsed) {
256
276
  if (parsed?.type === 'tool' && parsed.tool) {
257
- return { type: 'tool', tool: parsed.tool, args: parsed.args ?? {} };
277
+ const normalized = normalizeToolName(parsed.tool);
278
+ if (KNOWN_TOOLS.has(normalized)) return { type: 'tool', tool: normalized, args: parsed.args ?? {} };
258
279
  }
259
280
  if (parsed?.type === 'final') {
260
281
  return { type: 'final', content: typeof parsed.content === 'string' ? parsed.content : '' };
261
282
  }
262
- if (parsed?.tool && KNOWN_TOOLS.has(parsed.tool)) {
263
- return { type: 'tool', tool: parsed.tool, args: parsed.args ?? {} };
283
+ if (parsed?.tool) {
284
+ const normalized = normalizeToolName(parsed.tool);
285
+ if (KNOWN_TOOLS.has(normalized)) return { type: 'tool', tool: normalized, args: parsed.args ?? {} };
264
286
  }
265
287
  return null;
266
288
  }
@@ -283,6 +305,8 @@ const TOOL_ARG_KEYS = {
283
305
  scrape_site: ['url', 'selectors', 'limit', 'headers'],
284
306
  web_search: ['query', 'lang', 'limit'],
285
307
  web_read: ['url'],
308
+ upload_file: ['path', 'field', 'name', 'type'],
309
+ gmail: ['action', 'query', 'maxResults', 'id', 'to', 'subject', 'body'],
286
310
  create_canvas_image: ['width', 'height', 'background', 'elements', 'format', 'outputPath'],
287
311
  git: ['provider', 'action', 'method', 'path', 'body', 'headers', 'name', 'repoUrl', 'destination', 'branch', 'timeoutMs'],
288
312
  };
@@ -294,11 +318,43 @@ const LONG_VALUE_ARG = {
294
318
  replace_in_file: 'replace',
295
319
  };
296
320
 
321
+
322
+ function extractMalformedFinalContent(text) {
323
+ const typeMatch = text.match(/(?:["'])type(?:["'])\s*:\s*(?:["'])final(?:["'])/i);
324
+ if (!typeMatch) return null;
325
+
326
+ const contentMatch = text.match(/(?:["'])content(?:["'])\s*:\s*(["'])/i);
327
+ if (!contentMatch) return null;
328
+
329
+ const quote = contentMatch[1];
330
+ const start = contentMatch.index + contentMatch[0].length;
331
+ let esc = false;
332
+
333
+ for (let i = start; i < text.length; i += 1) {
334
+ const ch = text[i];
335
+ if (esc) { esc = false; continue; }
336
+ if (ch === '\\') { esc = true; continue; }
337
+ if (ch !== quote) continue;
338
+
339
+ const tail = text.slice(i + 1).trim();
340
+ if (!tail || /^}\s*$/.test(tail) || /^,\s*(["']\w+["']\s*:|})/.test(tail)) {
341
+ return unescapeJsonString(text.slice(start, i)).trim();
342
+ }
343
+ }
344
+
345
+ const lastQuote = text.lastIndexOf(quote);
346
+ if (lastQuote > start) {
347
+ return unescapeJsonString(text.slice(start, lastQuote)).trim();
348
+ }
349
+
350
+ return null;
351
+ }
352
+
297
353
  function fuzzyExtractTool(text) {
298
354
  const toolMatch = text.match(/(?:"|')?tool(?:"|')?\s*:\s*"(\w+)"/i);
299
355
  if (!toolMatch) return null;
300
356
 
301
- const tool = toolMatch[1];
357
+ const tool = normalizeToolName(toolMatch[1]);
302
358
  if (!KNOWN_TOOLS.has(tool)) return null;
303
359
 
304
360
  const longArg = LONG_VALUE_ARG[tool];
@@ -422,6 +478,13 @@ function parseAgentResponse(raw) {
422
478
  const tool = extractToolJson(text);
423
479
  if (tool) return { type: 'tool', tool: tool.tool, args: tool.args ?? {} };
424
480
 
481
+ const malformedFinalContent = extractMalformedFinalContent(text);
482
+ if (malformedFinalContent !== null) {
483
+ const embedded = extractToolJson(malformedFinalContent);
484
+ if (embedded) return { type: 'tool', tool: embedded.tool, args: embedded.args ?? {} };
485
+ return { type: 'final', content: malformedFinalContent };
486
+ }
487
+
425
488
  const xmlTool = extractXmlTool(text);
426
489
  if (xmlTool) return xmlTool;
427
490
 
@@ -485,7 +548,7 @@ function buildToolResultMessage(parsed, result) {
485
548
 
486
549
  function buildToolErrorMessage(parsed, errorMessage) {
487
550
  return [
488
- `La herramienta "${parsed.tool}" NO existe.`,
551
+ `La herramienta "${parsed.tool}" no se pudo ejecutar (nombre inválido o no disponible en este modo).`,
489
552
  `Error: ${errorMessage}`,
490
553
  `Las unicas herramientas disponibles son: ${TOOL_DEFINITIONS.map(t => t.name).join(', ')}.`,
491
554
  'Elige UNA de esas herramientas. Usa el formato exacto del nombre. No inventes herramientas.',
package/src/i18n.js CHANGED
@@ -30,7 +30,7 @@ const STRINGS = {
30
30
  webUrl: 'Web URL',
31
31
  chooseLanguage: 'Choose language from commands: /lang en or /lang es',
32
32
  langCurrent: 'Current language',
33
- langChanged: 'Language updated',
33
+ langChanged: 'Updated to language',
34
34
  langInvalid: 'Unsupported language. Available: en, es',
35
35
  noSavedSessions: 'No saved sessions.',
36
36
  noMemory: 'No compacted memory.',
@@ -75,7 +75,7 @@ const STRINGS = {
75
75
  webUrl: 'URL web',
76
76
  chooseLanguage: 'Elige idioma con /lang en o /lang es',
77
77
  langCurrent: 'Idioma actual',
78
- langChanged: 'Idioma actualizado',
78
+ langChanged: 'Actualizado al idioma',
79
79
  langInvalid: 'Idioma no soportado. Disponibles: en, es',
80
80
  noSavedSessions: 'No hay sesiones guardadas.',
81
81
  noMemory: 'Sin memoria compactada.',