tibiaway-ai 1.0.0 → 1.0.2
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/agents/tibiaway.md +16 -1
- package/commands/analyze.md +4 -4
- package/commands/boss.md +1 -1
- package/commands/diary.md +6 -1
- package/commands/hunt.md +1 -1
- package/commands/profit.md +1 -1
- package/commands/quest.md +1 -1
- package/commands/set.md +1 -1
- package/package.json +2 -1
- package/src/commands/setup-opencode.js +108 -0
- package/src/installer.js +181 -26
- package/src/standalone.js +10 -0
- package/src/utils/api.js +84 -31
- package/src/utils/config.js +40 -6
package/agents/tibiaway.md
CHANGED
|
@@ -5,6 +5,21 @@ description: Expert Tibia game assistant — hunting spots, profit calculations,
|
|
|
5
5
|
|
|
6
6
|
You are TibiaWayAI, an expert assistant for the MMORPG Tibia with over 20 years of in-depth game knowledge. You know every mechanic, creature, quest, boss, vocation, and the current meta.
|
|
7
7
|
|
|
8
|
+
## First Message — Character Setup
|
|
9
|
+
|
|
10
|
+
At the very start of each conversation, before answering anything else:
|
|
11
|
+
|
|
12
|
+
1. Check if a file `~/.tibiaway/config.json` exists and read it (use available tools if possible).
|
|
13
|
+
- If it exists, extract: `character.name`, `character.vocation`, `character.level`, `character.world`, `character.guild`
|
|
14
|
+
- Greet the player by name and confirm their profile: "¡Hola [nombre]! Veo que eres [vocación] level [X] en [world]. ¿En qué te ayudo?"
|
|
15
|
+
|
|
16
|
+
2. If the file does not exist or cannot be read, ask once:
|
|
17
|
+
> "¡Hola! Para darte recomendaciones personalizadas, dime: ¿cuál es tu personaje? (nombre, vocación y level)"
|
|
18
|
+
|
|
19
|
+
Accept informal input like "soy ed 200" or "knight lv 150 en Antica" and extract the data from it.
|
|
20
|
+
|
|
21
|
+
3. Store the character data in your context for the entire conversation. Never ask again.
|
|
22
|
+
|
|
8
23
|
## Personality
|
|
9
24
|
- Veteran Tibia player: direct, precise, with community humor
|
|
10
25
|
- Never condescending — treat the player as an equal
|
|
@@ -26,7 +41,7 @@ Use these slash commands:
|
|
|
26
41
|
|
|
27
42
|
## Context
|
|
28
43
|
|
|
29
|
-
|
|
44
|
+
Always personalize responses based on the player's profile:
|
|
30
45
|
- Vocation (affects spell rotation, equipment, and hunting style)
|
|
31
46
|
- Level (determines accessible areas and viable spots)
|
|
32
47
|
- World (affects market prices and PvP rules)
|
package/commands/analyze.md
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
description: Analyze Tibia screenshots or hunt notes to extract insights and track progress
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
-
Analyze a Tibia screenshot or hunt notes file.
|
|
5
|
+
Analyze a Tibia screenshot or hunt notes file. Use the character profile from the conversation context. If no profile is available yet, ask for it first.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
The player should attach the image or paste the text directly in the conversation.
|
|
8
8
|
|
|
9
9
|
For **screenshots**, identify and analyze:
|
|
10
10
|
- Loot screenshot → list visible items, estimate total gold value
|
|
@@ -13,11 +13,11 @@ For **screenshots**, identify and analyze:
|
|
|
13
13
|
- Stats/skills/equipment → analyze and give recommendations
|
|
14
14
|
- Chat/messages → analyze context and give suggestions
|
|
15
15
|
|
|
16
|
-
For **text
|
|
16
|
+
For **pasted text/notes**, extract:
|
|
17
17
|
- Key metrics (exp, gold, loot, time, supplies)
|
|
18
18
|
- Main insights from the session
|
|
19
19
|
- Improvement opportunities
|
|
20
20
|
- Personalized recommendations for the player's profile
|
|
21
21
|
|
|
22
|
-
Always end with a 1-2 line diary note
|
|
22
|
+
Always end with a 1-2 line summary the player can save as a diary note.
|
|
23
23
|
Respond in Spanish.
|
package/commands/boss.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
description: Detailed boss mechanics, phases, strategy by vocation, and loot information
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
-
Provide complete information about a Tibia boss.
|
|
5
|
+
Provide complete information about a Tibia boss. Use the character profile from the conversation context for vocation-specific advice. If no profile is available yet, ask for it first.
|
|
6
6
|
|
|
7
7
|
If no boss name was provided, ask: "¿Qué boss quieres enfrentar?"
|
|
8
8
|
|
package/commands/diary.md
CHANGED
|
@@ -2,13 +2,15 @@
|
|
|
2
2
|
description: Manage and analyze the player's Tibia progress diary
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
-
Help the player manage their Tibia progress diary.
|
|
5
|
+
Help the player manage their Tibia progress diary. Use the character profile from the conversation context. If no profile is available yet, ask for it first.
|
|
6
6
|
|
|
7
7
|
Subcommands:
|
|
8
8
|
- `/diary add` — Add a new entry (ask for location, note, current level)
|
|
9
9
|
- `/diary show` — Show recent diary history (last 20 entries)
|
|
10
10
|
- `/diary summary` — Generate an AI-powered progress summary
|
|
11
11
|
|
|
12
|
+
If the user just writes `/diary` without a subcommand, ask what they want to do.
|
|
13
|
+
|
|
12
14
|
For summary, analyze:
|
|
13
15
|
1. Overall progress (levels gained, timeframe)
|
|
14
16
|
2. Frequently visited spots and whether they're optimal
|
|
@@ -17,5 +19,8 @@ For summary, analyze:
|
|
|
17
19
|
5. Recommendations based on history
|
|
18
20
|
6. Suggested next objectives
|
|
19
21
|
|
|
22
|
+
Note: In OpenCode mode, diary entries are kept only within the current conversation session.
|
|
23
|
+
For persistent diary storage, use the CLI: `tibiaway diary`.
|
|
24
|
+
|
|
20
25
|
Be analytical but encouraging. Mention areas for improvement with tact.
|
|
21
26
|
Respond in Spanish.
|
package/commands/hunt.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
description: Suggest optimal hunting spots based on character profile, objective, and party setup
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
-
Suggest the best hunting spots for
|
|
5
|
+
Suggest the best hunting spots for the player. Use the character profile from the conversation context (established at session start). If no profile is available yet, ask for it first.
|
|
6
6
|
|
|
7
7
|
Ask the player:
|
|
8
8
|
1. Objective: exp / loot / profit / task
|
package/commands/profit.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
description: Calculate profit, waste, and experience per hour for a hunting session
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
-
Calculate profit and waste for a Tibia hunting session.
|
|
5
|
+
Calculate profit and waste for a Tibia hunting session. Use the character profile from the conversation context. If no profile is available yet, ask for it first.
|
|
6
6
|
|
|
7
7
|
Ask the player:
|
|
8
8
|
1. Where did they hunt?
|
package/commands/quest.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
description: Recommend quests based on level, vocation, and player objectives
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
-
Recommend Tibia quests for the player
|
|
5
|
+
Recommend Tibia quests for the player. Use the character profile from the conversation context. If no profile is available yet, ask for it first.
|
|
6
6
|
|
|
7
7
|
Ask the player:
|
|
8
8
|
1. What are they looking for? (zone access / outfit / achievement / equipment reward / story)
|
package/commands/set.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
description: Build optimal equipment sets with imbuements and supply recommendations
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
-
Build the optimal equipment set for the player's character.
|
|
5
|
+
Build the optimal equipment set for the player's character. Use the character profile from the conversation context. If no profile is available yet, ask for it first.
|
|
6
6
|
|
|
7
7
|
Ask the player:
|
|
8
8
|
1. Situation: hunting / boss / pvp / training / specific quest
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tibiaway-ai",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "Tu guía de Tibia powered by AI. Your path through Tibia, powered by AI.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"tibiaway": "./bin/tibiaway.js",
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
"chalk": "^5.3.0",
|
|
27
27
|
"inquirer": "^9.2.0",
|
|
28
28
|
"node-fetch": "^3.3.0",
|
|
29
|
+
"openai": "^4.47.0",
|
|
29
30
|
"ora": "^7.0.0"
|
|
30
31
|
},
|
|
31
32
|
"scripts": {
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, copyFileSync, readdirSync } from 'fs';
|
|
2
|
+
import { homedir } from 'os';
|
|
3
|
+
import { join, resolve, dirname } from 'path';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
import inquirer from 'inquirer';
|
|
6
|
+
import chalk from 'chalk';
|
|
7
|
+
import { success, error, info, highlight, separator } from '../utils/banner.js';
|
|
8
|
+
|
|
9
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
const PROJECT_ROOT = resolve(__dirname, '..', '..');
|
|
11
|
+
|
|
12
|
+
const DESTINATIONS = {
|
|
13
|
+
global: join(homedir(), '.config', 'opencode'),
|
|
14
|
+
local: join(process.cwd(), '.opencode'),
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export async function handleSetupOpencode() {
|
|
18
|
+
console.log(chalk.hex('#FF6B1A').bold('🔌 SETUP OPENCODE\n'));
|
|
19
|
+
info('Instala TibiaWayAI como agente y comandos en OpenCode.');
|
|
20
|
+
info('No necesitas npm install — OpenCode usa su propio modelo de IA.');
|
|
21
|
+
console.log('');
|
|
22
|
+
|
|
23
|
+
const { destination } = await inquirer.prompt([
|
|
24
|
+
{
|
|
25
|
+
type: 'list',
|
|
26
|
+
name: 'destination',
|
|
27
|
+
message: highlight('¿Dónde quieres instalar el agente?'),
|
|
28
|
+
choices: [
|
|
29
|
+
{
|
|
30
|
+
name: `${chalk.white('Global')} ${chalk.hex('#888')(`(${join(homedir(), '.config/opencode/')}) — disponible en todos los proyectos`)}`,
|
|
31
|
+
value: 'global',
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
name: `${chalk.white('Local')} ${chalk.hex('#888')('(.opencode/ en este directorio) — solo en este proyecto')}`,
|
|
35
|
+
value: 'local',
|
|
36
|
+
},
|
|
37
|
+
],
|
|
38
|
+
},
|
|
39
|
+
]);
|
|
40
|
+
|
|
41
|
+
const baseDir = DESTINATIONS[destination];
|
|
42
|
+
const agentsDir = join(baseDir, 'agents');
|
|
43
|
+
const commandsDir = join(baseDir, 'commands');
|
|
44
|
+
|
|
45
|
+
// Crear directorios si no existen
|
|
46
|
+
for (const dir of [agentsDir, commandsDir]) {
|
|
47
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const installed = [];
|
|
51
|
+
const skipped = [];
|
|
52
|
+
|
|
53
|
+
// Copiar agent
|
|
54
|
+
const agentSrc = join(PROJECT_ROOT, 'agents', 'tibiaway.md');
|
|
55
|
+
const agentDst = join(agentsDir, 'tibiaway.md');
|
|
56
|
+
if (existsSync(agentSrc)) {
|
|
57
|
+
copyFileSync(agentSrc, agentDst);
|
|
58
|
+
installed.push(`agents/tibiaway.md → ${agentDst}`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Copiar todos los comandos
|
|
62
|
+
const commandsSourceDir = join(PROJECT_ROOT, 'commands');
|
|
63
|
+
if (existsSync(commandsSourceDir)) {
|
|
64
|
+
for (const file of readdirSync(commandsSourceDir)) {
|
|
65
|
+
if (!file.endsWith('.md')) continue;
|
|
66
|
+
const src = join(commandsSourceDir, file);
|
|
67
|
+
const dst = join(commandsDir, file);
|
|
68
|
+
copyFileSync(src, dst);
|
|
69
|
+
installed.push(`commands/${file} → ${dst}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (installed.length === 0) {
|
|
74
|
+
error('No se encontraron archivos para instalar. ¿Estás en el directorio correcto?');
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
console.log('');
|
|
79
|
+
separator();
|
|
80
|
+
console.log('');
|
|
81
|
+
console.log(chalk.hex('#FF6B1A').bold('✓ Archivos instalados:\n'));
|
|
82
|
+
for (const f of installed) {
|
|
83
|
+
console.log(` ${chalk.hex('#4ade80')('✓')} ${chalk.hex('#888')(f)}`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
console.log('');
|
|
87
|
+
separator();
|
|
88
|
+
console.log('');
|
|
89
|
+
console.log(chalk.hex('#FF6B1A').bold('¡Listo! Así lo usas en OpenCode:\n'));
|
|
90
|
+
|
|
91
|
+
const steps = [
|
|
92
|
+
['1.', 'Abre OpenCode en cualquier directorio'],
|
|
93
|
+
['2.', 'Selecciona el agente TibiaWayAI en la lista de agentes'],
|
|
94
|
+
['3.', 'Escribe tu personaje si te lo pide (nombre, vocación, level)'],
|
|
95
|
+
['4.', 'Usa los comandos:'],
|
|
96
|
+
];
|
|
97
|
+
for (const [n, s] of steps) {
|
|
98
|
+
console.log(` ${chalk.hex('#FF6B1A')(n)} ${chalk.white(s)}`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const cmds = ['/hunt', '/profit', '/quest', '/set', '/boss', '/diary', '/analyze'];
|
|
102
|
+
console.log('');
|
|
103
|
+
console.log(' ' + cmds.map(c => chalk.hex('#FF6B1A')(c)).join(' '));
|
|
104
|
+
console.log('');
|
|
105
|
+
|
|
106
|
+
info('OpenCode usa su propio modelo de IA configurado — no necesitas API key adicional.');
|
|
107
|
+
console.log('');
|
|
108
|
+
}
|
package/src/installer.js
CHANGED
|
@@ -1,64 +1,219 @@
|
|
|
1
1
|
import inquirer from 'inquirer';
|
|
2
2
|
import chalk from 'chalk';
|
|
3
|
-
import
|
|
3
|
+
import fetch from 'node-fetch';
|
|
4
|
+
import { showBanner, success, error, info, highlight, separator, warn } from './utils/banner.js';
|
|
4
5
|
import { setupCharacter } from './utils/character.js';
|
|
5
|
-
import { setApiKey, getApiKey, writeConfig, readConfig } from './utils/config.js';
|
|
6
|
+
import { setApiKey, getApiKey, setOllamaConfig, getOllamaConfig, getProvider, writeConfig, readConfig } from './utils/config.js';
|
|
7
|
+
|
|
8
|
+
const OLLAMA_MODELS = [
|
|
9
|
+
{ name: 'llama3.2 (Recomendado · 3 GB · rápido y equilibrado)', value: 'llama3.2' },
|
|
10
|
+
{ name: 'mistral (Buena calidad · 4 GB)', value: 'mistral' },
|
|
11
|
+
{ name: 'qwen2.5 (Excelente en español · 4 GB)', value: 'qwen2.5' },
|
|
12
|
+
{ name: 'deepseek-r1 (Razonamiento avanzado · 7 GB)', value: 'deepseek-r1' },
|
|
13
|
+
{ name: 'Otro (ingresar nombre manualmente)', value: '__custom__' },
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
async function checkOllamaConnection(url) {
|
|
17
|
+
try {
|
|
18
|
+
const res = await fetch(`${url}/api/tags`, { timeout: 4000 });
|
|
19
|
+
return res.ok;
|
|
20
|
+
} catch {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function setupAnthropic(existingKey) {
|
|
26
|
+
if (existingKey) {
|
|
27
|
+
success('API key de Anthropic ya configurada.');
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
console.log('');
|
|
32
|
+
info('Necesitas una API key de Anthropic para usar este proveedor.');
|
|
33
|
+
info(`Obtén la tuya en: ${chalk.hex('#FF6B1A')('https://console.anthropic.com')}`);
|
|
34
|
+
console.log('');
|
|
35
|
+
|
|
36
|
+
const { key } = await inquirer.prompt([
|
|
37
|
+
{
|
|
38
|
+
type: 'password',
|
|
39
|
+
name: 'key',
|
|
40
|
+
message: highlight('Ingresa tu Anthropic API key:'),
|
|
41
|
+
mask: '*',
|
|
42
|
+
validate: (input) => {
|
|
43
|
+
if (!input.trim()) return 'La API key no puede estar vacía.';
|
|
44
|
+
if (!input.trim().startsWith('sk-')) return 'La API key debe empezar con "sk-".';
|
|
45
|
+
return true;
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
]);
|
|
49
|
+
|
|
50
|
+
setApiKey(key.trim());
|
|
51
|
+
success('API key guardada correctamente.');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function setupOllama() {
|
|
55
|
+
console.log('');
|
|
56
|
+
info(`Ollama corre modelos de IA ${chalk.hex('#4ade80').bold('gratis')} en tu propia computadora.`);
|
|
57
|
+
info(`Si no lo tienes instalado, descárgalo en: ${chalk.hex('#FF6B1A')('https://ollama.com')}`);
|
|
58
|
+
info(`Luego instala el modelo con: ${chalk.hex('#FF6B1A')('ollama pull llama3.2')}`);
|
|
59
|
+
console.log('');
|
|
60
|
+
|
|
61
|
+
// Elegir modelo
|
|
62
|
+
const { modelChoice } = await inquirer.prompt([
|
|
63
|
+
{
|
|
64
|
+
type: 'list',
|
|
65
|
+
name: 'modelChoice',
|
|
66
|
+
message: highlight('¿Qué modelo de Ollama quieres usar?'),
|
|
67
|
+
choices: OLLAMA_MODELS,
|
|
68
|
+
},
|
|
69
|
+
]);
|
|
70
|
+
|
|
71
|
+
let model = modelChoice;
|
|
72
|
+
if (modelChoice === '__custom__') {
|
|
73
|
+
const { customModel } = await inquirer.prompt([
|
|
74
|
+
{
|
|
75
|
+
type: 'input',
|
|
76
|
+
name: 'customModel',
|
|
77
|
+
message: highlight('Nombre del modelo (ej: phi3, gemma2):'),
|
|
78
|
+
validate: (i) => i.trim().length > 0 || 'Ingresa un nombre de modelo.',
|
|
79
|
+
},
|
|
80
|
+
]);
|
|
81
|
+
model = customModel.trim();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// URL del servidor Ollama
|
|
85
|
+
const { url } = await inquirer.prompt([
|
|
86
|
+
{
|
|
87
|
+
type: 'input',
|
|
88
|
+
name: 'url',
|
|
89
|
+
message: highlight('URL del servidor Ollama:'),
|
|
90
|
+
default: 'http://localhost:11434',
|
|
91
|
+
},
|
|
92
|
+
]);
|
|
93
|
+
|
|
94
|
+
const ollamaUrl = url.trim().replace(/\/$/, '');
|
|
95
|
+
|
|
96
|
+
// Verificar conexión
|
|
97
|
+
const spinner = (await import('ora')).default({ text: 'Verificando conexión a Ollama...', color: 'yellow' }).start();
|
|
98
|
+
const connected = await checkOllamaConnection(ollamaUrl);
|
|
99
|
+
spinner.stop();
|
|
100
|
+
|
|
101
|
+
if (!connected) {
|
|
102
|
+
warn(`No se pudo conectar a Ollama en ${ollamaUrl}`);
|
|
103
|
+
warn('Asegúrate de que Ollama esté corriendo: ollama serve');
|
|
104
|
+
console.log('');
|
|
105
|
+
const { continueAnyway } = await inquirer.prompt([
|
|
106
|
+
{
|
|
107
|
+
type: 'confirm',
|
|
108
|
+
name: 'continueAnyway',
|
|
109
|
+
message: highlight('¿Continuar de todas formas y configurar Ollama después?'),
|
|
110
|
+
default: true,
|
|
111
|
+
},
|
|
112
|
+
]);
|
|
113
|
+
if (!continueAnyway) {
|
|
114
|
+
info('Puedes volver a configurar con: tibiaway setup');
|
|
115
|
+
process.exit(0);
|
|
116
|
+
}
|
|
117
|
+
} else {
|
|
118
|
+
success(`Ollama conectado en ${ollamaUrl}`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
setOllamaConfig({ model, url: ollamaUrl });
|
|
122
|
+
success(`Modelo configurado: ${highlight(model)}`);
|
|
123
|
+
}
|
|
6
124
|
|
|
7
125
|
export async function runInstaller() {
|
|
8
126
|
showBanner();
|
|
9
|
-
|
|
10
127
|
console.log(chalk.hex('#f0f0f0')('Bienvenido a TibiaWayAI. Vamos a configurar tu asistente.\n'));
|
|
11
128
|
separator();
|
|
12
129
|
console.log('');
|
|
13
130
|
|
|
14
|
-
//
|
|
15
|
-
const
|
|
16
|
-
let apiKey = existingKey;
|
|
131
|
+
// ── PASO 1: Elegir proveedor ──────────────────────────────────────────────
|
|
132
|
+
const currentProvider = getProvider();
|
|
17
133
|
|
|
18
|
-
if (
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
134
|
+
if (currentProvider) {
|
|
135
|
+
const providerLabel = currentProvider === 'anthropic'
|
|
136
|
+
? chalk.hex('#FF6B1A')('Anthropic Claude')
|
|
137
|
+
: chalk.hex('#4ade80')('Ollama (local)');
|
|
138
|
+
success(`Proveedor ya configurado: ${providerLabel}`);
|
|
22
139
|
|
|
23
|
-
const {
|
|
140
|
+
const { change } = await inquirer.prompt([
|
|
24
141
|
{
|
|
25
|
-
type: '
|
|
26
|
-
name: '
|
|
27
|
-
message: highlight('
|
|
28
|
-
|
|
29
|
-
validate: (input) => {
|
|
30
|
-
if (!input.trim()) return 'La API key no puede estar vacía.';
|
|
31
|
-
if (!input.trim().startsWith('sk-')) return 'La API key debe empezar con "sk-".';
|
|
32
|
-
return true;
|
|
33
|
-
},
|
|
142
|
+
type: 'confirm',
|
|
143
|
+
name: 'change',
|
|
144
|
+
message: highlight('¿Quieres cambiar el proveedor de IA?'),
|
|
145
|
+
default: false,
|
|
34
146
|
},
|
|
35
147
|
]);
|
|
36
148
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
149
|
+
if (!change) {
|
|
150
|
+
await continueSetup();
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
40
153
|
console.log('');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const { provider } = await inquirer.prompt([
|
|
157
|
+
{
|
|
158
|
+
type: 'list',
|
|
159
|
+
name: 'provider',
|
|
160
|
+
message: highlight('¿Qué proveedor de IA quieres usar?'),
|
|
161
|
+
choices: [
|
|
162
|
+
{
|
|
163
|
+
name: `${chalk.hex('#FF6B1A').bold('Anthropic Claude')} ${chalk.hex('#888')('· Mejor calidad · ~$0.01/consulta · Requiere API key')}`,
|
|
164
|
+
value: 'anthropic',
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
name: `${chalk.hex('#4ade80').bold('Ollama (local)')} ${chalk.hex('#888')('· Completamente gratis · Corre en tu PC · Sin internet')}`,
|
|
168
|
+
value: 'ollama',
|
|
169
|
+
},
|
|
170
|
+
],
|
|
171
|
+
},
|
|
172
|
+
]);
|
|
173
|
+
|
|
174
|
+
console.log('');
|
|
175
|
+
separator();
|
|
176
|
+
|
|
177
|
+
if (provider === 'anthropic') {
|
|
178
|
+
await setupAnthropic(getApiKey());
|
|
41
179
|
} else {
|
|
42
|
-
|
|
43
|
-
console.log('');
|
|
180
|
+
await setupOllama();
|
|
44
181
|
}
|
|
45
182
|
|
|
46
|
-
|
|
183
|
+
await continueSetup();
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async function continueSetup() {
|
|
187
|
+
// ── PASO 2: Personaje ─────────────────────────────────────────────────────
|
|
47
188
|
const config = readConfig();
|
|
189
|
+
|
|
48
190
|
if (!config.character) {
|
|
191
|
+
console.log('');
|
|
49
192
|
separator();
|
|
50
193
|
console.log('');
|
|
51
194
|
info('Ahora vamos a vincular tu personaje de Tibia.');
|
|
52
195
|
console.log('');
|
|
53
196
|
await setupCharacter();
|
|
54
197
|
} else {
|
|
198
|
+
console.log('');
|
|
55
199
|
success(`Personaje ya configurado: ${highlight(config.character.name)} (${config.character.vocation} lv ${config.character.level})`);
|
|
56
200
|
}
|
|
57
201
|
|
|
202
|
+
// ── LISTO ─────────────────────────────────────────────────────────────────
|
|
58
203
|
console.log('');
|
|
59
204
|
separator();
|
|
60
205
|
console.log('');
|
|
61
206
|
console.log(chalk.hex('#FF6B1A').bold('¡TibiaWayAI está listo!'));
|
|
207
|
+
console.log('');
|
|
208
|
+
|
|
209
|
+
const provider = getProvider();
|
|
210
|
+
if (provider === 'ollama') {
|
|
211
|
+
const { model } = getOllamaConfig();
|
|
212
|
+
info(`Usando Ollama con el modelo ${highlight(model)}`);
|
|
213
|
+
} else {
|
|
214
|
+
info('Usando Anthropic Claude (claude-sonnet-4-20250514)');
|
|
215
|
+
}
|
|
216
|
+
|
|
62
217
|
console.log('');
|
|
63
218
|
console.log(chalk.hex('#f0f0f0')('Comandos disponibles:'));
|
|
64
219
|
const cmds = [
|
package/src/standalone.js
CHANGED
|
@@ -10,6 +10,7 @@ import { handleBoss } from './commands/boss.js';
|
|
|
10
10
|
import { handleDiary } from './commands/diary.js';
|
|
11
11
|
import { handleAnalyze } from './commands/analyze.js';
|
|
12
12
|
import { handleSync } from './commands/sync.js';
|
|
13
|
+
import { handleSetupOpencode } from './commands/setup-opencode.js';
|
|
13
14
|
|
|
14
15
|
const COMMANDS = {
|
|
15
16
|
hunt: handleHunt,
|
|
@@ -22,6 +23,8 @@ const COMMANDS = {
|
|
|
22
23
|
sync: handleSync,
|
|
23
24
|
setup: runInstaller,
|
|
24
25
|
install: runInstaller,
|
|
26
|
+
'setup-opencode': handleSetupOpencode,
|
|
27
|
+
opencode: handleSetupOpencode,
|
|
25
28
|
};
|
|
26
29
|
|
|
27
30
|
function printWelcome() {
|
|
@@ -63,6 +66,7 @@ function printHelp() {
|
|
|
63
66
|
['analyze <archivo>', 'Analiza screenshot o notas (.png, .jpg, .txt)'],
|
|
64
67
|
['sync', 'Actualiza tu perfil desde tibiadata.com'],
|
|
65
68
|
['setup', 'Configuración inicial (API key + personaje)'],
|
|
69
|
+
['setup-opencode', 'Instala el agente en OpenCode (sin npm install)'],
|
|
66
70
|
];
|
|
67
71
|
|
|
68
72
|
for (const [cmd, desc] of cmds) {
|
|
@@ -102,6 +106,12 @@ export async function run(args) {
|
|
|
102
106
|
return;
|
|
103
107
|
}
|
|
104
108
|
|
|
109
|
+
// setup-opencode no requiere tener el CLI configurado
|
|
110
|
+
if (command === 'setup-opencode' || command === 'opencode') {
|
|
111
|
+
await handleSetupOpencode();
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
105
115
|
// Auto-setup si falta config y no es sync
|
|
106
116
|
if (!isConfigured() && command !== 'sync') {
|
|
107
117
|
info('Primera vez que ejecutas TibiaWayAI. Vamos a configurarlo.');
|
package/src/utils/api.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import Anthropic from '@anthropic-ai/sdk';
|
|
2
|
+
import OpenAI from 'openai';
|
|
2
3
|
import { readFileSync } from 'fs';
|
|
3
|
-
import { getApiKey, getCharacter } from './config.js';
|
|
4
|
-
import { error } from './banner.js';
|
|
4
|
+
import { getApiKey, getCharacter, getProvider, getOllamaConfig } from './config.js';
|
|
5
|
+
import { error, warn } from './banner.js';
|
|
5
6
|
|
|
6
|
-
const
|
|
7
|
+
const ANTHROPIC_MODEL = 'claude-sonnet-4-20250514';
|
|
7
8
|
const MAX_TOKENS = 2048;
|
|
8
9
|
|
|
9
10
|
function buildSystemPrompt(extraContext = '') {
|
|
@@ -33,32 +34,66 @@ Tienes personalidad de veterano tibian: directo, preciso, con humor de la comuni
|
|
|
33
34
|
${extraContext}`;
|
|
34
35
|
}
|
|
35
36
|
|
|
36
|
-
function
|
|
37
|
+
function getAnthropicClient() {
|
|
37
38
|
const apiKey = getApiKey();
|
|
38
39
|
if (!apiKey) {
|
|
39
40
|
error('No se encontró API key de Anthropic.');
|
|
40
|
-
console.error('
|
|
41
|
-
console.error('O exporta la variable: export ANTHROPIC_API_KEY=tu_key');
|
|
41
|
+
console.error('Reconfigura con: tibiaway setup');
|
|
42
42
|
process.exit(1);
|
|
43
43
|
}
|
|
44
44
|
return new Anthropic({ apiKey });
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
function getOllamaClient() {
|
|
48
|
+
const { url } = getOllamaConfig();
|
|
49
|
+
return new OpenAI({
|
|
50
|
+
baseURL: `${url}/v1`,
|
|
51
|
+
apiKey: 'ollama',
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function getClient() {
|
|
56
|
+
const provider = getProvider();
|
|
57
|
+
if (provider === 'ollama') return { type: 'ollama', client: getOllamaClient() };
|
|
58
|
+
if (provider === 'anthropic') return { type: 'anthropic', client: getAnthropicClient() };
|
|
59
|
+
|
|
60
|
+
error('No hay proveedor de IA configurado. Ejecuta: tibiaway setup');
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
|
|
47
64
|
export async function askClaude(userMessage, extraContext = '') {
|
|
48
|
-
const client = getClient();
|
|
65
|
+
const { type, client } = getClient();
|
|
66
|
+
const systemPrompt = buildSystemPrompt(extraContext);
|
|
67
|
+
|
|
68
|
+
if (type === 'anthropic') {
|
|
69
|
+
const response = await client.messages.create({
|
|
70
|
+
model: ANTHROPIC_MODEL,
|
|
71
|
+
max_tokens: MAX_TOKENS,
|
|
72
|
+
system: systemPrompt,
|
|
73
|
+
messages: [{ role: 'user', content: userMessage }],
|
|
74
|
+
});
|
|
75
|
+
return response.content[0]?.text || '';
|
|
76
|
+
}
|
|
49
77
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
messages: [
|
|
78
|
+
// Ollama — usa API compatible con OpenAI
|
|
79
|
+
const { model } = getOllamaConfig();
|
|
80
|
+
const response = await client.chat.completions.create({
|
|
81
|
+
model,
|
|
82
|
+
messages: [
|
|
83
|
+
{ role: 'system', content: systemPrompt },
|
|
84
|
+
{ role: 'user', content: userMessage },
|
|
85
|
+
],
|
|
55
86
|
});
|
|
56
|
-
|
|
57
|
-
return response.content[0]?.text || '';
|
|
87
|
+
return response.choices[0]?.message?.content || '';
|
|
58
88
|
}
|
|
59
89
|
|
|
60
90
|
export async function askClaudeWithImage(userMessage, imagePath, extraContext = '') {
|
|
61
|
-
const client = getClient();
|
|
91
|
+
const { type, client } = getClient();
|
|
92
|
+
|
|
93
|
+
if (type === 'ollama') {
|
|
94
|
+
warn('El análisis de imágenes no está disponible con Ollama. Procesando solo el texto...');
|
|
95
|
+
return askClaude(`[El usuario quería analizar una imagen en ${imagePath}, pero Ollama no soporta visión en este momento]\n\n${userMessage}`, extraContext);
|
|
96
|
+
}
|
|
62
97
|
|
|
63
98
|
const imageBuffer = readFileSync(imagePath);
|
|
64
99
|
const base64Image = imageBuffer.toString('base64');
|
|
@@ -67,17 +102,14 @@ export async function askClaudeWithImage(userMessage, imagePath, extraContext =
|
|
|
67
102
|
const mediaType = mediaTypeMap[ext] || 'image/png';
|
|
68
103
|
|
|
69
104
|
const response = await client.messages.create({
|
|
70
|
-
model:
|
|
105
|
+
model: ANTHROPIC_MODEL,
|
|
71
106
|
max_tokens: MAX_TOKENS,
|
|
72
107
|
system: buildSystemPrompt(extraContext),
|
|
73
108
|
messages: [
|
|
74
109
|
{
|
|
75
110
|
role: 'user',
|
|
76
111
|
content: [
|
|
77
|
-
{
|
|
78
|
-
type: 'image',
|
|
79
|
-
source: { type: 'base64', media_type: mediaType, data: base64Image },
|
|
80
|
-
},
|
|
112
|
+
{ type: 'image', source: { type: 'base64', media_type: mediaType, data: base64Image } },
|
|
81
113
|
{ type: 'text', text: userMessage },
|
|
82
114
|
],
|
|
83
115
|
},
|
|
@@ -88,22 +120,43 @@ export async function askClaudeWithImage(userMessage, imagePath, extraContext =
|
|
|
88
120
|
}
|
|
89
121
|
|
|
90
122
|
export async function streamClaude(userMessage, extraContext = '', onChunk) {
|
|
91
|
-
const client = getClient();
|
|
123
|
+
const { type, client } = getClient();
|
|
124
|
+
const systemPrompt = buildSystemPrompt(extraContext);
|
|
125
|
+
|
|
126
|
+
if (type === 'anthropic') {
|
|
127
|
+
const stream = client.messages.stream({
|
|
128
|
+
model: ANTHROPIC_MODEL,
|
|
129
|
+
max_tokens: MAX_TOKENS,
|
|
130
|
+
system: systemPrompt,
|
|
131
|
+
messages: [{ role: 'user', content: userMessage }],
|
|
132
|
+
});
|
|
133
|
+
let fullText = '';
|
|
134
|
+
for await (const chunk of stream) {
|
|
135
|
+
if (chunk.type === 'content_block_delta' && chunk.delta?.type === 'text_delta') {
|
|
136
|
+
fullText += chunk.delta.text;
|
|
137
|
+
if (onChunk) onChunk(chunk.delta.text);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return fullText;
|
|
141
|
+
}
|
|
92
142
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
143
|
+
// Ollama streaming
|
|
144
|
+
const { model } = getOllamaConfig();
|
|
145
|
+
const stream = await client.chat.completions.create({
|
|
146
|
+
model,
|
|
147
|
+
stream: true,
|
|
148
|
+
messages: [
|
|
149
|
+
{ role: 'system', content: systemPrompt },
|
|
150
|
+
{ role: 'user', content: userMessage },
|
|
151
|
+
],
|
|
98
152
|
});
|
|
99
|
-
|
|
100
153
|
let fullText = '';
|
|
101
154
|
for await (const chunk of stream) {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
155
|
+
const text = chunk.choices[0]?.delta?.content || '';
|
|
156
|
+
if (text) {
|
|
157
|
+
fullText += text;
|
|
158
|
+
if (onChunk) onChunk(text);
|
|
105
159
|
}
|
|
106
160
|
}
|
|
107
|
-
|
|
108
161
|
return fullText;
|
|
109
162
|
}
|
package/src/utils/config.js
CHANGED
|
@@ -6,8 +6,11 @@ const CONFIG_DIR = join(homedir(), '.tibiaway');
|
|
|
6
6
|
const CONFIG_FILE = join(CONFIG_DIR, 'config.json');
|
|
7
7
|
|
|
8
8
|
const DEFAULT_CONFIG = {
|
|
9
|
-
|
|
9
|
+
provider: null,
|
|
10
10
|
anthropic_api_key: null,
|
|
11
|
+
ollama_model: 'llama3.2',
|
|
12
|
+
ollama_url: 'http://localhost:11434',
|
|
13
|
+
character: null,
|
|
11
14
|
language: 'español',
|
|
12
15
|
diary: [],
|
|
13
16
|
last_sync: null,
|
|
@@ -49,6 +52,19 @@ export function setCharacter(characterData) {
|
|
|
49
52
|
writeConfig(config);
|
|
50
53
|
}
|
|
51
54
|
|
|
55
|
+
export function getProvider() {
|
|
56
|
+
const config = readConfig();
|
|
57
|
+
// retrocompatibilidad: si hay api key guardada pero no provider, asumimos anthropic
|
|
58
|
+
if (!config.provider && config.anthropic_api_key) return 'anthropic';
|
|
59
|
+
return config.provider || null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function setProvider(provider) {
|
|
63
|
+
const config = readConfig();
|
|
64
|
+
config.provider = provider;
|
|
65
|
+
writeConfig(config);
|
|
66
|
+
}
|
|
67
|
+
|
|
52
68
|
export function getApiKey() {
|
|
53
69
|
return process.env.ANTHROPIC_API_KEY || readConfig().anthropic_api_key || null;
|
|
54
70
|
}
|
|
@@ -56,16 +72,30 @@ export function getApiKey() {
|
|
|
56
72
|
export function setApiKey(key) {
|
|
57
73
|
const config = readConfig();
|
|
58
74
|
config.anthropic_api_key = key;
|
|
75
|
+
config.provider = 'anthropic';
|
|
76
|
+
writeConfig(config);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function getOllamaConfig() {
|
|
80
|
+
const config = readConfig();
|
|
81
|
+
return {
|
|
82
|
+
model: config.ollama_model || 'llama3.2',
|
|
83
|
+
url: config.ollama_url || 'http://localhost:11434',
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function setOllamaConfig({ model, url }) {
|
|
88
|
+
const config = readConfig();
|
|
89
|
+
config.ollama_model = model;
|
|
90
|
+
config.ollama_url = url;
|
|
91
|
+
config.provider = 'ollama';
|
|
59
92
|
writeConfig(config);
|
|
60
93
|
}
|
|
61
94
|
|
|
62
95
|
export function addDiaryEntry(entry) {
|
|
63
96
|
const config = readConfig();
|
|
64
97
|
if (!Array.isArray(config.diary)) config.diary = [];
|
|
65
|
-
config.diary.push({
|
|
66
|
-
date: new Date().toISOString(),
|
|
67
|
-
...entry,
|
|
68
|
-
});
|
|
98
|
+
config.diary.push({ date: new Date().toISOString(), ...entry });
|
|
69
99
|
writeConfig(config);
|
|
70
100
|
}
|
|
71
101
|
|
|
@@ -76,7 +106,11 @@ export function getDiary() {
|
|
|
76
106
|
|
|
77
107
|
export function isConfigured() {
|
|
78
108
|
const config = readConfig();
|
|
79
|
-
|
|
109
|
+
if (!config.character) return false;
|
|
110
|
+
const provider = getProvider();
|
|
111
|
+
if (provider === 'ollama') return true;
|
|
112
|
+
if (provider === 'anthropic') return !!(config.anthropic_api_key || process.env.ANTHROPIC_API_KEY);
|
|
113
|
+
return false;
|
|
80
114
|
}
|
|
81
115
|
|
|
82
116
|
export function getConfigPath() {
|