open-jarvis-cli 1.1.0 → 1.1.1

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
@@ -6,7 +6,7 @@ import * as cfgMod from '../src/config.js';
6
6
  import * as memory from '../src/memory.js';
7
7
  import { startRepl, ask } from '../src/repl.js';
8
8
  import { stats, trySkill } from '../src/skills.js';
9
- import { runSetup, addKey, needsSetup, commandsHelp } from '../src/setup.js';
9
+ import { runSetup, addKey, changeModel, needsSetup, commandsHelp } from '../src/setup.js';
10
10
  import { byId } from '../src/providers.js';
11
11
  import { green, dim, red, cyan } from '../src/ui.js';
12
12
 
@@ -56,6 +56,7 @@ try {
56
56
  else if (cmd === 'config') handleConfig(rest);
57
57
  else if (cmd === 'setup' || cmd === 'switch') await runSetup();
58
58
  else if (cmd === 'key') await addKey();
59
+ else if (cmd === 'model') await changeModel();
59
60
  else if (cmd === 'ask') {
60
61
  const q = rest.join(' ').trim();
61
62
  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.1.1",
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/llm.js CHANGED
@@ -23,6 +23,7 @@ export function modelFor(cfg, id) {
23
23
 
24
24
  function classify(status, text) {
25
25
  if (/credit balance|insufficient|quota|billing|exhausted|exceeded|out of credits|payment/i.test(text) || status === 429 || status === 402) return 'limit';
26
+ if ([400, 404, 422].includes(status) && /model/i.test(text) && /valid|found|exist|unknown|invalid|support|decommission/i.test(text)) return 'model';
26
27
  if (status === 401 || status === 403) return 'auth';
27
28
  return 'other';
28
29
  }
@@ -124,7 +125,7 @@ export async function chat(cfg, system, history, onToken, signal) {
124
125
  try {
125
126
  return await run(id, cfg, system, history, onToken, signal);
126
127
  } catch (err) {
127
- if (signal?.aborted || id === 'ollama' || err.kind === 'limit' || err.kind === 'auth') throw err;
128
+ if (signal?.aborted || id === 'ollama' || err.kind === 'limit' || err.kind === 'auth' || err.kind === 'model') throw err;
128
129
  try {
129
130
  return await run('ollama', { ...cfg, AI_MODEL: '' }, system, history, onToken, signal);
130
131
  } catch {
@@ -132,3 +133,46 @@ export async function chat(cfg, system, history, onToken, signal) {
132
133
  }
133
134
  }
134
135
  }
136
+
137
+ /** Ask the provider which models exist right now. Returns an array of ids, or null on any failure. */
138
+ export async function listModels(cfg, id) {
139
+ const p = byId(id);
140
+ if (!p) return null;
141
+ const key = cfg.AI_API_KEY || '';
142
+ const ctl = new AbortController();
143
+ const timer = setTimeout(() => ctl.abort(), 6000);
144
+ try {
145
+ let url;
146
+ let headers = {};
147
+ if (p.type === 'openai') {
148
+ const chatUrl = p.custom ? (cfg.CUSTOM_URL || '').replace(/\/+$/, '') + '/chat/completions' : p.url;
149
+ url = chatUrl.replace(/\/chat\/completions$/, '/models');
150
+ headers = { authorization: `Bearer ${key}` };
151
+ } else if (p.type === 'anthropic') {
152
+ url = 'https://api.anthropic.com/v1/models?limit=100';
153
+ headers = { 'x-api-key': key, 'anthropic-version': '2023-06-01' };
154
+ } else if (p.type === 'gemini') {
155
+ url = 'https://generativelanguage.googleapis.com/v1beta/models?pageSize=200';
156
+ headers = { 'x-goog-api-key': key };
157
+ } else if (p.type === 'ollama') {
158
+ url = `${(cfg.OLLAMA_URL || 'http://localhost:11434').replace(/\/$/, '')}/api/tags`;
159
+ }
160
+ const res = await fetch(url, { headers, signal: ctl.signal });
161
+ if (!res.ok) return null;
162
+ const j = await res.json();
163
+ let ids;
164
+ if (p.type === 'gemini') {
165
+ ids = (j.models || []).filter((m) => (m.supportedGenerationMethods || []).includes('generateContent')).map((m) => m.name.replace(/^models\//, ''));
166
+ } else if (p.type === 'ollama') ids = (j.models || []).map((m) => m.name);
167
+ else ids = (j.data || []).map((m) => m.id);
168
+ ids = [...new Set(ids)].filter(Boolean);
169
+ if (id === 'openai') ids = ids.filter((m) => /^(gpt|o\d|chatgpt)/.test(m) && !/audio|realtime|transcribe|tts|image|embedding|moderation|search/.test(m));
170
+ if (id === 'openrouter') ids.sort((a, b) => (Number(b.endsWith(':free')) - Number(a.endsWith(':free'))) || a.localeCompare(b));
171
+ else ids.sort();
172
+ return ids.length ? ids : null;
173
+ } catch {
174
+ return null;
175
+ } finally {
176
+ clearTimeout(timer);
177
+ }
178
+ }
package/src/repl.js CHANGED
@@ -9,7 +9,7 @@ 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
14
  import { banner, spinner, green, cyan, dim, red, amber } from './ui.js';
15
15
 
@@ -29,6 +29,7 @@ function friendly(e) {
29
29
  const m = String(e.message || e);
30
30
  if (e.kind === 'auth') return 'The API key was rejected or missing.';
31
31
  if (e.kind === 'limit') return 'Limit reached (credits/quota or rate limit).';
32
+ if (e.kind === 'model') return 'This model name is not valid for the selected provider.';
32
33
  if (/fetch failed|ENOTFOUND|ECONN/i.test(m)) return 'Cannot reach the network (or Ollama is not running).';
33
34
  return m;
34
35
  }
@@ -58,15 +59,19 @@ async function tryOnce(cfg, history, text) {
58
59
  async function recover(err, S) {
59
60
  const id = detectProvider(S.cfg);
60
61
  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
- ]);
62
+ const model = err.kind === 'model';
63
+ console.log(amber(model ? `⚠ ${name}: model "${modelFor(S.cfg, id)}" is not valid.`
64
+ : err.kind === 'limit' ? `⚠ ${name}: limit reached.` : `⚠ ${name}: API key problem.`));
65
+ const options = [];
66
+ if (model) options.push({ label: 'Pick a different model', value: 'model', hint: 'command: jarvis model' });
67
+ else options.push({ label: `Add a NEW API key for ${name}`, value: 'key', hint: 'command: jarvis key' });
68
+ options.push({ label: 'Switch to another provider', value: 'setup', hint: 'command: jarvis setup' });
69
+ if (!model) options.push({ label: 'Retry the same message', value: 'retry' });
70
+ options.push({ label: 'Cancel', value: 'cancel' });
71
+ const c = await select('What would you like to do?', options);
68
72
  if (!c || c.value === 'cancel') return false;
69
73
  if (c.value === 'key' && !(await addKey())) return false;
74
+ if (c.value === 'model' && !(await changeModel())) return false;
70
75
  if (c.value === 'setup' && !(await runSetup())) return false;
71
76
  S.cfg = cfgMod.load();
72
77
  return true;
@@ -79,7 +84,7 @@ export async function ask(S, history, text) {
79
84
  if (res.ok) return res.reply;
80
85
  console.log(red(`✖ ${friendly(res.err)}`));
81
86
  const k = res.err.kind;
82
- if (k === 'limit' || k === 'auth') {
87
+ if (k === 'limit' || k === 'auth' || k === 'model') {
83
88
  console.log(dim(String(res.err.message).slice(0, 160)));
84
89
  if (!process.stdin.isTTY) {
85
90
  console.log(dim('Add a new key with: jarvis key (or) jarvis config set AI_API_KEY=your_key'));
@@ -147,7 +152,8 @@ export async function startRepl() {
147
152
  console.log(dim(`Voice ${S.cfg.VOICE}.`));
148
153
  } else if (cmd === 'model') {
149
154
  const id = detectProvider(S.cfg);
150
- console.log(`${byId(id)?.name || id} / ${modelFor(S.cfg, id)} ${dim('(change with /setup)')}`);
155
+ console.log(`${byId(id)?.name || id} / ${modelFor(S.cfg, id)}`);
156
+ if (await changeModel()) S.cfg = cfgMod.load();
151
157
  } else if (cmd === 'setup' || cmd === 'switch') {
152
158
  if (await runSetup()) { S.cfg = cfgMod.load(); history.length = 0; }
153
159
  } else if (cmd === 'key') {
package/src/setup.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import * as cfgMod from './config.js';
2
2
  import { PROVIDERS, byId } from './providers.js';
3
- import { detectProvider } from './llm.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,40 @@ 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
+ return m;
113
+ }
114
+ return pick.value;
115
+ }
116
+
117
+ /** Change only the model of the current provider (keeps the key). */
118
+ export async function changeModel() {
119
+ const cfg = cfgMod.load();
120
+ const id = detectProvider(cfg);
121
+ const p = byId(id);
122
+ if (!process.stdin.isTTY) { console.log(red('Run this in an interactive terminal, or use: jarvis config set AI_MODEL=model-name')); return false; }
123
+ if (!p) return false;
124
+ const model = await pickModel(p, cfg);
125
+ if (!model) return false;
126
+ cfgMod.saveProfile(id, { model });
127
+ console.log(green('✔ Model set: ') + model);
128
+ return true;
129
+ }
package/src/skills.js CHANGED
@@ -123,7 +123,7 @@ ${green('Skills')} ${dim('(no API call needed)')}
123
123
  ${green('Slash')}
124
124
  /voice on|off speak replies aloud
125
125
  /clear reset conversation
126
- /model show provider & model
126
+ /model show current model and pick another
127
127
  /setup change provider / model (arrow keys)
128
128
  /key add a NEW API key
129
129
  /help /exit