zyn-ai 1.2.0 → 1.3.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/README.md CHANGED
@@ -1,4 +1,4 @@
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" />
package/package.json CHANGED
@@ -1,18 +1,32 @@
1
1
  {
2
2
  "name": "zyn-ai",
3
- "version": "1.2.0",
4
- "description": "Professional local terminal and web agent",
5
- "author": "Maycol y Ado",
3
+ "version": "1.3.0",
4
+ "description": "Production-ready AI agent for CLI and web with real tool execution and automation",
5
+ "author": "Maycol",
6
+ "keywords": [
7
+ "ai-agent",
8
+ "cli-ai",
9
+ "terminal-ai",
10
+ "automation-agent",
11
+ "developer-tools",
12
+ "ai-cli",
13
+ "nodejs-cli",
14
+ "productivity-tool",
15
+ "local-ai-agent",
16
+ "command-line-ai",
17
+ "web-ai-agent",
18
+ "zyn"
19
+ ],
6
20
  "bin": {
7
- "Zyn": "./zyn.js",
8
- "zyn": "./zyn.js"
21
+ "zyn": "./zyn.js",
22
+ "Zyn": "./zyn.js"
9
23
  },
10
24
  "type": "commonjs",
11
25
  "scripts": {
12
- "start": "node ./zyn.js",
13
- "web": "node ./src/web/server.js",
14
- "test": "node ./zyn.js test",
15
- "check": "node --check ./zyn.js && node --check ./src/cli/runtime.js"
26
+ "start": "node zyn.js",
27
+ "dev": "node zyn.js",
28
+ "web": "node src/web/server.js",
29
+ "check": "node --check zyn.js && node --check src/cli/runtime.js"
16
30
  },
17
31
  "dependencies": {
18
32
  "axios": "^1.7.9",
@@ -23,11 +37,29 @@
23
37
  "ink": "^6.8.0",
24
38
  "keypress": "^0.2.1",
25
39
  "react": "^19.0.0",
26
- "session-file-store": "^1.5.0"
40
+ "session-file-store": "^1.5.0",
41
+ "jimp": "^1.6.1"
42
+ },
43
+ "repository": {
44
+ "type": "git",
45
+ "url": "https://github.com/SoyMaycol/Zyn"
46
+ },
47
+ "homepage": "https://github.com/SoyMaycol/Zyn#readme",
48
+ "bugs": {
49
+ "url": "https://github.com/SoyMaycol/Zyn/issues"
27
50
  },
28
51
  "engines": {
29
52
  "node": ">=18"
30
53
  },
31
- "license": "SEE LICENSE IN LICENSE",
54
+ "license": "MIT",
55
+ "files": [
56
+ "zyn.js",
57
+ "src/",
58
+ "README.md",
59
+ "LICENSE"
60
+ ],
61
+ "exports": {
62
+ ".": "./zyn.js"
63
+ },
32
64
  "preferGlobal": true
33
65
  }
@@ -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,7 @@ 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' },
27
29
  { name: 'lang', desc: 'change language' },
28
30
  { name: 'language', desc: 'change language' },
29
31
  { name: 'auto', desc: 'auto-approval' },
@@ -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) {
@@ -175,7 +176,7 @@ async function runInteractiveChatClassic(options = {}) {
175
176
  if (!input) continue;
176
177
 
177
178
  if (input === '/exit' || input === '/quit') {
178
- logEvent(state, 'info', 'Hasta luego');
179
+ logEvent(state, 'info', t(state.language, 'goodbye'));
179
180
  break;
180
181
  }
181
182
 
@@ -202,7 +203,7 @@ async function runInteractiveChatClassic(options = {}) {
202
203
  rl.removeListener('line', lineHandler);
203
204
 
204
205
  if (pendingExit) {
205
- logEvent(state, 'info', 'Hasta luego');
206
+ logEvent(state, 'info', t(state.language, 'goodbye'));
206
207
  break;
207
208
  }
208
209
  }
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;
package/src/core/agent.js CHANGED
@@ -197,6 +197,9 @@ async function runAgentTurn(input, state, ui, options = {}) {
197
197
  }
198
198
  ui.logEvent(state, 'info', `${state.language === 'es' ? 'Turno' : 'Turn'} ${state.turnCount}`);
199
199
 
200
+ let toolUsedThisTurn = false;
201
+ let finalWithoutToolRetries = 0;
202
+
200
203
  const directAction = parseDirectAction(input);
201
204
  if (directAction) {
202
205
  await appendTranscriptEntry(state.sessionId, { type: 'user', content: input });
@@ -227,8 +230,7 @@ async function runAgentTurn(input, state, ui, options = {}) {
227
230
  let repeatCount = 0;
228
231
  let step = 0;
229
232
  const turnLanguage = detectLanguage(input, state.language);
230
- let toolUsedThisTurn = false;
231
- let finalWithoutToolRetries = 0;
233
+ state.language = turnLanguage;
232
234
 
233
235
  while (true) {
234
236
  if (signal?.aborted) {
@@ -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,9 @@ 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.',
40
44
  ]
41
45
  : [
42
46
  'Always respond in English.',
@@ -47,6 +51,9 @@ function buildSystemPrompt(cwd, state = {}, options = {}) {
47
51
  'Never pretend you completed an action if you did not actually use tools or obtain a real result.',
48
52
  'If the task requires verification, try a real tool first and wait for its result before concluding.',
49
53
  'Do not end with a conclusion if you have not tested anything yet.',
54
+ 'If a task takes long, use run_command with an appropriate timeoutMs and verify the real result.',
55
+ '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.',
56
+ 'Use only tools listed under "Tool use". Never invent tool names or aliases.',
50
57
  ];
51
58
 
52
59
  const parts = [
@@ -175,6 +182,7 @@ const TOOL_ARG_KEYS = {
175
182
  fetch_url: ['url', 'selector', 'attribute', 'limit'],
176
183
  web_search: ['query'],
177
184
  web_read: ['url'],
185
+ create_canvas_image: ['width', 'height', 'background', 'elements', 'format', 'outputPath'],
178
186
  };
179
187
 
180
188
  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
 
@@ -0,0 +1,302 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { MODELS_FILE, PROVIDERS_FILE, REQUEST_TIMEOUT_MS } = require('../config');
4
+
5
+ const DEFAULT_HEADERS = {
6
+ 'Content-Type': 'application/json',
7
+ 'Accept': 'application/json',
8
+ 'User-Agent': 'Zyn/1.0',
9
+ };
10
+
11
+ function readJsonFile(filePath) {
12
+ try {
13
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
14
+ } catch {
15
+ return null;
16
+ }
17
+ }
18
+
19
+ function writeJsonFile(filePath, data) {
20
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
21
+ fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf8');
22
+ }
23
+
24
+ function normalizeBaseUrl(input) {
25
+ return String(input || '').trim().replace(/\/$/, '');
26
+ }
27
+
28
+ function slugify(text) {
29
+ return String(text || '')
30
+ .trim()
31
+ .toLowerCase()
32
+ .replace(/[^a-z0-9]+/g, '-')
33
+ .replace(/^-+|-+$/g, '') || 'model';
34
+ }
35
+
36
+ function titleize(text) {
37
+ return String(text || '')
38
+ .replace(/[-_]/g, ' ')
39
+ .replace(/\s+/g, ' ')
40
+ .trim()
41
+ .replace(/\b\w/g, ch => ch.toUpperCase());
42
+ }
43
+
44
+ async function fetchJson(url, options = {}) {
45
+ const controller = new AbortController();
46
+ const timeout = setTimeout(() => controller.abort(), options.timeoutMs || REQUEST_TIMEOUT_MS);
47
+ const signal = options.signal;
48
+ const onExternalAbort = () => controller.abort();
49
+ if (signal) {
50
+ if (signal.aborted) controller.abort();
51
+ else signal.addEventListener('abort', onExternalAbort, { once: true });
52
+ }
53
+
54
+ try {
55
+ const res = await fetch(url, {
56
+ method: options.method || 'GET',
57
+ headers: {
58
+ ...DEFAULT_HEADERS,
59
+ ...(options.headers || {}),
60
+ },
61
+ body: options.body ? JSON.stringify(options.body) : undefined,
62
+ signal: controller.signal,
63
+ });
64
+
65
+ const text = await res.text();
66
+ let data = null;
67
+ try {
68
+ data = text ? JSON.parse(text) : null;
69
+ } catch {
70
+ data = { raw: text };
71
+ }
72
+
73
+ if (!res.ok) {
74
+ const detail = typeof data === 'string'
75
+ ? data
76
+ : data?.message || data?.error || text;
77
+ const err = new Error(`HTTP ${res.status}: ${String(detail || '').slice(0, 300)}`);
78
+ err.status = res.status;
79
+ err.body = data;
80
+ throw err;
81
+ }
82
+
83
+ return data;
84
+ } finally {
85
+ clearTimeout(timeout);
86
+ if (signal) signal.removeEventListener('abort', onExternalAbort);
87
+ }
88
+ }
89
+
90
+ function loadProviderRegistry() {
91
+ const raw = readJsonFile(PROVIDERS_FILE);
92
+ if (!raw || typeof raw !== 'object') {
93
+ return { providers: {} };
94
+ }
95
+ if (raw.providers && typeof raw.providers === 'object') {
96
+ return { providers: raw.providers };
97
+ }
98
+ return { providers: raw };
99
+ }
100
+
101
+ function saveProviderRegistry(registry) {
102
+ writeJsonFile(PROVIDERS_FILE, registry);
103
+ }
104
+
105
+ function loadExternalModels() {
106
+ const raw = readJsonFile(MODELS_FILE);
107
+ if (!raw) return {};
108
+ if (Array.isArray(raw)) {
109
+ const output = {};
110
+ for (const item of raw) {
111
+ if (!item?.key) continue;
112
+ output[item.key] = item;
113
+ }
114
+ return output;
115
+ }
116
+ if (raw.models && typeof raw.models === 'object') return raw.models;
117
+ return raw && typeof raw === 'object' ? raw : {};
118
+ }
119
+
120
+ function saveExternalModels(models) {
121
+ writeJsonFile(MODELS_FILE, models);
122
+ }
123
+
124
+ function upsertProviderConfig(providerKey, config) {
125
+ const registry = loadProviderRegistry();
126
+ registry.providers[providerKey] = {
127
+ ...registry.providers[providerKey],
128
+ ...config,
129
+ provider: providerKey,
130
+ updatedAt: new Date().toISOString(),
131
+ };
132
+ saveProviderRegistry(registry);
133
+ return registry.providers[providerKey];
134
+ }
135
+
136
+ function removeProviderConfig(providerKey) {
137
+ const registry = loadProviderRegistry();
138
+ delete registry.providers[providerKey];
139
+ saveProviderRegistry(registry);
140
+ }
141
+
142
+ function listConfiguredProviders() {
143
+ const registry = loadProviderRegistry();
144
+ return Object.entries(registry.providers).map(([key, value]) => ({ key, ...value }));
145
+ }
146
+
147
+ function sanitizeModelKey(providerKey, modelId) {
148
+ return `${slugify(providerKey)}-${slugify(modelId)}`;
149
+ }
150
+
151
+ function buildModelRecord(providerKey, config, modelId, label, extra = {}) {
152
+ const key = sanitizeModelKey(providerKey, modelId);
153
+ const record = {
154
+ key,
155
+ label: label || titleize(modelId),
156
+ provider: providerKey,
157
+ modelId,
158
+ ...extra,
159
+ };
160
+
161
+ if (config?.baseUrl) record.baseUrl = config.baseUrl;
162
+ if (config?.apiKey) record.apiKey = config.apiKey;
163
+ if (config?.modelEndpoint) record.modelEndpoint = config.modelEndpoint;
164
+ if (config?.chatEndpoint) record.chatEndpoint = config.chatEndpoint;
165
+ return record;
166
+ }
167
+
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
+ async function fetchZenModels(config) {
206
+ const baseUrl = normalizeBaseUrl(config.baseUrl || 'https://opencode.ai/zen');
207
+ const data = await fetchJson(`${baseUrl}/v1/models`, {
208
+ headers: config.apiKey ? { Authorization: `Bearer ${config.apiKey}` } : {},
209
+ });
210
+ const models = Array.isArray(data?.data) ? data.data : Array.isArray(data?.models) ? data.models : [];
211
+ return models.map(model => buildModelRecord(
212
+ 'zen',
213
+ { ...config, baseUrl },
214
+ model.id || model.name,
215
+ model.name || titleize(model.id || model.name),
216
+ {
217
+ zenModel: model.id || model.name,
218
+ raw: model,
219
+ },
220
+ )).filter(item => item.modelId);
221
+ }
222
+
223
+ async function fetchQwenModels(config) {
224
+ const models = [
225
+ { id: 'qwen3.6-plus', label: 'Qwen 3.6 Plus' },
226
+ { id: 'qwen3.5-plus', label: 'Qwen 3.5 Plus' },
227
+ { id: 'qwen-coder-plus', label: 'Qwen Coder Plus' },
228
+ ];
229
+ return models.map(model => buildModelRecord(
230
+ 'qwen',
231
+ config,
232
+ model.id,
233
+ model.label,
234
+ {
235
+ qwenModel: model.id,
236
+ static: true,
237
+ },
238
+ ));
239
+ }
240
+
241
+ async function fetchProviderModels(providerKey, config = {}) {
242
+ const key = String(providerKey || '').trim();
243
+ if (key === 'ollama') return fetchOllamaModels(config);
244
+ if (key === 'openai-compatible') return fetchOpenAICompatibleModels(config);
245
+ if (key === 'zen') return fetchZenModels(config);
246
+ if (key === 'qwen') return fetchQwenModels(config);
247
+ throw new Error(`Proveedor no soportado: ${key}`);
248
+ }
249
+
250
+ function mergeProviderModels(providerKey, models) {
251
+ const current = loadExternalModels();
252
+ const next = {};
253
+
254
+ for (const [key, model] of Object.entries(current)) {
255
+ if (model.provider !== providerKey) {
256
+ next[key] = model;
257
+ }
258
+ }
259
+
260
+ for (const model of models) {
261
+ next[model.key] = model;
262
+ }
263
+
264
+ saveExternalModels(next);
265
+ return next;
266
+ }
267
+
268
+ async function syncProvider(providerKey) {
269
+ const registry = loadProviderRegistry();
270
+ const config = registry.providers[providerKey];
271
+ if (!config) {
272
+ throw new Error(`Proveedor no configurado: ${providerKey}`);
273
+ }
274
+ const models = await fetchProviderModels(providerKey, config);
275
+ registry.providers[providerKey] = {
276
+ ...config,
277
+ provider: providerKey,
278
+ updatedAt: new Date().toISOString(),
279
+ modelCount: models.length,
280
+ };
281
+ saveProviderRegistry(registry);
282
+ mergeProviderModels(providerKey, models);
283
+ return models;
284
+ }
285
+
286
+ function describeProviderConfig(providerKey) {
287
+ const registry = loadProviderRegistry();
288
+ return registry.providers[providerKey] || null;
289
+ }
290
+
291
+ module.exports = {
292
+ describeProviderConfig,
293
+ fetchProviderModels,
294
+ listConfiguredProviders,
295
+ loadProviderRegistry,
296
+ mergeProviderModels,
297
+ normalizeBaseUrl,
298
+ removeProviderConfig,
299
+ saveProviderRegistry,
300
+ syncProvider,
301
+ upsertProviderConfig,
302
+ };