open-jarvis-cli 1.1.1 → 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 +8 -0
- package/package.json +1 -1
- package/src/brand.js +8 -0
- package/src/config.js +5 -1
- package/src/greeting.js +50 -0
- package/src/llm.js +3 -1
- package/src/providers.js +11 -1
- package/src/repl.js +5 -3
- package/src/setup.js +5 -1
- package/src/skills.js +3 -0
- package/src/ui.js +8 -10
- package/src/voice.js +1 -1
package/bin/jarvis.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
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';
|
|
@@ -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
|
|
|
@@ -57,6 +59,12 @@ try {
|
|
|
57
59
|
else if (cmd === 'setup' || cmd === 'switch') await runSetup();
|
|
58
60
|
else if (cmd === 'key') await addKey();
|
|
59
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
|
+
}
|
|
60
68
|
else if (cmd === 'ask') {
|
|
61
69
|
const q = rest.join(' ').trim();
|
|
62
70
|
if (!q) console.log(red('Usage: jarvis ask "question"'));
|
package/package.json
CHANGED
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,7 +18,8 @@ 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) {
|
|
@@ -77,6 +78,7 @@ async function run(id, cfg, system, history, onToken, signal) {
|
|
|
77
78
|
const p = byId(id);
|
|
78
79
|
if (!p) throw new ApiError(`Unknown provider "${id}". Run: jarvis setup`, { kind: 'other' });
|
|
79
80
|
const model = modelFor(cfg, id);
|
|
81
|
+
if (!model) throw new ApiError('No model chosen yet. Pick one with: jarvis model', { kind: 'model' });
|
|
80
82
|
const key = cfg.AI_API_KEY;
|
|
81
83
|
let out = '';
|
|
82
84
|
const emit = (t) => { if (t) { out += t; onToken(t); } };
|
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
|
@@ -11,6 +11,7 @@ import { trySkill, HELP } from './skills.js';
|
|
|
11
11
|
import { speak, stopSpeaking } from './voice.js';
|
|
12
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;
|
|
@@ -60,7 +61,8 @@ async function recover(err, S) {
|
|
|
60
61
|
const id = detectProvider(S.cfg);
|
|
61
62
|
const name = byId(id)?.name || id;
|
|
62
63
|
const model = err.kind === 'model';
|
|
63
|
-
|
|
64
|
+
const cur = modelFor(S.cfg, id);
|
|
65
|
+
console.log(amber(model ? (cur ? `⚠ ${name}: model "${cur}" is not valid.` : `⚠ ${name}: no model chosen yet.`)
|
|
64
66
|
: err.kind === 'limit' ? `⚠ ${name}: limit reached.` : `⚠ ${name}: API key problem.`));
|
|
65
67
|
const options = [];
|
|
66
68
|
if (model) options.push({ label: 'Pick a different model', value: 'model', hint: 'command: jarvis model' });
|
|
@@ -114,7 +116,7 @@ export async function startRepl() {
|
|
|
114
116
|
const id = detectProvider(S.cfg);
|
|
115
117
|
banner({
|
|
116
118
|
tool: `OpenJarvisPro v${VERSION}`,
|
|
117
|
-
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)')}`,
|
|
118
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')),
|
|
119
121
|
});
|
|
120
122
|
};
|
|
@@ -164,7 +166,7 @@ export async function startRepl() {
|
|
|
164
166
|
if (local !== null) {
|
|
165
167
|
const out = await local;
|
|
166
168
|
console.log(green('JARVIS ❯ ') + out);
|
|
167
|
-
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);
|
|
168
170
|
} else {
|
|
169
171
|
await ask(S, history, text);
|
|
170
172
|
}
|
package/src/setup.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as cfgMod from './config.js';
|
|
2
|
-
import { PROVIDERS, byId } from './providers.js';
|
|
2
|
+
import { PROVIDERS, byId, isProviderName } from './providers.js';
|
|
3
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';
|
|
@@ -109,6 +109,10 @@ export async function pickModel(p, cfgLike) {
|
|
|
109
109
|
if (pick.value === '__custom') {
|
|
110
110
|
const m = await lineInput('Model name: ');
|
|
111
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
|
+
}
|
|
112
116
|
return m;
|
|
113
117
|
}
|
|
114
118
|
return pick.value;
|
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
|
|
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;
|