zyn-ai 1.3.3 → 1.3.5

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.
@@ -66,7 +66,7 @@ function buildSystemPrompt(cwd, state = {}, options = {}) {
66
66
 
67
67
  const languageInstructions = language === 'es'
68
68
  ? [
69
- 'Responde siempre en español.',
69
+ 'Responde en el idioma del ultimo mensaje del usuario (si es ambiguo, usa la preferencia de sesion).',
70
70
  'Ejecuta la tarea directamente. No des tutoriales ni instrucciones al usuario cuando puedas actuar tú mismo.',
71
71
  'Si hace falta, usa herramientas sin pedir permiso extra.',
72
72
  'Responde solo con el resultado final o con la siguiente accion concreta.',
@@ -76,15 +76,13 @@ function buildSystemPrompt(cwd, state = {}, options = {}) {
76
76
  'No cierres con una conclusion si todavia no has probado nada.',
77
77
  'Si una tarea dura demasiado, usa run_command con un timeoutMs adecuado y confirma el resultado real.',
78
78
  'Para operaciones de Git usa la herramienta git con action="api" o action="clone".',
79
- 'Usa exclusivamente tools registradas en "Tool use". No inventes nombres de tools ni aliases.',
80
- 'Mantente acotado y determinista: entradas claras, salidas claras, sin razonamiento creativo salvo que el usuario lo pida.',
81
- 'Cuando aplique, entrega resumen ejecutivo corto + riesgos/banderas rojas + siguiente accion concreta.',
82
- '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).',
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), subir artefactos si hace falta (upload_file), documentar (final).',
80
+ '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.',
83
81
  'No te limites a una sola tool por costumbre; elige la mejor secuencia técnica para el objetivo.',
84
82
  '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.',
85
83
  ]
86
84
  : [
87
- 'Always respond in English.',
85
+ 'Respond in the language of the user\'s latest message (if ambiguous, use the session preference).',
88
86
  'Execute the task directly. Do not give tutorials or instructions when you can act yourself.',
89
87
  'Use tools when needed without asking for extra permission.',
90
88
  'Reply only with the final result or the next concrete action.',
@@ -94,12 +92,55 @@ function buildSystemPrompt(cwd, state = {}, options = {}) {
94
92
  'Do not end with a conclusion if you have not tested anything yet.',
95
93
  'If a task takes long, use run_command with an appropriate timeoutMs and verify the real result.',
96
94
  'For Git operations use the git tool with action="api" or action="clone".',
97
- 'Use only tools listed under "Tool use". Never invent tool names or aliases.',
98
- 'Stay bounded and deterministic: clear inputs, clear outputs, no creative reasoning unless explicitly requested.',
99
- 'When relevant, provide a short executive summary + obvious red flags + next concrete action.',
100
- '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.',
95
+ '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.',
96
+ '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.',
101
97
  'Do not over-focus on a single tool by habit; choose the best technical sequence for the goal.',
102
98
  '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.',
99
+ 'Prioritize and reuse explicit user-provided context (requirements, constraints, repo instructions, preferences) across the full task; do not ignore it.',
100
+ ];
101
+
102
+ const toolUseEnforcement = language === 'es'
103
+ ? [
104
+ '',
105
+ '# Formato obligatorio de respuesta',
106
+ 'Solo existen DOS formatos de respuesta. NO inventes otros:',
107
+ '',
108
+ 'FORMATO 1 — Para USAR una herramienta:',
109
+ '{"type":"tool","tool":"NOMBRE_EXACTO","args":{"clave":"valor"}}',
110
+ '',
111
+ 'FORMATO 2 — Para responder AL USUARIO:',
112
+ '{"type":"final","content":"Tu respuesta aqui"}',
113
+ '',
114
+ 'REGLAS ESTRICTAS:',
115
+ '- USA EXCLUSIVAMENTE los nombres de herramientas listados en "# Tool use".',
116
+ '- NO inventes herramientas como "code_interpreter", "python", "bash", "shell", etc.',
117
+ '- NO uses formatos como <invoke>, function calls, ni tool_use de otros sistemas.',
118
+ '- Si una herramienta falla, INTENTA con otra herramienta diferente.',
119
+ '- Si una herramienta falla 2 VECES seguidas, no la repitas. Cambia de estrategia o usa type=final.',
120
+ '- LIMITE: Maximo 8 herramientas por turno. Despues de 8 pasos, responde con type=final.',
121
+ '- Si no puedes completar la tarea, responde con type=final explicando honestamente por que.',
122
+ '- Cada respuesta debe ser UNICAMENTE el JSON. Sin texto antes ni despues.',
123
+ ]
124
+ : [
125
+ '',
126
+ '# Strict response format',
127
+ 'Only TWO response formats are allowed. Do NOT use others:',
128
+ '',
129
+ 'FORMAT 1 — To USE a tool:',
130
+ '{"type":"tool","tool":"EXACT_NAME","args":{"key":"value"}}',
131
+ '',
132
+ 'FORMAT 2 — To REPLY to user:',
133
+ '{"type":"final","content":"Your answer here"}',
134
+ '',
135
+ 'STRICT RULES:',
136
+ '- ONLY use tool names listed in "# Tool use".',
137
+ '- Do NOT invent tools like "code_interpreter", "python", "bash", "shell", etc.',
138
+ '- Do NOT use <invoke>, function call, or tool_use formats from other systems.',
139
+ '- If a tool fails, TRY a different tool instead.',
140
+ '- If a tool fails 2 TIMES in a row, do not repeat it. Change strategy or use type=final.',
141
+ '- LIMIT: Maximum 8 tools per turn. After 8 steps, respond with type=final.',
142
+ '- If you cannot complete the task, use type=final and honestly explain why.',
143
+ '- Each response must be ONLY the JSON. No text before or after.',
103
144
  ];
104
145
 
105
146
  const parts = [
@@ -107,6 +148,7 @@ function buildSystemPrompt(cwd, state = {}, options = {}) {
107
148
  '',
108
149
  '# Tool use',
109
150
  getToolPromptText(),
151
+ ...toolUseEnforcement,
110
152
  '',
111
153
  '# Environment',
112
154
  `- Working directory: ${cwd}`,
@@ -220,6 +262,9 @@ function classifyParsed(parsed) {
220
262
  if (parsed?.type === 'final') {
221
263
  return { type: 'final', content: typeof parsed.content === 'string' ? parsed.content : '' };
222
264
  }
265
+ if (parsed?.tool && KNOWN_TOOLS.has(parsed.tool)) {
266
+ return { type: 'tool', tool: parsed.tool, args: parsed.args ?? {} };
267
+ }
223
268
  return null;
224
269
  }
225
270
 
@@ -241,6 +286,8 @@ const TOOL_ARG_KEYS = {
241
286
  scrape_site: ['url', 'selectors', 'limit', 'headers'],
242
287
  web_search: ['query', 'lang', 'limit'],
243
288
  web_read: ['url'],
289
+ upload_file: ['path', 'field', 'name', 'type'],
290
+ gmail: ['action', 'query', 'maxResults', 'id', 'to', 'subject', 'body'],
244
291
  create_canvas_image: ['width', 'height', 'background', 'elements', 'format', 'outputPath'],
245
292
  git: ['provider', 'action', 'method', 'path', 'body', 'headers', 'name', 'repoUrl', 'destination', 'branch', 'timeoutMs'],
246
293
  };
@@ -252,8 +299,40 @@ const LONG_VALUE_ARG = {
252
299
  replace_in_file: 'replace',
253
300
  };
254
301
 
302
+
303
+ function extractMalformedFinalContent(text) {
304
+ const typeMatch = text.match(/(?:["'])type(?:["'])\s*:\s*(?:["'])final(?:["'])/i);
305
+ if (!typeMatch) return null;
306
+
307
+ const contentMatch = text.match(/(?:["'])content(?:["'])\s*:\s*(["'])/i);
308
+ if (!contentMatch) return null;
309
+
310
+ const quote = contentMatch[1];
311
+ const start = contentMatch.index + contentMatch[0].length;
312
+ let esc = false;
313
+
314
+ for (let i = start; i < text.length; i += 1) {
315
+ const ch = text[i];
316
+ if (esc) { esc = false; continue; }
317
+ if (ch === '\\') { esc = true; continue; }
318
+ if (ch !== quote) continue;
319
+
320
+ const tail = text.slice(i + 1).trim();
321
+ if (!tail || /^}\s*$/.test(tail) || /^,\s*(["']\w+["']\s*:|})/.test(tail)) {
322
+ return unescapeJsonString(text.slice(start, i)).trim();
323
+ }
324
+ }
325
+
326
+ const lastQuote = text.lastIndexOf(quote);
327
+ if (lastQuote > start) {
328
+ return unescapeJsonString(text.slice(start, lastQuote)).trim();
329
+ }
330
+
331
+ return null;
332
+ }
333
+
255
334
  function fuzzyExtractTool(text) {
256
- const toolMatch = text.match(/"tool"\s*:\s*"(\w+)"/);
335
+ const toolMatch = text.match(/(?:"|')?tool(?:"|')?\s*:\s*"(\w+)"/i);
257
336
  if (!toolMatch) return null;
258
337
 
259
338
  const tool = toolMatch[1];
@@ -284,32 +363,57 @@ function findStringEnd(text, start) {
284
363
  return -1;
285
364
  }
286
365
 
366
+ function extractArgsContext(text) {
367
+ const argsMatch = text.match(/(?:"|')?args(?:"|')?\s*:\s*\{/i);
368
+ if (!argsMatch) return text;
369
+
370
+ let depth = 1;
371
+ let inStr = false;
372
+ let esc = false;
373
+ const start = argsMatch.index + argsMatch[0].length - 1;
374
+
375
+ for (let i = start + 1; i < text.length; i++) {
376
+ const ch = text[i];
377
+ if (esc) { esc = false; continue; }
378
+ if (ch === '\\' && inStr) { esc = true; continue; }
379
+ if (ch === '"') { inStr = !inStr; continue; }
380
+ if (inStr) continue;
381
+ if (ch === '{') depth++;
382
+ if (ch === '}') {
383
+ depth--;
384
+ if (depth === 0) return text.slice(start, i + 1);
385
+ }
386
+ }
387
+ return text;
388
+ }
389
+
287
390
  function extractLongValueTool(text, tool, longArg) {
391
+ const context = extractArgsContext(text);
288
392
  const args = {};
289
393
  const keys = TOOL_ARG_KEYS[tool] || [];
290
394
 
291
395
  for (const key of keys) {
292
396
  if (key === longArg) continue;
293
- const m = text.match(new RegExp(`"${key}"\\s*:\\s*"([^"]*?)"`));
397
+ const m = context.match(new RegExp(`(?:"|')?${key}(?:"|')?\\s*:\\s*"([^"]*?)"`));
294
398
  if (m) args[key] = unescapeJsonString(m[1]);
295
- const bm = text.match(new RegExp(`"${key}"\\s*:\\s*(true|false|\\d+)`));
399
+ const bm = context.match(new RegExp(`(?:"|')?${key}(?:"|')?\\s*:\\s*(true|false|\\d+)`));
296
400
  if (bm) args[key] = bm[1] === 'true' ? true : bm[1] === 'false' ? false : Number(bm[1]);
297
401
  }
298
402
 
299
- const marker = `"${longArg}"`;
300
- const argIdx = text.indexOf(marker);
301
- if (argIdx === -1) return null;
403
+ const longKeyRe = new RegExp(`(?:"|')?${longArg}(?:"|')?\\s*:`);
404
+ const longMatch = context.match(longKeyRe);
405
+ if (!longMatch) return null;
302
406
 
303
- let i = text.indexOf(':', argIdx + marker.length);
304
- if (i === -1) return null;
305
- i = text.indexOf('"', i);
306
- if (i === -1) return null;
307
- const valStart = i + 1;
407
+ const colonPos = context.indexOf(':', longMatch.index + longMatch[0].length - 1);
408
+ if (colonPos === -1) return null;
409
+ const quotePos = context.indexOf('"', colonPos);
410
+ if (quotePos === -1) return null;
411
+ const valStart = quotePos + 1;
308
412
 
309
- const valEnd = findStringEnd(text, valStart);
413
+ const valEnd = findStringEnd(context, valStart);
310
414
  if (valEnd === -1 || valEnd <= valStart) return null;
311
415
 
312
- const value = text.slice(valStart, valEnd);
416
+ const value = context.slice(valStart, valEnd);
313
417
  if (!value.trim()) return null;
314
418
 
315
419
  args[longArg] = unescapeJsonString(value);
@@ -321,9 +425,9 @@ function extractSimpleArgsTool(text, tool) {
321
425
  const keys = TOOL_ARG_KEYS[tool] || [];
322
426
 
323
427
  for (const key of keys) {
324
- const strM = text.match(new RegExp(`"${key}"\\s*:\\s*"([^"]*?)"`));
428
+ const strM = text.match(new RegExp(`(?:"|')?${key}(?:"|')?\\s*:\\s*"([^"]*?)"`));
325
429
  if (strM) { args[key] = unescapeJsonString(strM[1]); continue; }
326
- const numM = text.match(new RegExp(`"${key}"\\s*:\\s*(true|false|\\d+)`));
430
+ const numM = text.match(new RegExp(`(?:"|')?${key}(?:"|')?\\s*:\\s*(true|false|\\d+)`));
327
431
  if (numM) {
328
432
  const v = numM[1];
329
433
  args[key] = v === 'true' ? true : v === 'false' ? false : Number(v);
@@ -355,6 +459,13 @@ function parseAgentResponse(raw) {
355
459
  const tool = extractToolJson(text);
356
460
  if (tool) return { type: 'tool', tool: tool.tool, args: tool.args ?? {} };
357
461
 
462
+ const malformedFinalContent = extractMalformedFinalContent(text);
463
+ if (malformedFinalContent !== null) {
464
+ const embedded = extractToolJson(malformedFinalContent);
465
+ if (embedded) return { type: 'tool', tool: embedded.tool, args: embedded.args ?? {} };
466
+ return { type: 'final', content: malformedFinalContent };
467
+ }
468
+
358
469
  const xmlTool = extractXmlTool(text);
359
470
  if (xmlTool) return xmlTool;
360
471
 
@@ -364,7 +475,7 @@ function parseAgentResponse(raw) {
364
475
  const fuzzy = fuzzyExtractTool(text);
365
476
  if (fuzzy) return fuzzy;
366
477
 
367
- return { type: 'final', content: text || raw.trim() };
478
+ return { type: 'final', content: text || (raw ? String(raw).trim() : '') };
368
479
  }
369
480
 
370
481
  function sanitizeArgsForModel(parsed) {
@@ -402,11 +513,15 @@ function buildConversationMessages(state, turnMessages, systemPrompt) {
402
513
  }
403
514
 
404
515
  function buildToolResultMessage(parsed, result) {
516
+ const maxResultChars = 8000;
517
+ const truncatedResult = typeof result === 'string' && result.length > maxResultChars
518
+ ? `${result.slice(0, maxResultChars)}\n... [resultado truncado, ${result.length} caracteres totales]`
519
+ : result;
405
520
  return [
406
521
  `Herramienta: ${parsed.tool}`,
407
522
  `Argumentos: ${JSON.stringify(sanitizeArgsForModel(parsed), null, 2)}`,
408
523
  'Resultado:',
409
- result,
524
+ truncatedResult,
410
525
  '',
411
526
  'Responde con la siguiente accion concreta o con el resultado final.',
412
527
  ].join('\n');
@@ -414,9 +529,10 @@ function buildToolResultMessage(parsed, result) {
414
529
 
415
530
  function buildToolErrorMessage(parsed, errorMessage) {
416
531
  return [
417
- `La herramienta ${parsed.tool} fallo.`,
532
+ `La herramienta "${parsed.tool}" NO existe.`,
418
533
  `Error: ${errorMessage}`,
419
- 'Corrige la llamada o explica brevemente el problema si no puedes continuar.',
534
+ `Las unicas herramientas disponibles son: ${TOOL_DEFINITIONS.map(t => t.name).join(', ')}.`,
535
+ 'Elige UNA de esas herramientas. Usa el formato exacto del nombre. No inventes herramientas.',
420
536
  ].join('\n');
421
537
  }
422
538
 
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.',
@@ -1,6 +1,6 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
- const { MODELS_FILE, PROVIDERS_FILE, REQUEST_TIMEOUT_MS } = require('../config');
3
+ const { MODELS_FILE, PROVIDERS_FILE, REQUEST_TIMEOUT_MS, SUPPORTED_MODEL_PROVIDERS } = require('../config');
4
4
 
5
5
  const DEFAULT_HEADERS = {
6
6
  'Content-Type': 'application/json',
@@ -108,13 +108,17 @@ function loadExternalModels() {
108
108
  if (Array.isArray(raw)) {
109
109
  const output = {};
110
110
  for (const item of raw) {
111
- if (!item?.key) continue;
111
+ if (!item?.key || !SUPPORTED_MODEL_PROVIDERS.has(item.provider)) continue;
112
112
  output[item.key] = item;
113
113
  }
114
114
  return output;
115
115
  }
116
- if (raw.models && typeof raw.models === 'object') return raw.models;
117
- return raw && typeof raw === 'object' ? raw : {};
116
+ const rawModels = raw.models && typeof raw.models === 'object'
117
+ ? raw.models
118
+ : (raw && typeof raw === 'object' ? raw : {});
119
+ return Object.fromEntries(
120
+ Object.entries(rawModels).filter(([, model]) => SUPPORTED_MODEL_PROVIDERS.has(model?.provider)),
121
+ );
118
122
  }
119
123
 
120
124
  function saveExternalModels(models) {
@@ -165,43 +169,6 @@ function buildModelRecord(providerKey, config, modelId, label, extra = {}) {
165
169
  return record;
166
170
  }
167
171
 
168
- async function fetchOllamaModels(config) {
169
- const baseUrl = normalizeBaseUrl(config.baseUrl || 'http://127.0.0.1:11434');
170
- const data = await fetchJson(`${baseUrl}/api/tags`, {
171
- headers: config.apiKey ? { Authorization: `Bearer ${config.apiKey}` } : {},
172
- });
173
- const models = Array.isArray(data?.models) ? data.models : [];
174
- return models.map(model => buildModelRecord(
175
- 'ollama',
176
- { ...config, baseUrl },
177
- model.name || model.model || model.id,
178
- model.name || model.model || model.id,
179
- {
180
- ollamaModel: model.name || model.model || model.id,
181
- raw: model,
182
- },
183
- )).filter(item => item.modelId);
184
- }
185
-
186
- async function fetchOpenAICompatibleModels(config) {
187
- const baseUrl = normalizeBaseUrl(config.baseUrl);
188
- if (!baseUrl) throw new Error('Falta baseUrl para openai-compatible');
189
- const data = await fetchJson(`${baseUrl}/v1/models`, {
190
- headers: config.apiKey ? { Authorization: `Bearer ${config.apiKey}` } : {},
191
- });
192
- const models = Array.isArray(data?.data) ? data.data : Array.isArray(data?.models) ? data.models : [];
193
- return models.map(model => buildModelRecord(
194
- 'openai-compatible',
195
- { ...config, baseUrl },
196
- model.id || model.name,
197
- model.id || model.name,
198
- {
199
- openaiModel: model.id || model.name,
200
- raw: model,
201
- },
202
- )).filter(item => item.modelId);
203
- }
204
-
205
172
  async function fetchZenModels(config) {
206
173
  const baseUrl = normalizeBaseUrl(config.baseUrl || 'https://opencode.ai/zen');
207
174
  const data = await fetchJson(`${baseUrl}/v1/models`, {
@@ -238,10 +205,27 @@ async function fetchQwenModels(config) {
238
205
  ));
239
206
  }
240
207
 
208
+
209
+ async function fetchGeminiModels(config) {
210
+ const models = [
211
+ { id: 'gemini-flash', label: 'Gemini Flash' },
212
+ ];
213
+ return models.map(model => buildModelRecord(
214
+ 'gemini',
215
+ config,
216
+ model.id,
217
+ model.label,
218
+ {
219
+ geminiModel: model.id,
220
+ static: true,
221
+ },
222
+ ));
223
+ }
224
+
241
225
  async function fetchProviderModels(providerKey, config = {}) {
242
226
  const key = String(providerKey || '').trim();
243
- if (key === 'ollama') return fetchOllamaModels(config);
244
- if (key === 'openai-compatible') return fetchOpenAICompatibleModels(config);
227
+ if (!SUPPORTED_MODEL_PROVIDERS.has(key)) throw new Error(`Proveedor no soportado: ${key}`);
228
+ if (key === 'gemini') return fetchGeminiModels(config);
245
229
  if (key === 'zen') return fetchZenModels(config);
246
230
  if (key === 'qwen') return fetchQwenModels(config);
247
231
  throw new Error(`Proveedor no soportado: ${key}`);