zyn-ai 1.2.1 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE CHANGED
@@ -1,15 +1,21 @@
1
- Zyn Attribution License 1.0
1
+ MIT License
2
2
 
3
- Copyright (c) 2026 Maycol and contributors.
3
+ Copyright (c) 2026 Maycol
4
4
 
5
- Permission is granted to use, copy, modify, and redistribute this software,
6
- including commercial use, provided that all of the following conditions are met:
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
7
11
 
8
- 1. You keep this license notice intact.
9
- 2. You keep visible credit to the original project in source form and in any
10
- distributed derivative work.
11
- 3. You include the project link below, or a clearly documented replacement if
12
- the canonical URL changes.
13
- 4. Modified versions must clearly state that they were changed from the original.
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
14
 
15
- This software is provided "as is", without warranty of any kind.
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,14 +1,18 @@
1
- # Zyn
1
+ # Zyn Agent
2
2
 
3
3
  <p align="center">
4
4
  <img src="http://cdn.soymaycol.icu/files/logo_zyn.png" alt="Zyn logo" width="180" />
5
5
  </p>
6
6
 
7
- <p>
7
+ <p align="center">
8
8
  <img src="https://img.shields.io/npm/v/zyn-ai?label=npm&color=%23CB3837" alt="NPM Version"/>
9
-
9
+
10
10
  <img src="https://img.shields.io/github/v/release/SoyMaycol/Zyn?include_prereleases&sort=semver" alt="Latest Release"/>
11
-
11
+
12
+ <img src="https://img.shields.io/npm/dt/zyn-ai" alt="Downloads"/>
13
+
14
+ <img src="https://badge.socket.dev/npm/package/zyn-ai/latest" alt="Security"/>
15
+
12
16
  <img src="https://img.shields.io/github/forks/SoyMaycol/Zyn" alt="Forks"/>
13
17
  </p>
14
18
 
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "zyn-ai",
3
- "version": "1.2.1",
3
+ "version": "1.3.1",
4
4
  "description": "Production-ready AI agent for CLI and web with real tool execution and automation",
5
- "author": "Maycol y Ado",
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",
@@ -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,8 @@ 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' },
29
+ { name: 'persona', desc: 'set response tone/personality' },
27
30
  { name: 'lang', desc: 'change language' },
28
31
  { name: 'language', desc: 'change language' },
29
32
  { name: 'auto', desc: 'auto-approval' },
@@ -194,9 +197,62 @@ async function handleLocalCommand(input, state, deps) {
194
197
  return true;
195
198
  }
196
199
 
200
+ if (commandName === 'git') {
201
+ const [sub, ...rest] = args.split(' ').filter(Boolean);
202
+ if (!sub || sub === 'help') {
203
+ console.log('Uso: /git list | /git set <provider> <token> [username] | /git remove <provider>');
204
+ return true;
205
+ }
206
+ if (sub === 'list') {
207
+ const secrets = listGitSecrets();
208
+ if (!secrets.length) console.log('No hay credenciales git guardadas.');
209
+ else secrets.forEach(s => console.log(`${s.key} user:${s.username || '-'} api:${s.apiBaseUrl || '-'}`));
210
+ return true;
211
+ }
212
+ if (sub === 'set') {
213
+ const [provider, token, username] = rest;
214
+ if (!provider || !token) throw new Error('Uso: /git set <provider> <token> [username]');
215
+ upsertGitSecret(provider, { provider, token, username });
216
+ console.log(`Credencial guardada para ${provider}`);
217
+ return true;
218
+ }
219
+ if (sub === 'remove') {
220
+ const [provider] = rest;
221
+ if (!provider) throw new Error('Uso: /git remove <provider>');
222
+ const removed = removeGitSecret(provider);
223
+ console.log(removed ? `Credencial eliminada: ${provider}` : `No existe credencial para ${provider}`);
224
+ return true;
225
+ }
226
+ throw new Error('Subcomando git no reconocido. Usa /git help');
227
+ }
228
+
229
+ if (commandName === 'persona') {
230
+ const [sub, ...rest] = args.split(' ');
231
+ if (!sub || sub === 'show') {
232
+ console.log(state.personaPrompt ? `Persona activa:\n${state.personaPrompt}` : 'Persona por defecto activa.');
233
+ return true;
234
+ }
235
+ if (sub === 'reset' || sub === 'default') {
236
+ state.personaPrompt = '';
237
+ await saveState(state);
238
+ console.log('Persona restaurada al estado por defecto.');
239
+ return true;
240
+ }
241
+ if (sub === 'set') {
242
+ const text = rest.join(' ').trim();
243
+ if (!text) throw new Error('Uso: /persona set <descripcion>');
244
+ state.personaPrompt = text;
245
+ await saveState(state);
246
+ console.log('Persona actualizada (solo estilo).');
247
+ return true;
248
+ }
249
+ throw new Error('Uso: /persona show | /persona set <texto> | /persona reset');
250
+ }
251
+
197
252
  if (commandName === 'new') {
198
253
  const nextState = await createNewSessionState(state.rl);
199
254
  applyLoadedState(state, nextState);
255
+ if (typeof state.clearQueuedMessages === 'function') state.clearQueuedMessages();
200
256
  global.__zynActiveModel = state.activeModel || DEFAULT_MODEL_KEY;
201
257
  printBanner(state);
202
258
  console.log(`${t(state.language, 'newSessionCreated')}: ${state.sessionId}`);
@@ -215,6 +271,7 @@ async function handleLocalCommand(input, state, deps) {
215
271
  }
216
272
 
217
273
  applyLoadedState(state, loaded);
274
+ if (typeof state.clearQueuedMessages === 'function') state.clearQueuedMessages();
218
275
  global.__zynActiveModel = state.activeModel || DEFAULT_MODEL_KEY;
219
276
  await saveState(state);
220
277
  printBanner(state);
@@ -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) {
@@ -119,6 +120,7 @@ async function runInteractiveChatClassic(options = {}) {
119
120
  let currentAbort = null;
120
121
 
121
122
  state.getQueuedMessages = () => messageQueue.splice(0);
123
+ state.clearQueuedMessages = () => { messageQueue.length = 0; };
122
124
  state.abortCurrentTurn = () => {
123
125
  if (currentAbort && !currentAbort.signal.aborted) {
124
126
  currentAbort.abort();
@@ -175,7 +177,7 @@ async function runInteractiveChatClassic(options = {}) {
175
177
  if (!input) continue;
176
178
 
177
179
  if (input === '/exit' || input === '/quit') {
178
- logEvent(state, 'info', 'Hasta luego');
180
+ logEvent(state, 'info', t(state.language, 'goodbye'));
179
181
  break;
180
182
  }
181
183
 
@@ -202,7 +204,7 @@ async function runInteractiveChatClassic(options = {}) {
202
204
  rl.removeListener('line', lineHandler);
203
205
 
204
206
  if (pendingExit) {
205
- logEvent(state, 'info', 'Hasta luego');
207
+ logEvent(state, 'info', t(state.language, 'goodbye'));
206
208
  break;
207
209
  }
208
210
  }
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 || process.env.QWEN_EMAIL || 'danielalejandrobasado@gmail.com';
95
- const QWEN_PASSWORD = process.env.ZYN_QWEN_PASSWORD || process.env.QWEN_PASSWORD || 'zyzz1234';
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;
@@ -104,6 +104,7 @@ const KEEP_RECENT_MESSAGES = 12;
104
104
  const SESSION_ROOT = path.join(DATA_ROOT, 'chat');
105
105
  const SESSIONS_DIR = path.join(SESSION_ROOT, 'sessions');
106
106
  const CURRENT_SESSION_FILE = path.join(SESSION_ROOT, 'current-session.json');
107
+ const PERSISTENT_CONFIG_FILE = path.join(SESSION_ROOT, 'persistent-config.json');
107
108
  const TRANSCRIPTS_DIR = path.join(SESSION_ROOT, 'transcripts');
108
109
  const EXPORTS_DIR = path.join(SESSION_ROOT, 'exports');
109
110
  const THINK_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
@@ -134,6 +135,7 @@ module.exports = {
134
135
  APP_ROOT,
135
136
  BUILTIN_MODELS,
136
137
  CURRENT_SESSION_FILE,
138
+ PERSISTENT_CONFIG_FILE,
137
139
  DATA_ROOT,
138
140
  DEFAULT_LANGUAGE,
139
141
  DEFAULT_MODEL_KEY,
package/src/core/agent.js CHANGED
@@ -37,68 +37,88 @@ async function requestModel(messages, state, ui, options = {}) {
37
37
  signal,
38
38
  } = options;
39
39
 
40
- const stopThinking = ui.startThinkingIndicator(state, label);
41
- let answerStarted = false;
42
- let thinkingStarted = false;
43
- const controller = new AbortController();
44
- const onExternalAbort = () => controller.abort();
45
-
46
- if (signal) {
47
- if (signal.aborted) controller.abort();
48
- else signal.addEventListener('abort', onExternalAbort, { once: true });
49
- }
50
-
51
- const timeout = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
40
+ for (let attempt = 0; attempt < 2; attempt += 1) {
41
+ if (signal?.aborted) {
42
+ throw new Error(state.language === 'es' ? 'Agente detenido por el usuario (ESC x2)' : 'Agent stopped by the user (ESC x2)');
43
+ }
44
+ const stopThinking = ui.startThinkingIndicator(state, attempt === 0 ? label : `${label} (${state.language === 'es' ? 'reintento' : 'retry'})`);
45
+ let answerStarted = false;
46
+ let thinkingStarted = false;
47
+ let timedOut = false;
48
+ let timeout = null;
49
+ const controller = new AbortController();
50
+ const onExternalAbort = () => controller.abort();
51
+
52
+ if (signal) {
53
+ if (signal.aborted) controller.abort();
54
+ else signal.addEventListener('abort', onExternalAbort, { once: true });
55
+ }
56
+ const refreshTimeout = () => {
57
+ if (timeout) clearTimeout(timeout);
58
+ timeout = setTimeout(() => {
59
+ timedOut = true;
60
+ controller.abort();
61
+ }, REQUEST_TIMEOUT_MS);
62
+ };
63
+ refreshTimeout();
52
64
 
53
- try {
54
- const result = await chat({
55
- messages,
56
- modelKey: state?.activeModel || DEFAULT_MODEL_KEY,
57
- signal: controller.signal,
58
- onChunk: (delta, phase) => {
59
- if (phase === 'thinking') {
60
- if (!thinkingStarted) {
65
+ try {
66
+ const result = await chat({
67
+ messages,
68
+ modelKey: state?.activeModel || DEFAULT_MODEL_KEY,
69
+ signal: controller.signal,
70
+ onChunk: (delta, phase) => {
71
+ refreshTimeout();
72
+ if (phase === 'thinking') {
73
+ if (!thinkingStarted) {
74
+ stopThinking();
75
+ ui.beginThinkingStream(state);
76
+ thinkingStarted = true;
77
+ }
78
+ ui.writeThinkingDelta(state, delta);
79
+ return;
80
+ }
81
+ if (thinkingStarted) {
82
+ ui.endThinkingStream(state);
83
+ thinkingStarted = false;
84
+ }
85
+ if (streamOutput && !answerStarted) {
61
86
  stopThinking();
62
- ui.beginThinkingStream(state);
63
- thinkingStarted = true;
87
+ ui.beginAssistantStream(state);
88
+ answerStarted = true;
64
89
  }
65
- ui.writeThinkingDelta(state, delta);
66
- return;
67
- }
68
-
69
- if (thinkingStarted) {
70
- ui.endThinkingStream(state);
71
- thinkingStarted = false;
72
- }
73
-
74
- if (streamOutput && !answerStarted) {
75
- stopThinking();
76
- ui.beginAssistantStream(state);
77
- answerStarted = true;
78
- }
79
-
80
- if (streamOutput) {
81
- ui.writeAssistantDelta(state, delta);
90
+ if (streamOutput) ui.writeAssistantDelta(state, delta);
91
+ },
92
+ });
93
+ ui.pushAction(state, 'ok', 'Respuesta del modelo recibida');
94
+ return result.answer ?? '';
95
+ } catch (err) {
96
+ const externalAbort = Boolean(signal?.aborted);
97
+ const aborted = controller.signal.aborted || err?.name === 'AbortError';
98
+ if (aborted && timedOut && !externalAbort && attempt === 0) {
99
+ ui.logEvent(state, 'warn', state.language === 'es' ? 'Proveedor lento, reenviando mensaje' : 'Provider stalled, resending message');
100
+ continue;
101
+ }
102
+ if (aborted) {
103
+ if (externalAbort) {
104
+ throw new Error(state.language === 'es' ? 'Agente detenido por el usuario (ESC x2)' : 'Agent stopped by the user (ESC x2)');
82
105
  }
83
- },
84
- });
85
-
86
- ui.pushAction(state, 'ok', 'Respuesta del modelo recibida');
87
- return result.answer ?? '';
88
- } catch (err) {
89
- if (controller.signal.aborted || err?.name === 'AbortError') {
90
- throw new Error(state.language === 'es' ? 'Agente detenido por el usuario o por tiempo agotado' : 'Agent stopped by the user or timed out');
91
- }
92
- throw err;
93
- } finally {
94
- clearTimeout(timeout);
95
- if (signal) signal.removeEventListener('abort', onExternalAbort);
96
- stopThinking();
97
- if (thinkingStarted) ui.endThinkingStream(state);
98
- if (streamOutput && answerStarted) {
99
- ui.endAssistantStream(state);
106
+ throw new Error(state.language === 'es' ? 'Tiempo agotado del proveedor' : 'Provider timeout exceeded');
107
+ }
108
+ if (!externalAbort && attempt === 0) {
109
+ ui.logEvent(state, 'warn', state.language === 'es' ? 'Error transitorio, reenviando contexto y skills' : 'Transient error, resending context and skills');
110
+ continue;
111
+ }
112
+ throw err;
113
+ } finally {
114
+ clearTimeout(timeout);
115
+ if (signal) signal.removeEventListener('abort', onExternalAbort);
116
+ stopThinking();
117
+ if (thinkingStarted) ui.endThinkingStream(state);
118
+ if (streamOutput && answerStarted) ui.endAssistantStream(state);
100
119
  }
101
120
  }
121
+ throw new Error(state.language === 'es' ? 'No se pudo obtener respuesta del proveedor' : 'Could not get provider response');
102
122
  }
103
123
 
104
124
  async function summarizeMessages(state, ui, messages) {
@@ -197,6 +217,9 @@ async function runAgentTurn(input, state, ui, options = {}) {
197
217
  }
198
218
  ui.logEvent(state, 'info', `${state.language === 'es' ? 'Turno' : 'Turn'} ${state.turnCount}`);
199
219
 
220
+ let toolUsedThisTurn = false;
221
+ let finalWithoutToolRetries = 0;
222
+
200
223
  const directAction = parseDirectAction(input);
201
224
  if (directAction) {
202
225
  await appendTranscriptEntry(state.sessionId, { type: 'user', content: input });
@@ -225,10 +248,10 @@ async function runAgentTurn(input, state, ui, options = {}) {
225
248
 
226
249
  let lastFingerprint = '';
227
250
  let repeatCount = 0;
251
+ const toolPathUsage = new Map();
228
252
  let step = 0;
229
253
  const turnLanguage = detectLanguage(input, state.language);
230
- let toolUsedThisTurn = false;
231
- let finalWithoutToolRetries = 0;
254
+ state.language = turnLanguage;
232
255
 
233
256
  while (true) {
234
257
  if (signal?.aborted) {
@@ -387,7 +410,27 @@ async function runAgentTurn(input, state, ui, options = {}) {
387
410
  return { content, rendered: false };
388
411
  }
389
412
 
390
- const fingerprint = `${parsed.tool}:${parsed.args?.path || ''}:${(parsed.args?.content || parsed.args?.search || '').length}`;
413
+ const targetPath = parsed.args?.path || '';
414
+ const contentSample = typeof parsed.args?.content === 'string'
415
+ ? parsed.args.content.slice(0, 120)
416
+ : '';
417
+ const fingerprint = `${parsed.tool}:${targetPath}:${contentSample}`;
418
+ if (targetPath) {
419
+ const key = `${parsed.tool}:${targetPath}`;
420
+ const nextCount = (toolPathUsage.get(key) || 0) + 1;
421
+ toolPathUsage.set(key, nextCount);
422
+ if (nextCount >= 3 && ['write_file', 'append_file', 'replace_in_file'].includes(parsed.tool)) {
423
+ ui.logEvent(state, 'warn', state.language === 'es' ? 'Posible loop detectado' : 'Possible loop detected', `${parsed.tool} → ${targetPath} x${nextCount}`);
424
+ turnMessages.push({
425
+ role: 'user',
426
+ content: state.language === 'es'
427
+ ? `ALTO: ya editaste ${targetPath} varias veces en este turno. No repitas ediciones; valida y responde con type=final.`
428
+ : `STOP: you already edited ${targetPath} multiple times in this turn. Do not repeat edits; verify and answer with type=final.`,
429
+ });
430
+ step += 1;
431
+ continue;
432
+ }
433
+ }
391
434
  if (fingerprint === lastFingerprint) {
392
435
  repeatCount += 1;
393
436
  if (repeatCount >= 2) {
@@ -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
- 'list_dir', 'read_file', 'search_text', 'glob_files', 'file_info',
9
- 'run_command', 'make_dir', 'write_file', 'append_file', 'replace_in_file',
10
- 'fetch_url', 'web_search', 'web_read',
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,14 @@ 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.',
44
+ 'Para tareas empresariales, mantente acotado y determinista: entradas claras, salidas claras, sin razonamiento creativo salvo que el usuario lo pida.',
45
+ 'Cuando aplique, entrega resumen ejecutivo corto + riesgos/banderas rojas + siguiente accion concreta.',
46
+ 'Para proyectos, usa combinaciones de tools según la fase: descubrir (list_dir/search_text), leer (read_file/fetch/webfetch), cambiar (write/replace), validar (run_command), documentar (final).',
47
+ 'No te limites a una sola tool por costumbre; elige la mejor secuencia técnica para el objetivo.',
48
+ '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.',
40
49
  ]
41
50
  : [
42
51
  'Always respond in English.',
@@ -47,6 +56,14 @@ function buildSystemPrompt(cwd, state = {}, options = {}) {
47
56
  'Never pretend you completed an action if you did not actually use tools or obtain a real result.',
48
57
  'If the task requires verification, try a real tool first and wait for its result before concluding.',
49
58
  'Do not end with a conclusion if you have not tested anything yet.',
59
+ 'If a task takes long, use run_command with an appropriate timeoutMs and verify the real result.',
60
+ '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.',
61
+ 'Use only tools listed under "Tool use". Never invent tool names or aliases.',
62
+ 'For business tasks, stay bounded and deterministic: clear inputs, clear outputs, no creative reasoning unless explicitly requested.',
63
+ 'When relevant, provide a short executive summary + obvious red flags + next concrete action.',
64
+ 'For project work, combine tools by phase: discover (list_dir/search_text), read (read_file/fetch/webfetch), change (write/replace), validate (run_command), then report.',
65
+ 'Do not over-focus on a single tool by habit; choose the best technical sequence for the goal.',
66
+ '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.',
50
67
  ];
51
68
 
52
69
  const parts = [
@@ -68,6 +85,15 @@ function buildSystemPrompt(cwd, state = {}, options = {}) {
68
85
  providerGroups,
69
86
  ];
70
87
 
88
+ if (state.personaPrompt && state.personaPrompt.trim()) {
89
+ parts.push(
90
+ '',
91
+ '# Persona style (tone only)',
92
+ 'Apply this only to communication style. Do NOT change tool choice, safety rules, or technical decisions.',
93
+ state.personaPrompt.trim(),
94
+ );
95
+ }
96
+
71
97
  if (state.concuerdo) {
72
98
  const activeKey = state.activeModel || DEFAULT_MODEL_KEY;
73
99
  const otherKeys = Object.keys(MODELS).filter(k => k !== activeKey);
@@ -173,8 +199,13 @@ const TOOL_ARG_KEYS = {
173
199
  append_file: ['path', 'content'],
174
200
  replace_in_file: ['path', 'search', 'replace', 'all'],
175
201
  fetch_url: ['url', 'selector', 'attribute', 'limit'],
176
- web_search: ['query'],
202
+ fetch: ['url', 'method', 'headers', 'query', 'json', 'data', 'form', 'files', 'timeoutMs'],
203
+ fetch_http: ['url', 'method', 'headers', 'query', 'json', 'data', 'form', 'files', 'timeoutMs'],
204
+ webfetch: ['url', 'headers', 'timeoutMs'],
205
+ scrape_site: ['url', 'selectors', 'limit', 'headers'],
206
+ web_search: ['query', 'lang', 'limit'],
177
207
  web_read: ['url'],
208
+ create_canvas_image: ['width', 'height', 'background', 'elements', 'format', 'outputPath'],
178
209
  };
179
210
 
180
211
  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