zyn-ai 1.3.4 → 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.
- package/README.md +7 -11
- package/package.json +1 -1
- package/src/cli/commands.js +77 -7
- package/src/config.js +28 -21
- package/src/core/agent.js +52 -11
- package/src/core/prompts.js +48 -4
- package/src/i18n.js +2 -2
- package/src/providers/catalog.js +27 -43
- package/src/providers/gemini/index.js +338 -0
- package/src/providers/scraperClient.js +3 -6
- package/src/tools/index.js +230 -0
- package/src/tui/app.mjs +17 -1
- package/src/utils/gmailAuth.js +427 -0
- package/src/utils/sessionStorage.js +16 -9
- package/src/web/public/index.html +3 -1
- package/src/web/server.js +10 -3
- package/src/web/webAgent.js +5 -3
- package/src/providers/ollama/index.js +0 -78
- package/src/providers/openaiCompatible/index.js +0 -97
package/README.md
CHANGED
|
@@ -31,7 +31,6 @@ Zyn is a local AI agent designed for terminal and web usage. It supports persist
|
|
|
31
31
|
- Node.js 18+
|
|
32
32
|
- npm
|
|
33
33
|
- Internet connection for remote providers
|
|
34
|
-
- Optional: Ollama for local models
|
|
35
34
|
|
|
36
35
|
---
|
|
37
36
|
|
|
@@ -133,6 +132,9 @@ Commands:
|
|
|
133
132
|
|---|---|
|
|
134
133
|
| `/tools` | List tools |
|
|
135
134
|
| `/skills` | List skills |
|
|
135
|
+
| `/gmail connect` | Connect Gmail with Google OAuth + PKCE |
|
|
136
|
+
| `/gmail status` | Show Gmail connection status |
|
|
137
|
+
| `/gmail disconnect` | Remove saved Gmail tokens |
|
|
136
138
|
| `/cwd` | Show working directory |
|
|
137
139
|
|
|
138
140
|
### Web & Export
|
|
@@ -164,16 +166,10 @@ Example:
|
|
|
164
166
|
```json
|
|
165
167
|
{
|
|
166
168
|
"models": {
|
|
167
|
-
"my-
|
|
168
|
-
"label": "
|
|
169
|
-
"provider": "
|
|
170
|
-
"
|
|
171
|
-
},
|
|
172
|
-
"my-remote-model": {
|
|
173
|
-
"label": "My remote model",
|
|
174
|
-
"provider": "openai-compatible",
|
|
175
|
-
"openaiModel": "gpt-4o-mini",
|
|
176
|
-
"baseUrl": "https://api.example.com/v1"
|
|
169
|
+
"my-gemini-flash": {
|
|
170
|
+
"label": "Gemini Flash",
|
|
171
|
+
"provider": "gemini",
|
|
172
|
+
"geminiModel": "gemini-flash"
|
|
177
173
|
}
|
|
178
174
|
}
|
|
179
175
|
}
|
package/package.json
CHANGED
package/src/cli/commands.js
CHANGED
|
@@ -4,14 +4,35 @@ const { spawn } = require('child_process');
|
|
|
4
4
|
|
|
5
5
|
const fsp = fs.promises;
|
|
6
6
|
const { listSkills, SKILLS_DIR } = require('../core/skills');
|
|
7
|
-
const { DEFAULT_LANGUAGE, DEFAULT_MODEL_KEY, MODELS, listProvidersFromModels } = require('../config');
|
|
7
|
+
const { DEFAULT_LANGUAGE, DEFAULT_MODEL_KEY, GEMINI_MODEL_WARNING, MODELS, listProvidersFromModels } = require('../config');
|
|
8
8
|
const { languageLabel, normalizeLanguage, t } = require('../i18n');
|
|
9
9
|
const { createNewSessionState, listSessions, loadSessionState, saveState } = require('../utils/sessionStorage');
|
|
10
10
|
const { listGitSecrets, removeGitSecret, upsertGitSecret } = require('../utils/secretStorage');
|
|
11
|
+
const { clearGmailAuth, getGmailAuthStatus, startGmailOAuthFlow } = require('../utils/gmailAuth');
|
|
11
12
|
const { exportTranscriptText, formatTranscriptPreview } = require('../utils/transcriptStorage');
|
|
12
13
|
const { resolveInputPath } = require('../utils/pathUtils');
|
|
13
14
|
const { printTools } = require('../tools');
|
|
14
15
|
|
|
16
|
+
|
|
17
|
+
function getModelWarning(key) {
|
|
18
|
+
const model = MODELS[key];
|
|
19
|
+
return model?.provider === 'gemini' ? GEMINI_MODEL_WARNING : '';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function printModelChanged(key) {
|
|
23
|
+
const warning = getModelWarning(key);
|
|
24
|
+
console.log(`Model: ${MODELS[key].label}`);
|
|
25
|
+
if (warning) console.log(`Warning: ${warning}`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function printLanguageChanged(language) {
|
|
29
|
+
const normalized = normalizeLanguage(language);
|
|
30
|
+
const label = languageLabel(normalized);
|
|
31
|
+
console.log(normalized === 'es'
|
|
32
|
+
? `Actualizado al idioma ${label} (${normalized})`
|
|
33
|
+
: `Updated to language ${label} (${normalized})`);
|
|
34
|
+
}
|
|
35
|
+
|
|
15
36
|
const SLASH_COMMANDS = [
|
|
16
37
|
{ name: 'help', desc: 'full help', descEs: 'ayuda completa' },
|
|
17
38
|
{ name: 'status', desc: 'current status', descEs: 'estado actual' },
|
|
@@ -28,6 +49,7 @@ const SLASH_COMMANDS = [
|
|
|
28
49
|
{ name: 'models', desc: 'list models', descEs: 'listar modelos' },
|
|
29
50
|
{ name: 'providers', desc: 'list providers', descEs: 'listar proveedores' },
|
|
30
51
|
{ name: 'git', desc: 'configure git credentials', descEs: 'configurar credenciales git' },
|
|
52
|
+
{ name: 'gmail', desc: 'connect Gmail account', descEs: 'conectar cuenta Gmail' },
|
|
31
53
|
{ name: 'persona', desc: 'set response tone/personality', descEs: 'definir tono/persona' },
|
|
32
54
|
{ name: 'lang', desc: 'change language', descEs: 'cambiar idioma' },
|
|
33
55
|
{ name: 'language', desc: 'change language', descEs: 'cambiar idioma' },
|
|
@@ -121,6 +143,9 @@ function printHelp(state = {}) {
|
|
|
121
143
|
console.log(` ${b('/git set <provider> <token> [user] [apiBaseUrl:URL] [cloneBaseUrl:URL] [name:X]')}`);
|
|
122
144
|
console.log(` ${b('/git list')} List configured git profiles`);
|
|
123
145
|
console.log(` ${b('/git remove <provider> [name]')} Remove git credentials`);
|
|
146
|
+
console.log(` ${b('/gmail connect')} Connect Gmail with Google OAuth + PKCE`);
|
|
147
|
+
console.log(` ${b('/gmail status')} Show Gmail connection status`);
|
|
148
|
+
console.log(` ${b('/gmail disconnect')} Remove saved Gmail tokens`);
|
|
124
149
|
console.log(` ${b('/cwd')} Show current working directory`);
|
|
125
150
|
console.log(` ${b('/cwd <path>')} Change working directory`);
|
|
126
151
|
console.log('');
|
|
@@ -261,7 +286,7 @@ async function handleLocalCommand(input, state, deps) {
|
|
|
261
286
|
|
|
262
287
|
state.language = nextLanguage;
|
|
263
288
|
await saveState(state);
|
|
264
|
-
|
|
289
|
+
printLanguageChanged(nextLanguage);
|
|
265
290
|
return true;
|
|
266
291
|
}
|
|
267
292
|
|
|
@@ -401,7 +426,7 @@ async function handleLocalCommand(input, state, deps) {
|
|
|
401
426
|
}
|
|
402
427
|
state.language = nextLanguage;
|
|
403
428
|
await saveState(state);
|
|
404
|
-
|
|
429
|
+
printLanguageChanged(nextLanguage);
|
|
405
430
|
return true;
|
|
406
431
|
}
|
|
407
432
|
|
|
@@ -416,9 +441,9 @@ async function handleLocalCommand(input, state, deps) {
|
|
|
416
441
|
await saveState(state);
|
|
417
442
|
await appendTranscriptEntry(state.sessionId, {
|
|
418
443
|
type: 'system',
|
|
419
|
-
content: `Model switched to: ${MODELS[key].label}`,
|
|
444
|
+
content: `Model switched to: ${MODELS[key].label}${getModelWarning(key) ? `\nWarning: ${getModelWarning(key)}` : ''}`,
|
|
420
445
|
});
|
|
421
|
-
|
|
446
|
+
printModelChanged(key);
|
|
422
447
|
return true;
|
|
423
448
|
}
|
|
424
449
|
|
|
@@ -479,9 +504,9 @@ async function handleLocalCommand(input, state, deps) {
|
|
|
479
504
|
await saveState(state);
|
|
480
505
|
await appendTranscriptEntry(state.sessionId, {
|
|
481
506
|
type: 'system',
|
|
482
|
-
content: `Model switched to: ${MODELS[key].label}`,
|
|
507
|
+
content: `Model switched to: ${MODELS[key].label}${getModelWarning(key) ? `\nWarning: ${getModelWarning(key)}` : ''}`,
|
|
483
508
|
});
|
|
484
|
-
|
|
509
|
+
printModelChanged(key);
|
|
485
510
|
return true;
|
|
486
511
|
}
|
|
487
512
|
|
|
@@ -541,6 +566,51 @@ async function handleLocalCommand(input, state, deps) {
|
|
|
541
566
|
return true;
|
|
542
567
|
}
|
|
543
568
|
|
|
569
|
+
|
|
570
|
+
if (commandName === 'gmail') {
|
|
571
|
+
const [subRaw, ...rest] = String(args || 'status').trim().split(/\s+/).filter(Boolean);
|
|
572
|
+
const sub = (subRaw || 'status').toLowerCase();
|
|
573
|
+
|
|
574
|
+
if (sub === 'connect' || sub === 'login') {
|
|
575
|
+
const portArg = rest.find(part => /^\d{2,5}$/.test(part));
|
|
576
|
+
const flow = await startGmailOAuthFlow({ port: portArg ? Number(portArg) : 0, flow: 'code' });
|
|
577
|
+
console.log(flow.authUrl);
|
|
578
|
+
if (flow.flow === 'device') {
|
|
579
|
+
console.log(`Código: ${flow.userCode}`);
|
|
580
|
+
console.log('Abre el link, ingresa el código y autoriza Gmail.');
|
|
581
|
+
} else {
|
|
582
|
+
console.log('Abre el link, inicia sesión y vuelve aquí.');
|
|
583
|
+
}
|
|
584
|
+
flow.done
|
|
585
|
+
.then(auth => {
|
|
586
|
+
const email = auth?.profile?.email || 'cuenta conectada';
|
|
587
|
+
console.error(`Gmail conectado: ${email}`);
|
|
588
|
+
})
|
|
589
|
+
.catch(err => console.error(`Gmail OAuth fallo: ${err.message}`));
|
|
590
|
+
return true;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
if (sub === 'status') {
|
|
594
|
+
const status = await getGmailAuthStatus();
|
|
595
|
+
if (!status.connected) {
|
|
596
|
+
console.log('Gmail: no conectado. Usa /gmail connect.');
|
|
597
|
+
} else {
|
|
598
|
+
console.log(`Gmail: conectado${status.email ? ` (${status.email})` : ''}`);
|
|
599
|
+
console.log(`Scopes: ${status.scopes.join(', ') || '-'}`);
|
|
600
|
+
console.log(`Expira: ${status.expiryDate ? new Date(status.expiryDate).toISOString() : '-'}`);
|
|
601
|
+
}
|
|
602
|
+
return true;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
if (sub === 'disconnect' || sub === 'logout' || sub === 'remove') {
|
|
606
|
+
await clearGmailAuth();
|
|
607
|
+
console.log('Gmail desconectado.');
|
|
608
|
+
return true;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
throw new Error('Use /gmail connect|status|disconnect');
|
|
612
|
+
}
|
|
613
|
+
|
|
544
614
|
if (commandName === 'web') {
|
|
545
615
|
let host = '127.0.0.1';
|
|
546
616
|
let port = 3000;
|
package/src/config.js
CHANGED
|
@@ -34,23 +34,16 @@ const BUILTIN_MODELS = {
|
|
|
34
34
|
provider: 'zen',
|
|
35
35
|
zenModel: 'trinity-large-preview-free',
|
|
36
36
|
},
|
|
37
|
-
'
|
|
38
|
-
label: '
|
|
39
|
-
provider: '
|
|
40
|
-
|
|
41
|
-
},
|
|
42
|
-
'ollama-gemma': {
|
|
43
|
-
label: 'Ollama Gemma 3',
|
|
44
|
-
provider: 'ollama',
|
|
45
|
-
ollamaModel: 'gemma3:latest',
|
|
46
|
-
},
|
|
47
|
-
'openai-mini': {
|
|
48
|
-
label: 'OpenAI Compatible Mini',
|
|
49
|
-
provider: 'openai-compatible',
|
|
50
|
-
openaiModel: 'gpt-4o-mini',
|
|
37
|
+
'gemini-flash': {
|
|
38
|
+
label: 'Gemini Flash',
|
|
39
|
+
provider: 'gemini',
|
|
40
|
+
geminiModel: 'gemini-flash',
|
|
51
41
|
},
|
|
52
42
|
};
|
|
53
43
|
|
|
44
|
+
const SUPPORTED_MODEL_PROVIDERS = new Set(['qwen', 'zen', 'gemini']);
|
|
45
|
+
const GEMINI_MODEL_WARNING = 'It is not recommended for use in production; it is unstable and ineffective.';
|
|
46
|
+
|
|
54
47
|
function readJsonFile(filePath) {
|
|
55
48
|
try {
|
|
56
49
|
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
@@ -66,7 +59,7 @@ function loadExternalModels() {
|
|
|
66
59
|
if (Array.isArray(raw)) {
|
|
67
60
|
const output = {};
|
|
68
61
|
for (const item of raw) {
|
|
69
|
-
if (!item?.key || !item?.provider) continue;
|
|
62
|
+
if (!item?.key || !item?.provider || !SUPPORTED_MODEL_PROVIDERS.has(item.provider)) continue;
|
|
70
63
|
output[item.key] = {
|
|
71
64
|
label: item.label || item.key,
|
|
72
65
|
provider: item.provider,
|
|
@@ -76,11 +69,13 @@ function loadExternalModels() {
|
|
|
76
69
|
return output;
|
|
77
70
|
}
|
|
78
71
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
72
|
+
const rawModels = raw && typeof raw === 'object' && raw.models && typeof raw.models === 'object'
|
|
73
|
+
? raw.models
|
|
74
|
+
: (raw && typeof raw === 'object' ? raw : {});
|
|
82
75
|
|
|
83
|
-
return
|
|
76
|
+
return Object.fromEntries(
|
|
77
|
+
Object.entries(rawModels).filter(([, model]) => SUPPORTED_MODEL_PROVIDERS.has(model?.provider)),
|
|
78
|
+
);
|
|
84
79
|
}
|
|
85
80
|
|
|
86
81
|
const MODELS = {
|
|
@@ -99,8 +94,10 @@ const MAX_OUTPUT_CHARS = 12000;
|
|
|
99
94
|
const MAX_FILE_LINES = 5000;
|
|
100
95
|
const ACTION_LOG_LIMIT = 40;
|
|
101
96
|
const REQUEST_TIMEOUT_MS = Number(process.env.ZYN_REQUEST_TIMEOUT_MS || 180000);
|
|
102
|
-
const MAX_HISTORY_CHARS =
|
|
103
|
-
const KEEP_RECENT_MESSAGES =
|
|
97
|
+
const MAX_HISTORY_CHARS = 60000;
|
|
98
|
+
const KEEP_RECENT_MESSAGES = 50;
|
|
99
|
+
const PROVIDER_TIMEOUT_RETRY_DELAY_MS = Number(process.env.ZYN_PROVIDER_TIMEOUT_RETRY_DELAY_MS || 600000);
|
|
100
|
+
const PROVIDER_TIMEOUT_MAX_ATTEMPTS = 3;
|
|
104
101
|
const SESSION_ROOT = path.join(DATA_ROOT, 'chat');
|
|
105
102
|
const SESSIONS_DIR = path.join(SESSION_ROOT, 'sessions');
|
|
106
103
|
const CURRENT_SESSION_FILE = path.join(SESSION_ROOT, 'current-session.json');
|
|
@@ -110,6 +107,9 @@ const EXPORTS_DIR = path.join(SESSION_ROOT, 'exports');
|
|
|
110
107
|
const THINK_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
111
108
|
const USER_DATA_ROOT = path.join(os.homedir(), '.zyn');
|
|
112
109
|
const TASKS_FILE = path.join(USER_DATA_ROOT, 'tasks.json');
|
|
110
|
+
const GMAIL_CLIENT_ID = '871944347395-rnpsjsqgbnvlfb05hqk4dc9283olgnh2.apps.googleusercontent.com';
|
|
111
|
+
const GMAIL_CLIENT_SECRET = process.env.ZYN_GMAIL_CLIENT_SECRET || '';
|
|
112
|
+
const GMAIL_AUTH_FILE = path.join(USER_DATA_ROOT, 'gmail-auth.json');
|
|
113
113
|
const PROVIDERS_FILE = path.join(DATA_ROOT, 'providers.json');
|
|
114
114
|
|
|
115
115
|
function listProvidersFromModels(models = MODELS) {
|
|
@@ -142,6 +142,10 @@ module.exports = {
|
|
|
142
142
|
DATA_ROOT,
|
|
143
143
|
DEFAULT_LANGUAGE,
|
|
144
144
|
DEFAULT_MODEL_KEY,
|
|
145
|
+
GEMINI_MODEL_WARNING,
|
|
146
|
+
GMAIL_AUTH_FILE,
|
|
147
|
+
GMAIL_CLIENT_ID,
|
|
148
|
+
GMAIL_CLIENT_SECRET,
|
|
145
149
|
EXPORTS_DIR,
|
|
146
150
|
HOME_DIR,
|
|
147
151
|
KEEP_RECENT_MESSAGES,
|
|
@@ -152,6 +156,9 @@ module.exports = {
|
|
|
152
156
|
MODELS,
|
|
153
157
|
MODELS_FILE,
|
|
154
158
|
PROVIDERS_FILE,
|
|
159
|
+
SUPPORTED_MODEL_PROVIDERS,
|
|
160
|
+
PROVIDER_TIMEOUT_MAX_ATTEMPTS,
|
|
161
|
+
PROVIDER_TIMEOUT_RETRY_DELAY_MS,
|
|
155
162
|
QWEN_EMAIL,
|
|
156
163
|
QWEN_PASSWORD,
|
|
157
164
|
REQUEST_TIMEOUT_MS,
|
package/src/core/agent.js
CHANGED
|
@@ -4,6 +4,8 @@ const {
|
|
|
4
4
|
MAX_HISTORY_CHARS,
|
|
5
5
|
MAX_TOOL_STEPS,
|
|
6
6
|
MODELS,
|
|
7
|
+
PROVIDER_TIMEOUT_MAX_ATTEMPTS,
|
|
8
|
+
PROVIDER_TIMEOUT_RETRY_DELAY_MS,
|
|
7
9
|
REQUEST_TIMEOUT_MS,
|
|
8
10
|
} = require('../config');
|
|
9
11
|
const { chat, chatSilent } = require('../providers/scraperClient');
|
|
@@ -25,6 +27,29 @@ const { normalizeText, shortText } = require('../utils/text');
|
|
|
25
27
|
const { detectLanguage } = require('../i18n');
|
|
26
28
|
|
|
27
29
|
|
|
30
|
+
|
|
31
|
+
function waitForRetry(ms, signal) {
|
|
32
|
+
if (!ms || ms <= 0) return Promise.resolve();
|
|
33
|
+
if (signal?.aborted) return Promise.reject(new Error('aborted'));
|
|
34
|
+
|
|
35
|
+
return new Promise((resolve, reject) => {
|
|
36
|
+
const timeout = setTimeout(cleanupAndResolve, ms);
|
|
37
|
+
const onAbort = () => {
|
|
38
|
+
clearTimeout(timeout);
|
|
39
|
+
cleanup();
|
|
40
|
+
reject(new Error('aborted'));
|
|
41
|
+
};
|
|
42
|
+
function cleanup() {
|
|
43
|
+
if (signal) signal.removeEventListener('abort', onAbort);
|
|
44
|
+
}
|
|
45
|
+
function cleanupAndResolve() {
|
|
46
|
+
cleanup();
|
|
47
|
+
resolve();
|
|
48
|
+
}
|
|
49
|
+
if (signal) signal.addEventListener('abort', onAbort, { once: true });
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
28
53
|
function looksLikeActionRequest(text) {
|
|
29
54
|
const sample = normalizeText(String(text || '')).toLowerCase();
|
|
30
55
|
if (!sample) return false;
|
|
@@ -38,7 +63,7 @@ async function requestModel(messages, state, ui, options = {}) {
|
|
|
38
63
|
signal,
|
|
39
64
|
} = options;
|
|
40
65
|
|
|
41
|
-
for (let attempt = 0; attempt <
|
|
66
|
+
for (let attempt = 0; attempt < PROVIDER_TIMEOUT_MAX_ATTEMPTS; attempt += 1) {
|
|
42
67
|
if (signal?.aborted) {
|
|
43
68
|
throw new Error(state.language === 'es' ? 'Agente detenido por el usuario (ESC x2)' : 'Agent stopped by the user (ESC x2)');
|
|
44
69
|
}
|
|
@@ -96,8 +121,21 @@ async function requestModel(messages, state, ui, options = {}) {
|
|
|
96
121
|
} catch (err) {
|
|
97
122
|
const externalAbort = Boolean(signal?.aborted);
|
|
98
123
|
const aborted = controller.signal.aborted || err?.name === 'AbortError';
|
|
99
|
-
if (aborted && timedOut && !externalAbort && attempt
|
|
100
|
-
|
|
124
|
+
if (aborted && timedOut && !externalAbort && attempt < PROVIDER_TIMEOUT_MAX_ATTEMPTS - 1) {
|
|
125
|
+
const waitMinutes = Math.round(PROVIDER_TIMEOUT_RETRY_DELAY_MS / 60000);
|
|
126
|
+
ui.logEvent(
|
|
127
|
+
state,
|
|
128
|
+
'warn',
|
|
129
|
+
state.language === 'es' ? 'Tiempo agotado del proveedor' : 'Provider timeout',
|
|
130
|
+
state.language === 'es'
|
|
131
|
+
? `Esperando ${waitMinutes} minutos antes de reenviar (${attempt + 2}/${PROVIDER_TIMEOUT_MAX_ATTEMPTS})`
|
|
132
|
+
: `Waiting ${waitMinutes} minutes before resending (${attempt + 2}/${PROVIDER_TIMEOUT_MAX_ATTEMPTS})`,
|
|
133
|
+
);
|
|
134
|
+
try {
|
|
135
|
+
await waitForRetry(PROVIDER_TIMEOUT_RETRY_DELAY_MS, signal);
|
|
136
|
+
} catch {
|
|
137
|
+
throw new Error(state.language === 'es' ? 'Agente detenido por el usuario (ESC x2)' : 'Agent stopped by the user (ESC x2)');
|
|
138
|
+
}
|
|
101
139
|
continue;
|
|
102
140
|
}
|
|
103
141
|
if (aborted) {
|
|
@@ -106,7 +144,7 @@ async function requestModel(messages, state, ui, options = {}) {
|
|
|
106
144
|
}
|
|
107
145
|
throw new Error(state.language === 'es' ? 'Tiempo agotado del proveedor' : 'Provider timeout exceeded');
|
|
108
146
|
}
|
|
109
|
-
if (!externalAbort && attempt
|
|
147
|
+
if (!externalAbort && attempt < PROVIDER_TIMEOUT_MAX_ATTEMPTS - 1) {
|
|
110
148
|
ui.logEvent(state, 'warn', state.language === 'es' ? 'Error transitorio, reenviando contexto y skills' : 'Transient error, resending context and skills');
|
|
111
149
|
continue;
|
|
112
150
|
}
|
|
@@ -131,10 +169,13 @@ async function summarizeMessages(state, ui, messages) {
|
|
|
131
169
|
{
|
|
132
170
|
role: 'system',
|
|
133
171
|
content: [
|
|
134
|
-
state.language === 'es' ? '
|
|
135
|
-
state.language === 'es' ? 'Escribe en
|
|
136
|
-
state.language === 'es'
|
|
137
|
-
|
|
172
|
+
state.language === 'es' ? 'Compacta la conversacion para memoria persistente del agente.' : 'Compact the conversation for the agent persistent memory.',
|
|
173
|
+
state.language === 'es' ? 'Escribe en español y conserva los idiomas/preferencias indicados por el usuario.' : 'Write in English and preserve any languages/preferences requested by the user.',
|
|
174
|
+
state.language === 'es'
|
|
175
|
+
? 'Resume sin perder detalles críticos: objetivo del proyecto, progreso exacto, decisiones, archivos/rutas tocadas, comandos ejecutados, resultados, errores, credenciales/configuraciones no secretas, restricciones, próximos pasos y preferencias de idioma.'
|
|
176
|
+
: 'Summarize without losing critical details: project goal, exact progress, decisions, touched files/paths, executed commands, results, errors, non-secret credentials/configuration, constraints, next steps, and language preferences.',
|
|
177
|
+
state.language === 'es' ? 'No inventes; si algo está pendiente márcalo como Pendiente. Mantén nombres propios, rutas, APIs, límites y valores numéricos intactos.' : 'Do not invent; if something is pending mark it as Pending. Keep proper nouns, paths, APIs, limits, and numeric values intact.',
|
|
178
|
+
'Format: compact bullet list with sections Contexto/Progreso/Archivos/Comandos/Pendiente/Preferencias. Max 20 lines.',
|
|
138
179
|
].join('\n'),
|
|
139
180
|
},
|
|
140
181
|
{
|
|
@@ -205,9 +246,9 @@ async function answerFromToolResult(input, call, result, state, ui) {
|
|
|
205
246
|
|
|
206
247
|
const output = await requestModel(messages, state, ui, {
|
|
207
248
|
label: state.language === 'es' ? 'Resumiendo resultado' : 'Summarizing result',
|
|
208
|
-
streamOutput: true,
|
|
209
249
|
});
|
|
210
|
-
|
|
250
|
+
const parsed = parseAgentResponse(output);
|
|
251
|
+
return parsed.type === 'final' ? normalizeText(parsed.content) : normalizeText(output);
|
|
211
252
|
}
|
|
212
253
|
|
|
213
254
|
async function runAgentTurn(input, state, ui, options = {}) {
|
|
@@ -241,7 +282,7 @@ async function runAgentTurn(input, state, ui, options = {}) {
|
|
|
241
282
|
});
|
|
242
283
|
ui.logEvent(state, 'ok', state.language === 'es' ? 'Respuesta lista' : 'Response ready');
|
|
243
284
|
await persistSessionState(state, ui);
|
|
244
|
-
return { content: finalAnswer, rendered:
|
|
285
|
+
return { content: finalAnswer, rendered: false };
|
|
245
286
|
}
|
|
246
287
|
|
|
247
288
|
const turnMessages = [{ role: 'user', content: input }];
|
package/src/core/prompts.js
CHANGED
|
@@ -66,7 +66,7 @@ function buildSystemPrompt(cwd, state = {}, options = {}) {
|
|
|
66
66
|
|
|
67
67
|
const languageInstructions = language === 'es'
|
|
68
68
|
? [
|
|
69
|
-
'Responde
|
|
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,12 +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
|
-
'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.',
|
|
80
81
|
'No te limites a una sola tool por costumbre; elige la mejor secuencia técnica para el objetivo.',
|
|
81
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.',
|
|
82
83
|
]
|
|
83
84
|
: [
|
|
84
|
-
'
|
|
85
|
+
'Respond in the language of the user\'s latest message (if ambiguous, use the session preference).',
|
|
85
86
|
'Execute the task directly. Do not give tutorials or instructions when you can act yourself.',
|
|
86
87
|
'Use tools when needed without asking for extra permission.',
|
|
87
88
|
'Reply only with the final result or the next concrete action.',
|
|
@@ -91,9 +92,11 @@ function buildSystemPrompt(cwd, state = {}, options = {}) {
|
|
|
91
92
|
'Do not end with a conclusion if you have not tested anything yet.',
|
|
92
93
|
'If a task takes long, use run_command with an appropriate timeoutMs and verify the real result.',
|
|
93
94
|
'For Git operations use the git tool with action="api" or action="clone".',
|
|
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.',
|
|
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.',
|
|
95
97
|
'Do not over-focus on a single tool by habit; choose the best technical sequence for the goal.',
|
|
96
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.',
|
|
97
100
|
];
|
|
98
101
|
|
|
99
102
|
const toolUseEnforcement = language === 'es'
|
|
@@ -283,6 +286,8 @@ const TOOL_ARG_KEYS = {
|
|
|
283
286
|
scrape_site: ['url', 'selectors', 'limit', 'headers'],
|
|
284
287
|
web_search: ['query', 'lang', 'limit'],
|
|
285
288
|
web_read: ['url'],
|
|
289
|
+
upload_file: ['path', 'field', 'name', 'type'],
|
|
290
|
+
gmail: ['action', 'query', 'maxResults', 'id', 'to', 'subject', 'body'],
|
|
286
291
|
create_canvas_image: ['width', 'height', 'background', 'elements', 'format', 'outputPath'],
|
|
287
292
|
git: ['provider', 'action', 'method', 'path', 'body', 'headers', 'name', 'repoUrl', 'destination', 'branch', 'timeoutMs'],
|
|
288
293
|
};
|
|
@@ -294,6 +299,38 @@ const LONG_VALUE_ARG = {
|
|
|
294
299
|
replace_in_file: 'replace',
|
|
295
300
|
};
|
|
296
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
|
+
|
|
297
334
|
function fuzzyExtractTool(text) {
|
|
298
335
|
const toolMatch = text.match(/(?:"|')?tool(?:"|')?\s*:\s*"(\w+)"/i);
|
|
299
336
|
if (!toolMatch) return null;
|
|
@@ -422,6 +459,13 @@ function parseAgentResponse(raw) {
|
|
|
422
459
|
const tool = extractToolJson(text);
|
|
423
460
|
if (tool) return { type: 'tool', tool: tool.tool, args: tool.args ?? {} };
|
|
424
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
|
+
|
|
425
469
|
const xmlTool = extractXmlTool(text);
|
|
426
470
|
if (xmlTool) return xmlTool;
|
|
427
471
|
|
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: '
|
|
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: '
|
|
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.',
|
package/src/providers/catalog.js
CHANGED
|
@@ -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
|
-
|
|
117
|
-
|
|
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
|
|
244
|
-
if (key === '
|
|
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}`);
|