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 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.1.0",
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
- "bin",
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;
@@ -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
- 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) {
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
- 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
@@ -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
- console.log(amber(err.kind === 'limit' ? `⚠ ${name}: limit reached.` : `⚠ ${name}: API key problem.`));
62
- const c = await select('What would you like to do?', [
63
- { label: `Add a NEW API key for ${name}`, value: 'key', hint: 'command: jarvis key' },
64
- { label: 'Switch to another provider / model', value: 'setup', hint: 'command: jarvis setup' },
65
- { label: 'Retry the same message', value: 'retry' },
66
- { label: 'Cancel', value: 'cancel' },
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)} ${dim('(change with /setup)')}`);
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 mItems = [
69
- ...(p.models || []).map((m, i) => ({ label: m, value: m, hint: i === 0 ? 'recommended' : '' })),
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 provider & model
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 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;