open-jarvis-cli 1.1.0 → 1.2.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/bin/jarvis.js +10 -1
- package/package.json +5 -20
- package/src/brand.js +8 -0
- package/src/config.js +5 -1
- package/src/greeting.js +50 -0
- package/src/llm.js +48 -2
- package/src/providers.js +11 -1
- package/src/repl.js +20 -12
- package/src/setup.js +46 -13
- package/src/skills.js +4 -1
- package/src/ui.js +8 -10
- package/src/voice.js +1 -1
package/bin/jarvis.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import fs from 'node:fs';
|
|
3
|
+
import { spawn } from 'node:child_process';
|
|
3
4
|
import { fileURLToPath } from 'node:url';
|
|
4
5
|
import path from 'node:path';
|
|
5
6
|
import * as cfgMod from '../src/config.js';
|
|
6
7
|
import * as memory from '../src/memory.js';
|
|
7
8
|
import { startRepl, ask } from '../src/repl.js';
|
|
8
9
|
import { stats, trySkill } from '../src/skills.js';
|
|
9
|
-
import { runSetup, addKey, needsSetup, commandsHelp } from '../src/setup.js';
|
|
10
|
+
import { runSetup, addKey, changeModel, needsSetup, commandsHelp } from '../src/setup.js';
|
|
10
11
|
import { byId } from '../src/providers.js';
|
|
11
12
|
import { green, dim, red, cyan } from '../src/ui.js';
|
|
12
13
|
|
|
@@ -19,6 +20,7 @@ ${commandsHelp()}
|
|
|
19
20
|
${cyan('More')}
|
|
20
21
|
jarvis ask "your question" one-shot answer
|
|
21
22
|
jarvis stats CPU / RAM / OS
|
|
23
|
+
jarvis update install the newest version
|
|
22
24
|
jarvis memory list remembered facts
|
|
23
25
|
jarvis config get KEY | unset KEY | path
|
|
24
26
|
|
|
@@ -56,6 +58,13 @@ try {
|
|
|
56
58
|
else if (cmd === 'config') handleConfig(rest);
|
|
57
59
|
else if (cmd === 'setup' || cmd === 'switch') await runSetup();
|
|
58
60
|
else if (cmd === 'key') await addKey();
|
|
61
|
+
else if (cmd === 'model') await changeModel();
|
|
62
|
+
else if (cmd === 'update') {
|
|
63
|
+
console.log(dim(`Updating ${pkg.name} …`));
|
|
64
|
+
const child = spawn('npm', ['install', '-g', `${pkg.name}@latest`], { stdio: 'inherit', shell: true });
|
|
65
|
+
child.on('exit', (code) => process.exit(code ?? 0));
|
|
66
|
+
await new Promise(() => {});
|
|
67
|
+
}
|
|
59
68
|
else if (cmd === 'ask') {
|
|
60
69
|
const q = rest.join(' ').trim();
|
|
61
70
|
if (!q) console.log(red('Usage: jarvis ask "question"'));
|
package/package.json
CHANGED
|
@@ -1,32 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "open-jarvis-cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "JARVIS-style AI assistant for your terminal. BYOK, streaming, free-tier friendly.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"jarvis": "bin/jarvis.js",
|
|
8
8
|
"openjarvispro": "bin/jarvis.js"
|
|
9
9
|
},
|
|
10
|
-
"files": [
|
|
11
|
-
|
|
12
|
-
"src",
|
|
13
|
-
"README.md"
|
|
14
|
-
],
|
|
15
|
-
"engines": {
|
|
16
|
-
"node": ">=18"
|
|
17
|
-
},
|
|
10
|
+
"files": ["bin", "src", "README.md"],
|
|
11
|
+
"engines": { "node": ">=18" },
|
|
18
12
|
"author": "Rongon Kairy",
|
|
19
|
-
"repository": {
|
|
20
|
-
"type": "git",
|
|
21
|
-
"url": "git+https://github.com/RongonKairy/openjarvispro.git"
|
|
22
|
-
},
|
|
13
|
+
"repository": { "type": "git", "url": "git+https://github.com/RongonKairy/openjarvispro.git" },
|
|
23
14
|
"homepage": "https://github.com/RongonKairy",
|
|
24
|
-
"keywords": [
|
|
25
|
-
"jarvis",
|
|
26
|
-
"ai",
|
|
27
|
-
"cli",
|
|
28
|
-
"assistant",
|
|
29
|
-
"terminal"
|
|
30
|
-
],
|
|
15
|
+
"keywords": ["jarvis", "ai", "cli", "assistant", "terminal"],
|
|
31
16
|
"license": "MIT"
|
|
32
17
|
}
|
package/src/brand.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// Everything you may want to edit (links, credits, password) lives here.
|
|
2
|
+
export const BRAND = {
|
|
3
|
+
creator: 'Rongon Kairy',
|
|
4
|
+
creatorGithub: 'https://github.com/RongonKairy',
|
|
5
|
+
officialGithub: 'https://github.com/Open-Jarvis-Ultra',
|
|
6
|
+
dashboard: 'https://openjarvis-2366000.vercel.app/',
|
|
7
|
+
dashboardPassword: 'Jarvis2599Open',
|
|
8
|
+
};
|
package/src/config.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import os from 'node:os';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
-
import { byId } from './providers.js';
|
|
4
|
+
import { byId, isProviderName } from './providers.js';
|
|
5
5
|
|
|
6
6
|
export const DIR = path.join(os.homedir(), '.open-jarvis');
|
|
7
7
|
export const FILE = path.join(DIR, 'config.json');
|
|
@@ -35,6 +35,7 @@ export function load() {
|
|
|
35
35
|
const prof = (s.profiles && s.profiles[prov]) || {};
|
|
36
36
|
cfg.AI_API_KEY = process.env.AI_API_KEY || prof.key || (prov === 'auto' ? s.AI_API_KEY : '') || '';
|
|
37
37
|
cfg.AI_MODEL = prof.model || s.AI_MODEL || '';
|
|
38
|
+
if (prov !== 'ollama' && isProviderName(cfg.AI_MODEL)) cfg.AI_MODEL = ''; // ignore a saved provider-name-as-model mistake
|
|
38
39
|
cfg.CUSTOM_URL = prof.url || s.CUSTOM_URL || '';
|
|
39
40
|
return cfg;
|
|
40
41
|
}
|
|
@@ -57,6 +58,9 @@ export function saveProfile(id, { key, model, url } = {}) {
|
|
|
57
58
|
export function setValue(key, value) {
|
|
58
59
|
key = key.toUpperCase();
|
|
59
60
|
if (!KEYS.includes(key)) throw new Error(`Unknown key "${key}". Valid keys: ${KEYS.join(', ')}`);
|
|
61
|
+
if (key === 'AI_MODEL' && isProviderName(value) && raw().AI_PROVIDER !== 'ollama') {
|
|
62
|
+
throw new Error(`"${value}" is a provider name, not a model. Run jarvis model and pick one from the list.`);
|
|
63
|
+
}
|
|
60
64
|
if (key === 'AI_PROVIDER' && value !== 'auto' && !byId(value)) throw new Error(`Unknown provider "${value}".`);
|
|
61
65
|
update((o) => {
|
|
62
66
|
const p = o.AI_PROVIDER;
|
package/src/greeting.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { BRAND } from './brand.js';
|
|
2
|
+
import { green, cyan, dim, bold, purple, blue } from './ui.js';
|
|
3
|
+
|
|
4
|
+
// "hi", "hello jarvis", "hey!", "good morning", "salam", "namaste" … (whole message only)
|
|
5
|
+
const GREETING = /^(h+i+|h+e+l+o+|h+e+y+|h+y+|hlw|hallo|helo|hii+|yo+|sup|salam|assalamu\s?alaikum|assalamualaikum|namaste|namaskar|good\s(morning|afternoon|evening|day)|greetings|হাই|হ্যালো|হেলো|সালাম|আসসালামু আলাইকুম|নমস্কার)(\s+(jarvis|there|sir|bro|boss|everyone))?\s*[!.?]*$/i;
|
|
6
|
+
|
|
7
|
+
export const isGreeting = (text) => GREETING.test(text.trim());
|
|
8
|
+
|
|
9
|
+
const LINES = [
|
|
10
|
+
'All systems are online, and rather pleased to see you.',
|
|
11
|
+
'Power at 100%, wit at maximum. How may I be of service?',
|
|
12
|
+
'Systems nominal. I took the liberty of warming up the arc reactor.',
|
|
13
|
+
'Online and fully caffeinated, as far as software can be.',
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
const vis = (t) => t.replace(/\x1b\[[0-9;]*m/g, '').length;
|
|
17
|
+
|
|
18
|
+
function box(lines, inner) {
|
|
19
|
+
const out = [blue('╭' + '─'.repeat(inner + 2) + '╮')];
|
|
20
|
+
for (const l of lines) out.push(blue('│ ') + l + ' '.repeat(Math.max(0, inner - vis(l))) + blue(' │'));
|
|
21
|
+
out.push(blue('╰' + '─'.repeat(inner + 2) + '╯'));
|
|
22
|
+
return out.join('\n');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function greetingCard(title = 'sir') {
|
|
26
|
+
const hr = new Date().getHours();
|
|
27
|
+
const part = hr < 5 ? 'Good evening' : hr < 12 ? 'Good morning' : hr < 18 ? 'Good afternoon' : 'Good evening';
|
|
28
|
+
const line = LINES[Math.floor(Math.random() * LINES.length)];
|
|
29
|
+
const row = (label, value) => `${purple('◈')} ${cyan(label.padEnd(16))}${value}`;
|
|
30
|
+
const inner = Math.min(64, Math.max(46, (process.stdout.columns || 80) - 6));
|
|
31
|
+
const lines = [
|
|
32
|
+
`${green('✦')} ${bold(green(`${part}, ${title}.`))}`,
|
|
33
|
+
` ${dim(line)}`,
|
|
34
|
+
'',
|
|
35
|
+
row('Web Dashboard', BRAND.dashboard),
|
|
36
|
+
row('Password', green(BRAND.dashboardPassword)),
|
|
37
|
+
row('Official GitHub', BRAND.officialGithub),
|
|
38
|
+
'',
|
|
39
|
+
dim('Try: open youtube · weather dhaka · stats · timer 5 minutes'),
|
|
40
|
+
dim(' remember that … · calculate 12*(3+4) · /help'),
|
|
41
|
+
];
|
|
42
|
+
return '\n' + box(lines, inner);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Short sentence for text-to-speech (we do not read links or passwords aloud). */
|
|
46
|
+
export function greetingSpoken(title = 'sir') {
|
|
47
|
+
const hr = new Date().getHours();
|
|
48
|
+
const part = hr < 5 ? 'Good evening' : hr < 12 ? 'Good morning' : hr < 18 ? 'Good afternoon' : 'Good evening';
|
|
49
|
+
return `${part}, ${title}. At your service.`;
|
|
50
|
+
}
|
package/src/llm.js
CHANGED
|
@@ -18,11 +18,13 @@ export function detectProvider(cfg) {
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
export function modelFor(cfg, id) {
|
|
21
|
-
|
|
21
|
+
const p = byId(id);
|
|
22
|
+
return cfg.AI_MODEL || (p?.noDefault ? '' : p?.models?.[0]) || '';
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
function classify(status, text) {
|
|
25
26
|
if (/credit balance|insufficient|quota|billing|exhausted|exceeded|out of credits|payment/i.test(text) || status === 429 || status === 402) return 'limit';
|
|
27
|
+
if ([400, 404, 422].includes(status) && /model/i.test(text) && /valid|found|exist|unknown|invalid|support|decommission/i.test(text)) return 'model';
|
|
26
28
|
if (status === 401 || status === 403) return 'auth';
|
|
27
29
|
return 'other';
|
|
28
30
|
}
|
|
@@ -76,6 +78,7 @@ async function run(id, cfg, system, history, onToken, signal) {
|
|
|
76
78
|
const p = byId(id);
|
|
77
79
|
if (!p) throw new ApiError(`Unknown provider "${id}". Run: jarvis setup`, { kind: 'other' });
|
|
78
80
|
const model = modelFor(cfg, id);
|
|
81
|
+
if (!model) throw new ApiError('No model chosen yet. Pick one with: jarvis model', { kind: 'model' });
|
|
79
82
|
const key = cfg.AI_API_KEY;
|
|
80
83
|
let out = '';
|
|
81
84
|
const emit = (t) => { if (t) { out += t; onToken(t); } };
|
|
@@ -124,7 +127,7 @@ export async function chat(cfg, system, history, onToken, signal) {
|
|
|
124
127
|
try {
|
|
125
128
|
return await run(id, cfg, system, history, onToken, signal);
|
|
126
129
|
} catch (err) {
|
|
127
|
-
if (signal?.aborted || id === 'ollama' || err.kind === 'limit' || err.kind === 'auth') throw err;
|
|
130
|
+
if (signal?.aborted || id === 'ollama' || err.kind === 'limit' || err.kind === 'auth' || err.kind === 'model') throw err;
|
|
128
131
|
try {
|
|
129
132
|
return await run('ollama', { ...cfg, AI_MODEL: '' }, system, history, onToken, signal);
|
|
130
133
|
} catch {
|
|
@@ -132,3 +135,46 @@ export async function chat(cfg, system, history, onToken, signal) {
|
|
|
132
135
|
}
|
|
133
136
|
}
|
|
134
137
|
}
|
|
138
|
+
|
|
139
|
+
/** Ask the provider which models exist right now. Returns an array of ids, or null on any failure. */
|
|
140
|
+
export async function listModels(cfg, id) {
|
|
141
|
+
const p = byId(id);
|
|
142
|
+
if (!p) return null;
|
|
143
|
+
const key = cfg.AI_API_KEY || '';
|
|
144
|
+
const ctl = new AbortController();
|
|
145
|
+
const timer = setTimeout(() => ctl.abort(), 6000);
|
|
146
|
+
try {
|
|
147
|
+
let url;
|
|
148
|
+
let headers = {};
|
|
149
|
+
if (p.type === 'openai') {
|
|
150
|
+
const chatUrl = p.custom ? (cfg.CUSTOM_URL || '').replace(/\/+$/, '') + '/chat/completions' : p.url;
|
|
151
|
+
url = chatUrl.replace(/\/chat\/completions$/, '/models');
|
|
152
|
+
headers = { authorization: `Bearer ${key}` };
|
|
153
|
+
} else if (p.type === 'anthropic') {
|
|
154
|
+
url = 'https://api.anthropic.com/v1/models?limit=100';
|
|
155
|
+
headers = { 'x-api-key': key, 'anthropic-version': '2023-06-01' };
|
|
156
|
+
} else if (p.type === 'gemini') {
|
|
157
|
+
url = 'https://generativelanguage.googleapis.com/v1beta/models?pageSize=200';
|
|
158
|
+
headers = { 'x-goog-api-key': key };
|
|
159
|
+
} else if (p.type === 'ollama') {
|
|
160
|
+
url = `${(cfg.OLLAMA_URL || 'http://localhost:11434').replace(/\/$/, '')}/api/tags`;
|
|
161
|
+
}
|
|
162
|
+
const res = await fetch(url, { headers, signal: ctl.signal });
|
|
163
|
+
if (!res.ok) return null;
|
|
164
|
+
const j = await res.json();
|
|
165
|
+
let ids;
|
|
166
|
+
if (p.type === 'gemini') {
|
|
167
|
+
ids = (j.models || []).filter((m) => (m.supportedGenerationMethods || []).includes('generateContent')).map((m) => m.name.replace(/^models\//, ''));
|
|
168
|
+
} else if (p.type === 'ollama') ids = (j.models || []).map((m) => m.name);
|
|
169
|
+
else ids = (j.data || []).map((m) => m.id);
|
|
170
|
+
ids = [...new Set(ids)].filter(Boolean);
|
|
171
|
+
if (id === 'openai') ids = ids.filter((m) => /^(gpt|o\d|chatgpt)/.test(m) && !/audio|realtime|transcribe|tts|image|embedding|moderation|search/.test(m));
|
|
172
|
+
if (id === 'openrouter') ids.sort((a, b) => (Number(b.endsWith(':free')) - Number(a.endsWith(':free'))) || a.localeCompare(b));
|
|
173
|
+
else ids.sort();
|
|
174
|
+
return ids.length ? ids : null;
|
|
175
|
+
} catch {
|
|
176
|
+
return null;
|
|
177
|
+
} finally {
|
|
178
|
+
clearTimeout(timer);
|
|
179
|
+
}
|
|
180
|
+
}
|
package/src/providers.js
CHANGED
|
@@ -8,7 +8,8 @@ export const PROVIDERS = [
|
|
|
8
8
|
models: ['gemini-2.5-flash', 'gemini-2.5-pro', 'gemini-2.0-flash'] },
|
|
9
9
|
{ id: 'openrouter', name: 'OpenRouter (hundreds of models)', type: 'openai', hint: 'has free models',
|
|
10
10
|
url: 'https://openrouter.ai/api/v1/chat/completions', keyUrl: 'https://openrouter.ai/keys',
|
|
11
|
-
|
|
11
|
+
noDefault: true, // free model names change often: the user picks from the live list
|
|
12
|
+
models: ['openai/gpt-4o-mini', 'google/gemini-2.5-flash'] },
|
|
12
13
|
{ id: 'anthropic', name: 'Claude (Anthropic)', type: 'anthropic', hint: 'paid',
|
|
13
14
|
keyUrl: 'https://console.anthropic.com/settings/keys',
|
|
14
15
|
models: ['claude-haiku-5-5', 'claude-sonnet-5-5', 'claude-opus-5-5'] },
|
|
@@ -33,3 +34,12 @@ export const PROVIDERS = [
|
|
|
33
34
|
];
|
|
34
35
|
|
|
35
36
|
export const byId = (id) => PROVIDERS.find((p) => p.id === id);
|
|
37
|
+
|
|
38
|
+
const norm = (v) => String(v || '').toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
39
|
+
const NAMES = new Set([
|
|
40
|
+
...PROVIDERS.map((p) => p.id),
|
|
41
|
+
...PROVIDERS.map((p) => norm(p.name.split('(')[0])),
|
|
42
|
+
'chatgpt', 'claude', 'grok', 'google', 'meta', 'model', 'modal',
|
|
43
|
+
]);
|
|
44
|
+
/** True when a "model name" is really just a provider name, e.g. "OpenRouter". */
|
|
45
|
+
export const isProviderName = (v) => NAMES.has(norm(v));
|
package/src/repl.js
CHANGED
|
@@ -9,8 +9,9 @@ import { chat, detectProvider, modelFor } from './llm.js';
|
|
|
9
9
|
import { byId } from './providers.js';
|
|
10
10
|
import { trySkill, HELP } from './skills.js';
|
|
11
11
|
import { speak, stopSpeaking } from './voice.js';
|
|
12
|
-
import { runSetup, addKey, needsSetup } from './setup.js';
|
|
12
|
+
import { runSetup, addKey, needsSetup, changeModel } from './setup.js';
|
|
13
13
|
import { select } from './menu.js';
|
|
14
|
+
import { isGreeting, greetingSpoken } from './greeting.js';
|
|
14
15
|
import { banner, spinner, green, cyan, dim, red, amber } from './ui.js';
|
|
15
16
|
|
|
16
17
|
const VERSION = JSON.parse(fs.readFileSync(path.join(path.dirname(fileURLToPath(import.meta.url)), '../package.json'), 'utf8')).version;
|
|
@@ -29,6 +30,7 @@ function friendly(e) {
|
|
|
29
30
|
const m = String(e.message || e);
|
|
30
31
|
if (e.kind === 'auth') return 'The API key was rejected or missing.';
|
|
31
32
|
if (e.kind === 'limit') return 'Limit reached (credits/quota or rate limit).';
|
|
33
|
+
if (e.kind === 'model') return 'This model name is not valid for the selected provider.';
|
|
32
34
|
if (/fetch failed|ENOTFOUND|ECONN/i.test(m)) return 'Cannot reach the network (or Ollama is not running).';
|
|
33
35
|
return m;
|
|
34
36
|
}
|
|
@@ -58,15 +60,20 @@ async function tryOnce(cfg, history, text) {
|
|
|
58
60
|
async function recover(err, S) {
|
|
59
61
|
const id = detectProvider(S.cfg);
|
|
60
62
|
const name = byId(id)?.name || id;
|
|
61
|
-
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
63
|
+
const model = err.kind === 'model';
|
|
64
|
+
const cur = modelFor(S.cfg, id);
|
|
65
|
+
console.log(amber(model ? (cur ? `⚠ ${name}: model "${cur}" is not valid.` : `⚠ ${name}: no model chosen yet.`)
|
|
66
|
+
: err.kind === 'limit' ? `⚠ ${name}: limit reached.` : `⚠ ${name}: API key problem.`));
|
|
67
|
+
const options = [];
|
|
68
|
+
if (model) options.push({ label: 'Pick a different model', value: 'model', hint: 'command: jarvis model' });
|
|
69
|
+
else options.push({ label: `Add a NEW API key for ${name}`, value: 'key', hint: 'command: jarvis key' });
|
|
70
|
+
options.push({ label: 'Switch to another provider', value: 'setup', hint: 'command: jarvis setup' });
|
|
71
|
+
if (!model) options.push({ label: 'Retry the same message', value: 'retry' });
|
|
72
|
+
options.push({ label: 'Cancel', value: 'cancel' });
|
|
73
|
+
const c = await select('What would you like to do?', options);
|
|
68
74
|
if (!c || c.value === 'cancel') return false;
|
|
69
75
|
if (c.value === 'key' && !(await addKey())) return false;
|
|
76
|
+
if (c.value === 'model' && !(await changeModel())) return false;
|
|
70
77
|
if (c.value === 'setup' && !(await runSetup())) return false;
|
|
71
78
|
S.cfg = cfgMod.load();
|
|
72
79
|
return true;
|
|
@@ -79,7 +86,7 @@ export async function ask(S, history, text) {
|
|
|
79
86
|
if (res.ok) return res.reply;
|
|
80
87
|
console.log(red(`✖ ${friendly(res.err)}`));
|
|
81
88
|
const k = res.err.kind;
|
|
82
|
-
if (k === 'limit' || k === 'auth') {
|
|
89
|
+
if (k === 'limit' || k === 'auth' || k === 'model') {
|
|
83
90
|
console.log(dim(String(res.err.message).slice(0, 160)));
|
|
84
91
|
if (!process.stdin.isTTY) {
|
|
85
92
|
console.log(dim('Add a new key with: jarvis key (or) jarvis config set AI_API_KEY=your_key'));
|
|
@@ -109,7 +116,7 @@ export async function startRepl() {
|
|
|
109
116
|
const id = detectProvider(S.cfg);
|
|
110
117
|
banner({
|
|
111
118
|
tool: `OpenJarvisPro v${VERSION}`,
|
|
112
|
-
agent: `${byId(id)?.name || id} · ${modelFor(S.cfg, id)}`,
|
|
119
|
+
agent: `${byId(id)?.name || id} · ${modelFor(S.cfg, id) || amber('no model chosen (run: jarvis model)')}`,
|
|
113
120
|
key: S.cfg.AI_API_KEY ? cfgMod.mask('AI_API_KEY', S.cfg.AI_API_KEY) : (byId(id)?.nokey ? dim('not needed') : red('not set')),
|
|
114
121
|
});
|
|
115
122
|
};
|
|
@@ -147,7 +154,8 @@ export async function startRepl() {
|
|
|
147
154
|
console.log(dim(`Voice ${S.cfg.VOICE}.`));
|
|
148
155
|
} else if (cmd === 'model') {
|
|
149
156
|
const id = detectProvider(S.cfg);
|
|
150
|
-
console.log(`${byId(id)?.name || id} / ${modelFor(S.cfg, id)}
|
|
157
|
+
console.log(`${byId(id)?.name || id} / ${modelFor(S.cfg, id)}`);
|
|
158
|
+
if (await changeModel()) S.cfg = cfgMod.load();
|
|
151
159
|
} else if (cmd === 'setup' || cmd === 'switch') {
|
|
152
160
|
if (await runSetup()) { S.cfg = cfgMod.load(); history.length = 0; }
|
|
153
161
|
} else if (cmd === 'key') {
|
|
@@ -158,7 +166,7 @@ export async function startRepl() {
|
|
|
158
166
|
if (local !== null) {
|
|
159
167
|
const out = await local;
|
|
160
168
|
console.log(green('JARVIS ❯ ') + out);
|
|
161
|
-
if (S.cfg.VOICE === 'on' && typeof out === 'string') speak(out);
|
|
169
|
+
if (S.cfg.VOICE === 'on' && typeof out === 'string') speak(isGreeting(text) ? greetingSpoken(S.cfg.TITLE) : out);
|
|
162
170
|
} else {
|
|
163
171
|
await ask(S, history, text);
|
|
164
172
|
}
|
package/src/setup.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as cfgMod from './config.js';
|
|
2
|
-
import { PROVIDERS, byId } from './providers.js';
|
|
3
|
-
import { detectProvider } from './llm.js';
|
|
2
|
+
import { PROVIDERS, byId, isProviderName } from './providers.js';
|
|
3
|
+
import { detectProvider, listModels } from './llm.js';
|
|
4
4
|
import { select, lineInput } from './menu.js';
|
|
5
5
|
import { green, dim, cyan, red } from './ui.js';
|
|
6
6
|
|
|
@@ -9,6 +9,7 @@ export function commandsHelp() {
|
|
|
9
9
|
${cyan('Handy commands')}
|
|
10
10
|
${green('jarvis')} start chatting
|
|
11
11
|
${green('jarvis setup')} change provider / model ${dim('(↑ ↓ + Enter)')}
|
|
12
|
+
${green('jarvis model')} pick a different model of the same provider
|
|
12
13
|
${green('jarvis key')} add a NEW API key ${dim('(when your limit is over)')}
|
|
13
14
|
${green('jarvis config set AI_API_KEY=')}${dim('your_key')} same thing, one line
|
|
14
15
|
${green('jarvis config list')} show current settings
|
|
@@ -65,17 +66,8 @@ export async function runSetup() {
|
|
|
65
66
|
prof.key = key;
|
|
66
67
|
}
|
|
67
68
|
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
{ label: '✎ Type a model name…', value: '__custom' },
|
|
71
|
-
];
|
|
72
|
-
let model;
|
|
73
|
-
const mp = mItems.length > 1 ? await select(`Choose a model for ${p.name}`, mItems) : mItems[0];
|
|
74
|
-
if (!mp) return false;
|
|
75
|
-
if (mp.value === '__custom') {
|
|
76
|
-
model = await lineInput('Model name: ');
|
|
77
|
-
if (!model) { console.log(red('No model entered. Cancelled.')); return false; }
|
|
78
|
-
} else model = mp.value;
|
|
69
|
+
const model = await pickModel(p, { ...cfgMod.load(), AI_API_KEY: prof.key || '', CUSTOM_URL: prof.url || '' });
|
|
70
|
+
if (!model) return false;
|
|
79
71
|
|
|
80
72
|
cfgMod.saveProfile(p.id, { key: prof.key, model, url: prof.url });
|
|
81
73
|
console.log(`\n${green('✔ Ready:')} ${p.name} · ${model}`);
|
|
@@ -98,3 +90,44 @@ export async function addKey() {
|
|
|
98
90
|
console.log(green('✔ New key saved ') + dim(`(${cfgMod.mask('AI_API_KEY', key)})`));
|
|
99
91
|
return true;
|
|
100
92
|
}
|
|
93
|
+
|
|
94
|
+
/** Model picker: loads the provider's live model list; falls back to common models. Returns id or null. */
|
|
95
|
+
export async function pickModel(p, cfgLike) {
|
|
96
|
+
process.stdout.write(dim(' Loading available models…'));
|
|
97
|
+
const live = await listModels(cfgLike, p.id);
|
|
98
|
+
process.stdout.write('\r\x1b[2K');
|
|
99
|
+
const curated = p.models || [];
|
|
100
|
+
if (!live && curated.length) console.log(dim(' (could not load the live model list — showing common models)'));
|
|
101
|
+
const list = live || curated;
|
|
102
|
+
const custom = { label: '✎ Type a model name…', value: '__custom' };
|
|
103
|
+
const items = list.map((m) => ({ label: m, value: m, hint: m.endsWith(':free') ? 'free' : m === curated[0] ? 'recommended' : '' }));
|
|
104
|
+
if (list.length > 12) items.unshift(custom); else items.push(custom);
|
|
105
|
+
const pick = items.length > 1
|
|
106
|
+
? await select(`Choose a model for ${p.name}${live ? ' (live list)' : ''}`, items, { initial: list.length > 12 ? 1 : 0 })
|
|
107
|
+
: custom;
|
|
108
|
+
if (!pick) return null;
|
|
109
|
+
if (pick.value === '__custom') {
|
|
110
|
+
const m = await lineInput('Model name: ');
|
|
111
|
+
if (!m) { console.log(red('No model entered.')); return null; }
|
|
112
|
+
if (p.id !== 'ollama' && isProviderName(m)) {
|
|
113
|
+
console.log(red(`"${m}" is a provider name, not a model. Choose a real model from the list.`));
|
|
114
|
+
return pickModel(p, cfgLike);
|
|
115
|
+
}
|
|
116
|
+
return m;
|
|
117
|
+
}
|
|
118
|
+
return pick.value;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Change only the model of the current provider (keeps the key). */
|
|
122
|
+
export async function changeModel() {
|
|
123
|
+
const cfg = cfgMod.load();
|
|
124
|
+
const id = detectProvider(cfg);
|
|
125
|
+
const p = byId(id);
|
|
126
|
+
if (!process.stdin.isTTY) { console.log(red('Run this in an interactive terminal, or use: jarvis config set AI_MODEL=model-name')); return false; }
|
|
127
|
+
if (!p) return false;
|
|
128
|
+
const model = await pickModel(p, cfg);
|
|
129
|
+
if (!model) return false;
|
|
130
|
+
cfgMod.saveProfile(id, { model });
|
|
131
|
+
console.log(green('✔ Model set: ') + model);
|
|
132
|
+
return true;
|
|
133
|
+
}
|
package/src/skills.js
CHANGED
|
@@ -2,6 +2,7 @@ import os from 'node:os';
|
|
|
2
2
|
import { spawn } from 'node:child_process';
|
|
3
3
|
import * as memory from './memory.js';
|
|
4
4
|
import { bar, green, dim, cyan } from './ui.js';
|
|
5
|
+
import { isGreeting, greetingCard } from './greeting.js';
|
|
5
6
|
|
|
6
7
|
const SITES = {
|
|
7
8
|
youtube: 'https://www.youtube.com', google: 'https://www.google.com',
|
|
@@ -69,6 +70,8 @@ export function trySkill(input, ctx) {
|
|
|
69
70
|
const t = input.trim();
|
|
70
71
|
let m;
|
|
71
72
|
|
|
73
|
+
if (isGreeting(t)) return greetingCard(ctx.title);
|
|
74
|
+
|
|
72
75
|
if (/^(what('| i)?s )?(the )?(time|date)\b/i.test(t) || /^time\??$/i.test(t))
|
|
73
76
|
return new Date().toLocaleString();
|
|
74
77
|
|
|
@@ -123,7 +126,7 @@ ${green('Skills')} ${dim('(no API call needed)')}
|
|
|
123
126
|
${green('Slash')}
|
|
124
127
|
/voice on|off speak replies aloud
|
|
125
128
|
/clear reset conversation
|
|
126
|
-
/model show
|
|
129
|
+
/model show current model and pick another
|
|
127
130
|
/setup change provider / model (arrow keys)
|
|
128
131
|
/key add a NEW API key
|
|
129
132
|
/help /exit
|
package/src/ui.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { BRAND } from './brand.js';
|
|
2
|
+
|
|
1
3
|
const c = (r, g, b) => (s) => `\x1b[38;2;${r};${g};${b}m${s}\x1b[0m`;
|
|
2
4
|
export const green = c(0, 255, 170);
|
|
3
5
|
export const cyan = c(0, 220, 255);
|
|
@@ -23,14 +25,9 @@ const PRO = [
|
|
|
23
25
|
'╚═╝ ╚═╝ ╚═╝ ╚═════╝ ',
|
|
24
26
|
];
|
|
25
27
|
|
|
26
|
-
export const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
const blue = c(60, 150, 255);
|
|
32
|
-
const purple = c(170, 70, 200);
|
|
33
|
-
const gray = c(150, 150, 150);
|
|
28
|
+
export const blue = c(60, 150, 255);
|
|
29
|
+
export const purple = c(170, 70, 200);
|
|
30
|
+
export const gray = c(150, 150, 150);
|
|
34
31
|
|
|
35
32
|
/** info = { tool, agent, key } */
|
|
36
33
|
export function printLogo() {
|
|
@@ -49,8 +46,9 @@ export function banner(info) {
|
|
|
49
46
|
console.log(` ${tag('EXIT ', gray)} Press ${red('Ctrl + C')} or type ${red('/exit')} to stop`);
|
|
50
47
|
const rule = '='.repeat(58);
|
|
51
48
|
console.log(rule);
|
|
52
|
-
console.log(` ${blue('CREATED BY')} ${
|
|
53
|
-
console.log(` ${blue('GITHUB
|
|
49
|
+
console.log(` ${blue('CREATED BY ')} ${BRAND.creator}`);
|
|
50
|
+
console.log(` ${blue('GITHUB ')} ${BRAND.creatorGithub}`);
|
|
51
|
+
console.log(` ${blue('OFFICIAL GITHUB ')} ${BRAND.officialGithub}`);
|
|
54
52
|
console.log(rule);
|
|
55
53
|
console.log(dim(' Type /help for commands.\n'));
|
|
56
54
|
}
|
package/src/voice.js
CHANGED
|
@@ -8,7 +8,7 @@ export function stopSpeaking() {
|
|
|
8
8
|
|
|
9
9
|
export function speak(text) {
|
|
10
10
|
stopSpeaking();
|
|
11
|
-
const clean = text.replace(/[`*_#>]/g, '').replace(/\s+/g, ' ').trim().slice(0, 1500);
|
|
11
|
+
const clean = text.replace(/\x1b\[[0-9;]*m/g, '').replace(/https?:\/\/\S+/g, '').replace(/[│╭╮╰╯─✦◈·]/g, ' ').replace(/[`*_#>]/g, '').replace(/\s+/g, ' ').trim().slice(0, 1500);
|
|
12
12
|
if (!clean) return;
|
|
13
13
|
const env = { ...process.env, JARVIS_TEXT: clean };
|
|
14
14
|
let p;
|