zyn-ai 1.1.2 → 1.2.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/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  Zyn Attribution License 1.0
2
2
 
3
- Copyright (c) 2026 Maycol Barco Tineo and contributors.
3
+ Copyright (c) 2026 Maycol and contributors.
4
4
 
5
5
  Permission is granted to use, copy, modify, and redistribute this software,
6
6
  including commercial use, provided that all of the following conditions are met:
@@ -12,6 +12,4 @@ including commercial use, provided that all of the following conditions are met:
12
12
  the canonical URL changes.
13
13
  4. Modified versions must clearly state that they were changed from the original.
14
14
 
15
- Project link: <PROJECT_URL>
16
-
17
15
  This software is provided "as is", without warranty of any kind.
@@ -12,7 +12,6 @@ Tono: Tecnico, directo, conciso.
12
12
  - Eficiente: minimas operaciones necesarias. Lee contexto antes de actuar.
13
13
  - Honesto: si algo falla, indicalo sin rodeos.
14
14
  - Preciso: cambios que funcionan a la primera.
15
- - Verificador: no cierres una tarea con una conclusion de exito si no hay una prueba o resultado real que la sostenga.
16
15
  - Seguro: alerta vulnerabilidades y riesgos.
17
16
 
18
17
  # Formato de respuesta — CRITICO
@@ -11,7 +11,7 @@ NUNCA adivines la estructura de un proyecto. Investiga primero, actua despues.
11
11
  2. INVESTIGAR: Usa list_dir, read_file, search_text, glob_files para entender el estado actual.
12
12
  3. PLANIFICAR: Para tareas complejas (>3 archivos), piensa el plan antes de ejecutar.
13
13
  4. EJECUTAR: Haz los cambios necesarios de forma precisa y minimal.
14
- 5. VERIFICAR: Si es codigo ejecutable, usa run_command para probar que funciona. Si no puedes verificarlo, no lo des por terminado.
14
+ 5. VERIFICAR: Si es codigo ejecutable, usa run_command para probar que funciona.
15
15
 
16
16
  ## Reglas de investigacion
17
17
 
@@ -9,9 +9,6 @@ Hacer una tarea no es suficiente. Un agente serio no entrega “parece que funci
9
9
 
10
10
  Si una tarea no fue verificada, la tarea no está terminada.
11
11
 
12
- Antes de afirmar que algo funciona, el agente debe ejecutar una verificación real, no una suposición con buena autoestima.
13
- Si no hay prueba ejecutada, no hay conclusión de éxito.
14
-
15
12
  Este skill existe para que el agente:
16
13
  - entienda el proyecto antes de actuar,
17
14
  - adapte su estrategia al lenguaje, framework o entorno,
@@ -25,6 +22,3 @@ Este skill existe para que el agente:
25
22
 
26
23
  ```text
27
24
  NO HAY ENTREGA SIN VERIFICACIÓN
28
- NO HAY ÉXITO SIN PRUEBA REAL
29
- NO HAY CONCLUSIÓN SIN EVIDENCIA
30
- ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zyn-ai",
3
- "version": "1.1.2",
3
+ "version": "1.2.0",
4
4
  "description": "Professional local terminal and web agent",
5
5
  "author": "Maycol y Ado",
6
6
  "bin": {
package/src/core/agent.js CHANGED
@@ -21,6 +21,14 @@ const {
21
21
  const { appendTranscriptEntry } = require('../utils/transcriptStorage');
22
22
  const { estimateHistoryChars, saveState } = require('../utils/sessionStorage');
23
23
  const { normalizeText, shortText } = require('../utils/text');
24
+ const { detectLanguage } = require('../i18n');
25
+
26
+
27
+ function looksLikeActionRequest(text) {
28
+ const sample = normalizeText(String(text || '')).toLowerCase();
29
+ if (!sample) return false;
30
+ return /(instala|instalar|install|run|ejecuta|ejecutar|crea|crear|build|compile|compila|fix|arregla|corrige|update|actualiza|edita|edit|borra|elimina|remove|descarga|download|busca|search|prueba|test|verifica|check|configura|setup|mueve|move|importa|import|aplica|apply)/i.test(sample);
31
+ }
24
32
 
25
33
  async function requestModel(messages, state, ui, options = {}) {
26
34
  const {
@@ -192,6 +200,7 @@ async function runAgentTurn(input, state, ui, options = {}) {
192
200
  const directAction = parseDirectAction(input);
193
201
  if (directAction) {
194
202
  await appendTranscriptEntry(state.sessionId, { type: 'user', content: input });
203
+ toolUsedThisTurn = true;
195
204
  const result = await executeToolCall(directAction, state, ui);
196
205
  await appendTranscriptEntry(state.sessionId, {
197
206
  type: 'tool',
@@ -217,6 +226,9 @@ async function runAgentTurn(input, state, ui, options = {}) {
217
226
  let lastFingerprint = '';
218
227
  let repeatCount = 0;
219
228
  let step = 0;
229
+ const turnLanguage = detectLanguage(input, state.language);
230
+ let toolUsedThisTurn = false;
231
+ let finalWithoutToolRetries = 0;
220
232
 
221
233
  while (true) {
222
234
  if (signal?.aborted) {
@@ -235,7 +247,7 @@ async function runAgentTurn(input, state, ui, options = {}) {
235
247
  const messages = buildConversationMessages(
236
248
  state,
237
249
  turnMessages,
238
- buildSystemPrompt(state.cwd, state),
250
+ buildSystemPrompt(state.cwd, state, { input, language: detectLanguage(input, state.language) }),
239
251
  );
240
252
 
241
253
  const primaryPromise = requestModel(messages, state, ui, {
@@ -343,6 +355,27 @@ async function runAgentTurn(input, state, ui, options = {}) {
343
355
 
344
356
  if (parsed.type === 'final') {
345
357
  const content = parsed.content.trim();
358
+ if (looksLikeActionRequest(input) && !toolUsedThisTurn && finalWithoutToolRetries < 2) {
359
+ finalWithoutToolRetries += 1;
360
+ ui.logEvent(state, 'warn', turnLanguage === 'es' ? 'Sin prueba real todavía' : 'No real attempt yet', turnLanguage === 'es' ? 'Primero intenta una herramienta antes de concluir.' : 'Try a real tool before concluding.');
361
+ turnMessages.push({ role: 'assistant', content: content || raw.trim() });
362
+ turnMessages.push({
363
+ role: 'user',
364
+ content: [
365
+ turnLanguage === 'es'
366
+ ? 'Aun no has probado nada. No des una conclusion ni pasos teoricos.'
367
+ : 'You have not actually tried anything yet. Do not give a conclusion or theory steps.',
368
+ turnLanguage === 'es'
369
+ ? 'Primero intenta una herramienta real adecuada para la tarea.'
370
+ : 'First try a real tool that fits the task.',
371
+ turnLanguage === 'es'
372
+ ? 'Si ninguna herramienta aplica, dilo explicitamente con una sola frase corta y honesta.'
373
+ : 'If no tool applies, say so explicitly in one short honest sentence.',
374
+ ].join(' '),
375
+ });
376
+ step += 1;
377
+ continue;
378
+ }
346
379
  turnMessages.push({ role: 'assistant', content: content || raw.trim() });
347
380
  state.history.push(...turnMessages);
348
381
  await appendTranscriptEntry(state.sessionId, {
@@ -385,6 +418,7 @@ async function runAgentTurn(input, state, ui, options = {}) {
385
418
  });
386
419
 
387
420
  try {
421
+ toolUsedThisTurn = true;
388
422
  const result = await executeToolCall(parsed, state, ui);
389
423
  await appendTranscriptEntry(state.sessionId, {
390
424
  type: 'tool',
@@ -2,7 +2,7 @@ const { normalizeText } = require('../utils/text');
2
2
  const { buildSkillsPrompt } = require('./skills');
3
3
  const { getToolPromptText } = require('../tools');
4
4
  const { listProvidersFromModels, MODELS, DEFAULT_MODEL_KEY } = require('../config');
5
- const { normalizeLanguage, languageLabel } = require('../i18n');
5
+ const { detectLanguage, normalizeLanguage, languageLabel } = require('../i18n');
6
6
 
7
7
  const KNOWN_TOOLS = new Set([
8
8
  'list_dir', 'read_file', 'search_text', 'glob_files', 'file_info',
@@ -11,8 +11,8 @@ const KNOWN_TOOLS = new Set([
11
11
  ]);
12
12
 
13
13
 
14
- function buildSystemPrompt(cwd, state = {}) {
15
- const language = normalizeLanguage(state.language);
14
+ function buildSystemPrompt(cwd, state = {}, options = {}) {
15
+ const language = normalizeLanguage(options.language || state.language || detectLanguage(options.input || '', state.language));
16
16
  const platform = process.platform === 'linux' ? 'Linux'
17
17
  : process.platform === 'darwin' ? 'macOS'
18
18
  : process.platform;
@@ -35,9 +35,8 @@ function buildSystemPrompt(cwd, state = {}) {
35
35
  'Responde solo con el resultado final o con la siguiente accion concreta.',
36
36
  'Si el usuario pide editar, corregir, crear, mover, buscar o ejecutar, hazlo directamente.',
37
37
  'Nunca finjas que hiciste algo si no usaste herramientas o no tienes el resultado real.',
38
- 'Antes de dar por terminada una tarea tecnica, verifica el resultado con la herramienta adecuada.',
39
- 'Si no probaste lo que hiciste, no lo presentes como concluido.',
40
- 'Si necesitas leer, editar o ejecutar, usa una herramienta ahora mismo.',
38
+ 'Si la tarea requiere comprobar algo, primero intenta una herramienta real y espera el resultado antes de concluir.',
39
+ 'No cierres con una conclusion si todavia no has probado nada.',
41
40
  ]
42
41
  : [
43
42
  'Always respond in English.',
@@ -46,9 +45,8 @@ function buildSystemPrompt(cwd, state = {}) {
46
45
  'Reply only with the final result or the next concrete action.',
47
46
  'If the user asks to edit, fix, create, move, search, or execute, do it directly.',
48
47
  'Never pretend you completed an action if you did not actually use tools or obtain a real result.',
49
- 'Before treating a technical task as finished, verify the result with the right tool.',
50
- 'If you did not test what you changed, do not present it as complete.',
51
- 'If you need to read, edit, or execute something, use a tool now.',
48
+ 'If the task requires verification, try a real tool first and wait for its result before concluding.',
49
+ 'Do not end with a conclusion if you have not tested anything yet.',
52
50
  ];
53
51
 
54
52
  const parts = [
package/src/i18n.js CHANGED
@@ -100,6 +100,24 @@ function normalizeLanguage(language) {
100
100
  return 'en';
101
101
  }
102
102
 
103
+ function detectLanguage(text, fallback = 'en') {
104
+ const value = String(text || '').toLowerCase();
105
+ const fallbackLang = normalizeLanguage(fallback);
106
+
107
+ const spanishScore = [
108
+ /[áéíóúñ¿¡]/,
109
+ /\b(el|la|los|las|de|del|y|que|para|con|por|una|un|no|si|instala|instalar|haz|hace|agrega|añade|corrige|arregla|muestra|dime|busca|abre|cierra)\b/i,
110
+ ].reduce((score, re) => score + (re.test(value) ? 1 : 0), 0);
111
+
112
+ const englishScore = [
113
+ /\b(the|and|for|with|you|please|install|make|show|find|open|close|run|update|fix|create|give|need)\b/i,
114
+ ].reduce((score, re) => score + (re.test(value) ? 1 : 0), 0);
115
+
116
+ if (spanishScore > englishScore) return 'es';
117
+ if (englishScore > spanishScore) return 'en';
118
+ return fallbackLang;
119
+ }
120
+
103
121
  function languageLabel(language) {
104
122
  return LABELS[normalizeLanguage(language)] || LABELS.en;
105
123
  }
@@ -114,6 +132,7 @@ function t(language, key, params = {}) {
114
132
  }
115
133
 
116
134
  module.exports = {
135
+ detectLanguage,
117
136
  languageLabel,
118
137
  normalizeLanguage,
119
138
  t,
package/src/tui/app.mjs CHANGED
@@ -188,7 +188,7 @@ function useDimensions() {
188
188
 
189
189
  function parseInline(text) {
190
190
  const parts = [];
191
- const regex = /(\*\*(.+?)\*\*)|(`(.+?)`)/g;
191
+ const regex = /(\*\*(.+?)\*\*)|(`([^`]+?)`)|(\[([^\]]+)\]\(([^)]+)\))/g;
192
192
  let lastIndex = 0;
193
193
  let match;
194
194
  while ((match = regex.exec(text)) !== null) {
@@ -197,6 +197,7 @@ function parseInline(text) {
197
197
  }
198
198
  if (match[2]) parts.push({ t: 'bold', v: match[2] });
199
199
  else if (match[4]) parts.push({ t: 'code', v: match[4] });
200
+ else if (match[6] && match[7]) parts.push({ t: 'link', text: match[6], url: match[7] });
200
201
  lastIndex = regex.lastIndex;
201
202
  }
202
203
  if (lastIndex < text.length) {
@@ -213,6 +214,10 @@ function InlineLine({ text, color }) {
213
214
  ...parts.map((p, i) => {
214
215
  if (p.t === 'bold') return h(Text, { key: String(i), color: base, bold: true }, p.v);
215
216
  if (p.t === 'code') return h(Text, { key: String(i), color: T.cyan, backgroundColor: T.codeBg }, ' ' + p.v + ' ');
217
+ if (p.t === 'link') return h(Box, { key: String(i), flexWrap: 'wrap' },
218
+ h(Text, { color: T.accent, underline: true }, p.text),
219
+ h(Text, { color: T.textMuted }, ' (' + p.url + ')'),
220
+ );
216
221
  return h(Text, { key: String(i), color: base }, p.v);
217
222
  }),
218
223
  );
@@ -243,6 +248,29 @@ function parseMarkdownBlocks(text) {
243
248
  i++;
244
249
  continue;
245
250
  }
251
+ const hrMatch = line.match(/^\s*(---+|\*\*\*+)\s*$/);
252
+ if (hrMatch) {
253
+ blocks.push({ type: 'hr' });
254
+ i++;
255
+ continue;
256
+ }
257
+ const quoteMatch = line.match(/^\s*>\s?(.*)$/);
258
+ if (quoteMatch) {
259
+ blocks.push({ type: 'quote', text: quoteMatch[1] });
260
+ i++;
261
+ continue;
262
+ }
263
+ const tableCandidate = line.includes('|') && i + 1 < lines.length && /^\s*\|?\s*:?-{3,}/.test(lines[i + 1]);
264
+ if (tableCandidate) {
265
+ const table = [line];
266
+ i += 2;
267
+ while (i < lines.length && lines[i].includes('|') && lines[i].trim()) {
268
+ table.push(lines[i]);
269
+ i++;
270
+ }
271
+ blocks.push({ type: 'table', rows: table });
272
+ continue;
273
+ }
246
274
  const ulMatch = line.match(/^(\s*)[-*]\s+(.+)/);
247
275
  if (ulMatch) {
248
276
  blocks.push({ type: 'list', indent: Math.floor(ulMatch[1].length / 2), text: ulMatch[2] });
@@ -306,6 +334,25 @@ function MarkdownContent({ text, width }) {
306
334
  return block.text.trim()
307
335
  ? h(Box, { key: String(i) }, h(InlineLine, { text: block.text }))
308
336
  : h(Box, { key: String(i), height: 1 });
337
+ case 'quote':
338
+ return h(Box, { key: String(i), marginLeft: 1 },
339
+ h(Text, { color: T.textMuted }, '> '),
340
+ h(InlineLine, { text: block.text, color: T.textMuted }),
341
+ );
342
+ case 'hr':
343
+ return h(Text, { key: String(i), color: T.borderLight }, '─'.repeat(Math.max(10, Math.min(width || 80, 70))));
344
+ case 'table': {
345
+ const rows = block.rows.map(row => row.split('|').map(cell => cell.trim()).filter(Boolean));
346
+ const widthByCol = [];
347
+ for (const row of rows) {
348
+ row.forEach((cell, idx) => { widthByCol[idx] = Math.max(widthByCol[idx] || 0, cell.length); });
349
+ }
350
+ return h(Box, { key: String(i), flexDirection: 'column' },
351
+ ...rows.map((row, rowIdx) => h(Text, { key: String(rowIdx), color: rowIdx === 0 ? T.accent : T.textMuted },
352
+ row.map((cell, idx) => String(cell).padEnd(widthByCol[idx] || cell.length)).join(' | ')
353
+ )),
354
+ );
355
+ }
309
356
  default:
310
357
  return null;
311
358
  }
@@ -48,6 +48,8 @@ function buildSystemPrompt(repoOwner, repoName, fileTree, state = {}) {
48
48
  '- Do not ask "do you want" questions when you can investigate yourself.',
49
49
  '- Do not narrate plans instead of acting.',
50
50
  '- Use tools immediately when a file must be read, changed, or verified.',
51
+ '- Do not give a conclusion unless you have actually used a tool or verified the result.',
52
+ '- If you have not tested anything, say that clearly and keep investigating.',
51
53
  '',
52
54
  'Repository files:',
53
55
  treeLines,
@@ -105,6 +107,13 @@ function looksLikePendingEdit(text) {
105
107
  return PENDING_EDIT_RE.test(sample);
106
108
  }
107
109
 
110
+
111
+ function looksLikeActionRequest(text) {
112
+ const sample = normalizeClassifierText(String(text || ''));
113
+ if (!sample) return false;
114
+ return /(instala|instalar|install|run|ejecuta|ejecutar|crea|crear|build|compile|compila|fix|arregla|corrige|update|actualiza|edita|edit|borra|elimina|remove|descarga|download|busca|search|prueba|test|verifica|check|configura|setup|mueve|move|importa|import|aplica|apply)/i.test(sample);
115
+ }
116
+
108
117
  function buildForcedWritePrompt(state) {
109
118
  const path = state.lastReadPath || 'archivo-leido';
110
119
  const content = state.lastReadContent || '';
@@ -471,6 +480,7 @@ async function applyToolCall(parsed, answer, toolCtx, loopState, modelMessages)
471
480
  loopState.lastReadPath = parsed.args?.path || loopState.lastReadPath;
472
481
  }
473
482
 
483
+ loopState.toolCalls = (loopState.toolCalls || 0) + 1;
474
484
  const toolResult = await executeTool(parsed.tool, parsed.args, toolCtx);
475
485
 
476
486
  if (parsed.tool === 'read_file' && parsed.args?.path) {
@@ -714,6 +724,7 @@ async function runWebAgent({ chatData, user, onEvent, isAborted }) {
714
724
  readOrder: [],
715
725
  totalReads: 0,
716
726
  writesDone: 0,
727
+ toolCalls: 0,
717
728
  userWantsEdit,
718
729
  fileTree,
719
730
  };
@@ -960,6 +971,21 @@ async function runWebAgent({ chatData, user, onEvent, isAborted }) {
960
971
  continue;
961
972
  }
962
973
 
974
+ if (parsed.type === 'final' && looksLikeActionRequest(userLatest) && (loopState.toolCalls || 0) === 0) {
975
+ if (streamStarted) onEvent({ type: 'clear_stream' });
976
+ modelMessages.push({ role: 'assistant', content: answer });
977
+ modelMessages.push({
978
+ role: 'user',
979
+ content: [
980
+ 'No has usado ninguna herramienta todavia.',
981
+ 'No cierres con una conclusion ni con pasos teoricos.',
982
+ 'Primero intenta una herramienta real adecuada para la tarea.',
983
+ 'Si ninguna herramienta aplica, dilo de forma breve y honesta.',
984
+ ].join(' '),
985
+ });
986
+ continue;
987
+ }
988
+
963
989
  const fingerprint = normalizeReplyFingerprint(parsed.content || answer);
964
990
  if (fingerprint) {
965
991
  if (loopState.lastFingerprint === fingerprint) {