zyn-ai 1.3.2 → 1.3.4

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.
@@ -3,20 +3,56 @@ const { buildSkillsPrompt } = require('./skills');
3
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
+ const os = require('os');
7
+ const fs = require('fs');
8
+
9
+ function getPlatformInfo() {
10
+ // Detectar SO real primero
11
+ let osName = 'Unknown';
12
+
13
+ if (process.platform === 'linux') {
14
+ try {
15
+ const release = fs.readFileSync('/etc/os-release', 'utf8');
16
+ const nameMatch = release.match(/^PRETTY_NAME="?([^"\n]+)"?/m);
17
+ if (nameMatch) {
18
+ osName = nameMatch[1];
19
+ } else {
20
+ const idMatch = release.match(/^ID="?([^"\n]+)"?/m);
21
+ const verMatch = release.match(/^VERSION_ID="?([^"\n]+)"?/m);
22
+ osName = `Linux ${idMatch ? idMatch[1] : ''}${verMatch ? ' ' + verMatch[1] : ''}`.trim();
23
+ }
24
+ } catch {
25
+ try {
26
+ const { execSync } = require('child_process');
27
+ osName = execSync('uname -o -r -m', { encoding: 'utf8', timeout: 3000 }).trim();
28
+ } catch {
29
+ osName = `Linux ${os.release()} ${os.arch()}`;
30
+ }
31
+ }
32
+ } else if (process.platform === 'darwin') {
33
+ try {
34
+ const { execSync } = require('child_process');
35
+ const ver = execSync('sw_vers -productVersion', { encoding: 'utf8', timeout: 3000 }).trim();
36
+ osName = `macOS ${ver} (${os.arch()})`;
37
+ } catch {
38
+ osName = `macOS ${os.release()} (${os.arch()})`;
39
+ }
40
+ } else if (process.platform === 'win32') {
41
+ osName = `Windows ${os.release()} (${os.arch()})`;
42
+ }
43
+
44
+ return osName;
45
+ }
6
46
 
7
47
  const KNOWN_TOOLS = new Set([
8
48
  ...TOOL_DEFINITIONS.map(tool => tool.name),
9
49
  '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',
12
50
  ]);
13
51
 
14
52
 
15
53
  function buildSystemPrompt(cwd, state = {}, options = {}) {
16
54
  const language = normalizeLanguage(options.language || state.language || detectLanguage(options.input || '', state.language));
17
- const platform = process.platform === 'linux' ? 'Linux'
18
- : process.platform === 'darwin' ? 'macOS'
19
- : process.platform;
55
+ const platform = getPlatformInfo();
20
56
  const date = new Date().toLocaleDateString(language === 'es' ? 'es-ES' : 'en-US', {
21
57
  year: 'numeric',
22
58
  month: 'long',
@@ -39,10 +75,7 @@ function buildSystemPrompt(cwd, state = {}, options = {}) {
39
75
  'Si la tarea requiere comprobar algo, primero intenta una herramienta real y espera el resultado antes de concluir.',
40
76
  'No cierres con una conclusion si todavia no has probado nada.',
41
77
  '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.',
78
+ 'Para operaciones de Git usa la herramienta git con action="api" o action="clone".',
46
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).',
47
80
  'No te limites a una sola tool por costumbre; elige la mejor secuencia técnica para el objetivo.',
48
81
  '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.',
@@ -57,20 +90,62 @@ function buildSystemPrompt(cwd, state = {}, options = {}) {
57
90
  'If the task requires verification, try a real tool first and wait for its result before concluding.',
58
91
  'Do not end with a conclusion if you have not tested anything yet.',
59
92
  '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.',
93
+ 'For Git operations use the git tool with action="api" or action="clone".',
64
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.',
65
95
  'Do not over-focus on a single tool by habit; choose the best technical sequence for the goal.',
66
96
  '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.',
67
97
  ];
68
98
 
99
+ const toolUseEnforcement = language === 'es'
100
+ ? [
101
+ '',
102
+ '# Formato obligatorio de respuesta',
103
+ 'Solo existen DOS formatos de respuesta. NO inventes otros:',
104
+ '',
105
+ 'FORMATO 1 — Para USAR una herramienta:',
106
+ '{"type":"tool","tool":"NOMBRE_EXACTO","args":{"clave":"valor"}}',
107
+ '',
108
+ 'FORMATO 2 — Para responder AL USUARIO:',
109
+ '{"type":"final","content":"Tu respuesta aqui"}',
110
+ '',
111
+ 'REGLAS ESTRICTAS:',
112
+ '- USA EXCLUSIVAMENTE los nombres de herramientas listados en "# Tool use".',
113
+ '- NO inventes herramientas como "code_interpreter", "python", "bash", "shell", etc.',
114
+ '- NO uses formatos como <invoke>, function calls, ni tool_use de otros sistemas.',
115
+ '- Si una herramienta falla, INTENTA con otra herramienta diferente.',
116
+ '- Si una herramienta falla 2 VECES seguidas, no la repitas. Cambia de estrategia o usa type=final.',
117
+ '- LIMITE: Maximo 8 herramientas por turno. Despues de 8 pasos, responde con type=final.',
118
+ '- Si no puedes completar la tarea, responde con type=final explicando honestamente por que.',
119
+ '- Cada respuesta debe ser UNICAMENTE el JSON. Sin texto antes ni despues.',
120
+ ]
121
+ : [
122
+ '',
123
+ '# Strict response format',
124
+ 'Only TWO response formats are allowed. Do NOT use others:',
125
+ '',
126
+ 'FORMAT 1 — To USE a tool:',
127
+ '{"type":"tool","tool":"EXACT_NAME","args":{"key":"value"}}',
128
+ '',
129
+ 'FORMAT 2 — To REPLY to user:',
130
+ '{"type":"final","content":"Your answer here"}',
131
+ '',
132
+ 'STRICT RULES:',
133
+ '- ONLY use tool names listed in "# Tool use".',
134
+ '- Do NOT invent tools like "code_interpreter", "python", "bash", "shell", etc.',
135
+ '- Do NOT use <invoke>, function call, or tool_use formats from other systems.',
136
+ '- If a tool fails, TRY a different tool instead.',
137
+ '- If a tool fails 2 TIMES in a row, do not repeat it. Change strategy or use type=final.',
138
+ '- LIMIT: Maximum 8 tools per turn. After 8 steps, respond with type=final.',
139
+ '- If you cannot complete the task, use type=final and honestly explain why.',
140
+ '- Each response must be ONLY the JSON. No text before or after.',
141
+ ];
142
+
69
143
  const parts = [
70
144
  skills,
71
145
  '',
72
146
  '# Tool use',
73
147
  getToolPromptText(),
148
+ ...toolUseEnforcement,
74
149
  '',
75
150
  '# Environment',
76
151
  `- Working directory: ${cwd}`,
@@ -184,6 +259,9 @@ function classifyParsed(parsed) {
184
259
  if (parsed?.type === 'final') {
185
260
  return { type: 'final', content: typeof parsed.content === 'string' ? parsed.content : '' };
186
261
  }
262
+ if (parsed?.tool && KNOWN_TOOLS.has(parsed.tool)) {
263
+ return { type: 'tool', tool: parsed.tool, args: parsed.args ?? {} };
264
+ }
187
265
  return null;
188
266
  }
189
267
 
@@ -206,6 +284,7 @@ const TOOL_ARG_KEYS = {
206
284
  web_search: ['query', 'lang', 'limit'],
207
285
  web_read: ['url'],
208
286
  create_canvas_image: ['width', 'height', 'background', 'elements', 'format', 'outputPath'],
287
+ git: ['provider', 'action', 'method', 'path', 'body', 'headers', 'name', 'repoUrl', 'destination', 'branch', 'timeoutMs'],
209
288
  };
210
289
 
211
290
  const LONG_VALUE_ARG = {
@@ -216,7 +295,7 @@ const LONG_VALUE_ARG = {
216
295
  };
217
296
 
218
297
  function fuzzyExtractTool(text) {
219
- const toolMatch = text.match(/"tool"\s*:\s*"(\w+)"/);
298
+ const toolMatch = text.match(/(?:"|')?tool(?:"|')?\s*:\s*"(\w+)"/i);
220
299
  if (!toolMatch) return null;
221
300
 
222
301
  const tool = toolMatch[1];
@@ -247,32 +326,57 @@ function findStringEnd(text, start) {
247
326
  return -1;
248
327
  }
249
328
 
329
+ function extractArgsContext(text) {
330
+ const argsMatch = text.match(/(?:"|')?args(?:"|')?\s*:\s*\{/i);
331
+ if (!argsMatch) return text;
332
+
333
+ let depth = 1;
334
+ let inStr = false;
335
+ let esc = false;
336
+ const start = argsMatch.index + argsMatch[0].length - 1;
337
+
338
+ for (let i = start + 1; i < text.length; i++) {
339
+ const ch = text[i];
340
+ if (esc) { esc = false; continue; }
341
+ if (ch === '\\' && inStr) { esc = true; continue; }
342
+ if (ch === '"') { inStr = !inStr; continue; }
343
+ if (inStr) continue;
344
+ if (ch === '{') depth++;
345
+ if (ch === '}') {
346
+ depth--;
347
+ if (depth === 0) return text.slice(start, i + 1);
348
+ }
349
+ }
350
+ return text;
351
+ }
352
+
250
353
  function extractLongValueTool(text, tool, longArg) {
354
+ const context = extractArgsContext(text);
251
355
  const args = {};
252
356
  const keys = TOOL_ARG_KEYS[tool] || [];
253
357
 
254
358
  for (const key of keys) {
255
359
  if (key === longArg) continue;
256
- const m = text.match(new RegExp(`"${key}"\\s*:\\s*"([^"]*?)"`));
360
+ const m = context.match(new RegExp(`(?:"|')?${key}(?:"|')?\\s*:\\s*"([^"]*?)"`));
257
361
  if (m) args[key] = unescapeJsonString(m[1]);
258
- const bm = text.match(new RegExp(`"${key}"\\s*:\\s*(true|false|\\d+)`));
362
+ const bm = context.match(new RegExp(`(?:"|')?${key}(?:"|')?\\s*:\\s*(true|false|\\d+)`));
259
363
  if (bm) args[key] = bm[1] === 'true' ? true : bm[1] === 'false' ? false : Number(bm[1]);
260
364
  }
261
365
 
262
- const marker = `"${longArg}"`;
263
- const argIdx = text.indexOf(marker);
264
- if (argIdx === -1) return null;
366
+ const longKeyRe = new RegExp(`(?:"|')?${longArg}(?:"|')?\\s*:`);
367
+ const longMatch = context.match(longKeyRe);
368
+ if (!longMatch) return null;
265
369
 
266
- let i = text.indexOf(':', argIdx + marker.length);
267
- if (i === -1) return null;
268
- i = text.indexOf('"', i);
269
- if (i === -1) return null;
270
- const valStart = i + 1;
370
+ const colonPos = context.indexOf(':', longMatch.index + longMatch[0].length - 1);
371
+ if (colonPos === -1) return null;
372
+ const quotePos = context.indexOf('"', colonPos);
373
+ if (quotePos === -1) return null;
374
+ const valStart = quotePos + 1;
271
375
 
272
- const valEnd = findStringEnd(text, valStart);
376
+ const valEnd = findStringEnd(context, valStart);
273
377
  if (valEnd === -1 || valEnd <= valStart) return null;
274
378
 
275
- const value = text.slice(valStart, valEnd);
379
+ const value = context.slice(valStart, valEnd);
276
380
  if (!value.trim()) return null;
277
381
 
278
382
  args[longArg] = unescapeJsonString(value);
@@ -284,9 +388,9 @@ function extractSimpleArgsTool(text, tool) {
284
388
  const keys = TOOL_ARG_KEYS[tool] || [];
285
389
 
286
390
  for (const key of keys) {
287
- const strM = text.match(new RegExp(`"${key}"\\s*:\\s*"([^"]*?)"`));
391
+ const strM = text.match(new RegExp(`(?:"|')?${key}(?:"|')?\\s*:\\s*"([^"]*?)"`));
288
392
  if (strM) { args[key] = unescapeJsonString(strM[1]); continue; }
289
- const numM = text.match(new RegExp(`"${key}"\\s*:\\s*(true|false|\\d+)`));
393
+ const numM = text.match(new RegExp(`(?:"|')?${key}(?:"|')?\\s*:\\s*(true|false|\\d+)`));
290
394
  if (numM) {
291
395
  const v = numM[1];
292
396
  args[key] = v === 'true' ? true : v === 'false' ? false : Number(v);
@@ -327,7 +431,7 @@ function parseAgentResponse(raw) {
327
431
  const fuzzy = fuzzyExtractTool(text);
328
432
  if (fuzzy) return fuzzy;
329
433
 
330
- return { type: 'final', content: text || raw.trim() };
434
+ return { type: 'final', content: text || (raw ? String(raw).trim() : '') };
331
435
  }
332
436
 
333
437
  function sanitizeArgsForModel(parsed) {
@@ -365,11 +469,15 @@ function buildConversationMessages(state, turnMessages, systemPrompt) {
365
469
  }
366
470
 
367
471
  function buildToolResultMessage(parsed, result) {
472
+ const maxResultChars = 8000;
473
+ const truncatedResult = typeof result === 'string' && result.length > maxResultChars
474
+ ? `${result.slice(0, maxResultChars)}\n... [resultado truncado, ${result.length} caracteres totales]`
475
+ : result;
368
476
  return [
369
477
  `Herramienta: ${parsed.tool}`,
370
478
  `Argumentos: ${JSON.stringify(sanitizeArgsForModel(parsed), null, 2)}`,
371
479
  'Resultado:',
372
- result,
480
+ truncatedResult,
373
481
  '',
374
482
  'Responde con la siguiente accion concreta o con el resultado final.',
375
483
  ].join('\n');
@@ -377,9 +485,10 @@ function buildToolResultMessage(parsed, result) {
377
485
 
378
486
  function buildToolErrorMessage(parsed, errorMessage) {
379
487
  return [
380
- `La herramienta ${parsed.tool} fallo.`,
488
+ `La herramienta "${parsed.tool}" NO existe.`,
381
489
  `Error: ${errorMessage}`,
382
- 'Corrige la llamada o explica brevemente el problema si no puedes continuar.',
490
+ `Las unicas herramientas disponibles son: ${TOOL_DEFINITIONS.map(t => t.name).join(', ')}.`,
491
+ 'Elige UNA de esas herramientas. Usa el formato exacto del nombre. No inventes herramientas.',
383
492
  ].join('\n');
384
493
  }
385
494