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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "open-jarvis-cli",
3
- "version": "1.1.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": {
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;
@@ -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
- return cfg.AI_MODEL || byId(id)?.models?.[0] || '(set a model)';
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
- models: ['meta-llama/llama-3.3-70b-instruct:free', 'openai/gpt-4o-mini', 'google/gemini-2.5-flash'] },
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
- console.log(amber(model ? `⚠ ${name}: model "${modelFor(S.cfg, id)}" is not valid.`
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 CREDIT = {
27
- name: 'Rongon Kairy',
28
- github: 'https://github.com/RongonKairy',
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')} ${CREDIT.name}`);
53
- console.log(` ${blue('GITHUB ')} ${CREDIT.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;