@darksol/terminal 0.9.1 → 0.10.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/README.md CHANGED
@@ -15,7 +15,7 @@ A unified CLI for market intel, trading, AI-powered analysis, on-chain oracle, c
15
15
  [![License: GPL-3.0](https://img.shields.io/badge/License-GPL--3.0-gold.svg)](https://www.gnu.org/licenses/gpl-3.0)
16
16
  [![Node](https://img.shields.io/badge/node-%3E%3D18.0.0-green.svg)](https://nodejs.org/)
17
17
 
18
- - Current release: **0.9.1**
18
+ - Current release: **0.9.2**
19
19
  - Changelog: `CHANGELOG.md`
20
20
 
21
21
  ## Install
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@darksol/terminal",
3
- "version": "0.9.1",
3
+ "version": "0.10.0",
4
4
  "description": "DARKSOL Terminal — unified CLI for all DARKSOL services. Market intel, trading, oracle, casino, and more.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli.js CHANGED
@@ -17,6 +17,7 @@ import { executeLifiSwap, executeLifiBridge, checkBridgeStatus, showSupportedCha
17
17
  import { topMovers, tokenDetail, compareTokens } from './services/market.js';
18
18
  import { oracleFlip, oracleDice, oracleNumber, oracleShuffle, oracleHealth } from './services/oracle.js';
19
19
  import { casinoBet, casinoTables, casinoStats, casinoReceipt, casinoHealth, casinoVerify } from './services/casino.js';
20
+ import { pokerNewGame, pokerAction, pokerStatus, pokerHistory } from './services/poker.js';
20
21
  import { cardsCatalog, cardsOrder, cardsStatus } from './services/cards.js';
21
22
  import { facilitatorHealth, facilitatorVerify, facilitatorSettle } from './services/facilitator.js';
22
23
  import { buildersLeaderboard, buildersLookup, buildersFeed } from './services/builders.js';
@@ -32,6 +33,7 @@ import { clearMemories, exportMemories, getRecentMemories, searchMemories } from
32
33
  import { getAgentStatus, planAgentGoal, runAgentTask } from './agent/index.js';
33
34
  import { createRequire } from 'module';
34
35
  import { resolve } from 'path';
36
+ import { getConfiguredModel, getProviderDefaultModel } from './llm/models.js';
35
37
  const require = createRequire(import.meta.url);
36
38
  const { version: PKG_VERSION } = require('../package.json');
37
39
 
@@ -40,7 +42,7 @@ export function cli(argv) {
40
42
 
41
43
  program
42
44
  .name('darksol')
43
- .description(theme.gold('DARKSOL Terminal') + theme.dim(' Ghost in the machine with teeth 🌑'))
45
+ .description(theme.gold('DARKSOL Terminal') + theme.dim(' - Ghost in the machine with teeth 🌑'))
44
46
  .version(PKG_VERSION)
45
47
  ;
46
48
 
@@ -49,7 +51,7 @@ export function cli(argv) {
49
51
  // ═══════════════════════════════════════
50
52
  const wallet = program
51
53
  .command('wallet')
52
- .description('Wallet management create, import, list, balance');
54
+ .description('Wallet management - create, import, list, balance');
53
55
 
54
56
  wallet
55
57
  .command('create [name]')
@@ -114,7 +116,7 @@ export function cli(argv) {
114
116
  // ═══════════════════════════════════════
115
117
  const trade = program
116
118
  .command('trade')
117
- .description('Trading swap, snipe, DCA');
119
+ .description('Trading - swap, snipe, DCA');
118
120
 
119
121
  trade
120
122
  .command('swap')
@@ -162,14 +164,14 @@ export function cli(argv) {
162
164
  // If LI.FI failed (not cancelled), fall back to direct
163
165
  if (result?.error !== 'cancelled') {
164
166
  const { warn: showWarn, info: showInfo } = await import('./ui/components.js');
165
- showWarn('LI.FI route failed falling back to direct Uniswap V3...');
167
+ showWarn('LI.FI route failed - falling back to direct Uniswap V3...');
166
168
  console.log('');
167
169
  } else {
168
170
  return; // User cancelled, don't fallback
169
171
  }
170
172
  } catch {
171
173
  const { warn: showWarn } = await import('./ui/components.js');
172
- showWarn('LI.FI unavailable falling back to direct Uniswap V3...');
174
+ showWarn('LI.FI unavailable - falling back to direct Uniswap V3...');
173
175
  console.log('');
174
176
  }
175
177
  }
@@ -180,7 +182,7 @@ export function cli(argv) {
180
182
 
181
183
  trade
182
184
  .command('snipe <token>')
183
- .description('Snipe a token fast buy with ETH')
185
+ .description('Snipe a token - fast buy with ETH')
184
186
  .requiredOption('-a, --amount <eth>', 'ETH amount to spend')
185
187
  .option('-s, --slippage <percent>', 'Max slippage %', '1')
186
188
  .option('-g, --gas <multiplier>', 'Gas priority multiplier', '1.5')
@@ -214,7 +216,7 @@ export function cli(argv) {
214
216
  optimism: ['ETH/USDC', 'ETH/OP'],
215
217
  polygon: ['POL/USDC', 'POL/WETH', 'USDC/USDT'],
216
218
  };
217
- showSection(`COMMON PAIRS ${chain.toUpperCase()}`);
219
+ showSection(`COMMON PAIRS - ${chain.toUpperCase()}`);
218
220
  const pairs = byChain[chain] || byChain.base;
219
221
  pairs.forEach((p) => console.log(` ${theme.gold(p)}`));
220
222
  console.log('');
@@ -227,7 +229,7 @@ export function cli(argv) {
227
229
  // ═══════════════════════════════════════
228
230
  const bridge = program
229
231
  .command('bridge')
230
- .description('Cross-chain bridge move tokens between chains via LI.FI');
232
+ .description('Cross-chain bridge - move tokens between chains via LI.FI');
231
233
 
232
234
  bridge
233
235
  .command('send')
@@ -319,7 +321,7 @@ export function cli(argv) {
319
321
  // ═══════════════════════════════════════
320
322
  const market = program
321
323
  .command('market')
322
- .description('Market intel prices, movers, analysis');
324
+ .description('Market intel - prices, movers, analysis');
323
325
 
324
326
  market
325
327
  .command('top')
@@ -330,7 +332,7 @@ export function cli(argv) {
330
332
 
331
333
  market
332
334
  .command('token <query>')
333
- .description('Token detail price, volume, liquidity')
335
+ .description('Token detail - price, volume, liquidity')
334
336
  .action((query) => tokenDetail(query));
335
337
 
336
338
  market
@@ -375,7 +377,7 @@ export function cli(argv) {
375
377
  // ═══════════════════════════════════════
376
378
  const casino = program
377
379
  .command('casino')
378
- .description('The Clawsino on-chain betting');
380
+ .description('The Clawsino - on-chain betting');
379
381
 
380
382
  casino
381
383
  .command('status')
@@ -415,12 +417,29 @@ export function cli(argv) {
415
417
  .description('Verify bet on-chain')
416
418
  .action((id) => casinoVerify(id));
417
419
 
420
+ const poker = program
421
+ .command('poker [subcommand]')
422
+ .description('GTO Poker Arena — heads-up holdem against the house');
423
+
424
+ poker
425
+ .option('--free', 'Free play mode (default)')
426
+ .option('--real', 'Real mode ($1 buy-in, $2 payout on win)')
427
+ .action(async (subcommand, opts) => {
428
+ if (subcommand === 'status') {
429
+ return showPokerCliStatus();
430
+ }
431
+ if (subcommand === 'history') {
432
+ return showPokerCliHistory();
433
+ }
434
+ return playPokerCli(opts);
435
+ });
436
+
418
437
  // ═══════════════════════════════════════
419
438
  // CARDS COMMANDS
420
439
  // ═══════════════════════════════════════
421
440
  const cards = program
422
441
  .command('cards')
423
- .description('Prepaid cards crypto to Visa/MC');
442
+ .description('Prepaid cards - crypto to Visa/MC');
424
443
 
425
444
  cards
426
445
  .command('catalog')
@@ -497,7 +516,7 @@ export function cli(argv) {
497
516
  // ═══════════════════════════════════════
498
517
  const mail = program
499
518
  .command('mail')
500
- .description('📧 AgentMail email for your agent');
519
+ .description('📧 AgentMail - email for your agent');
501
520
 
502
521
  mail
503
522
  .command('setup')
@@ -751,7 +770,7 @@ export function cli(argv) {
751
770
  // ═══════════════════════════════════════
752
771
  program
753
772
  .command('setup')
754
- .description('First-run setup wizard configure AI provider, chain, wallet')
773
+ .description('First-run setup wizard - configure AI provider, chain, wallet')
755
774
  .option('-f, --force', 'Re-run even if already configured')
756
775
  .action((opts) => runSetupWizard({ force: opts.force }));
757
776
 
@@ -859,7 +878,7 @@ export function cli(argv) {
859
878
  // ═══════════════════════════════════════
860
879
  const keys = program
861
880
  .command('keys')
862
- .description('API key vault store keys for LLMs, data providers, RPCs');
881
+ .description('API key vault - store keys for LLMs, data providers, RPCs');
863
882
 
864
883
  keys
865
884
  .command('list')
@@ -882,7 +901,7 @@ export function cli(argv) {
882
901
  // ═══════════════════════════════════════
883
902
  const agent = program
884
903
  .command('agent')
885
- .description('Secure agent signer PK-isolated wallet for AI agents');
904
+ .description('Secure agent signer - PK-isolated wallet for AI agents');
886
905
 
887
906
  agent
888
907
  .command('task <goal...>')
@@ -990,7 +1009,7 @@ export function cli(argv) {
990
1009
  // ═══════════════════════════════════════
991
1010
  const skills = program
992
1011
  .command('skills')
993
- .description('DARKSOL skills directory install agent skills');
1012
+ .description('DARKSOL skills directory - install agent skills');
994
1013
 
995
1014
  skills
996
1015
  .command('list')
@@ -1055,7 +1074,7 @@ export function cli(argv) {
1055
1074
  .action(async (address, opts) => {
1056
1075
  showMiniBanner();
1057
1076
  if (address.length === 42 && address.startsWith('0x')) {
1058
- // Could be token or wallet try token first
1077
+ // Could be token or wallet - try token first
1059
1078
  try {
1060
1079
  await showTokenInfo(address, opts.chain);
1061
1080
  } catch {
@@ -1072,7 +1091,7 @@ export function cli(argv) {
1072
1091
  // ═══════════════════════════════════════
1073
1092
  const script = program
1074
1093
  .command('script')
1075
- .description('Execution scripts automated trading strategies');
1094
+ .description('Execution scripts - automated trading strategies');
1076
1095
 
1077
1096
  script
1078
1097
  .command('create')
@@ -1137,6 +1156,8 @@ export function cli(argv) {
1137
1156
  ['Output', cfg.output],
1138
1157
  ['Slippage', `${cfg.slippage}%`],
1139
1158
  ['Gas Multiplier', `${cfg.gasMultiplier}x`],
1159
+ ['LLM Provider', cfg.llm?.provider || theme.dim('(not set)')],
1160
+ ['LLM Model', getConfiguredModel(cfg.llm?.provider || 'openai') || theme.dim('(default)')],
1140
1161
  ['Soul User', cfg.soul?.userName || theme.dim('(not set)')],
1141
1162
  ['Agent Name', cfg.soul?.agentName || 'Darksol'],
1142
1163
  ['Tone', cfg.soul?.tone || theme.dim('(not set)')],
@@ -1153,6 +1174,33 @@ export function cli(argv) {
1153
1174
  console.log('');
1154
1175
  });
1155
1176
 
1177
+ config
1178
+ .command('model [model]')
1179
+ .description('Set the LLM model')
1180
+ .option('-p, --provider <provider>', 'LLM provider (defaults to current provider)')
1181
+ .action((model, opts) => {
1182
+ const provider = opts.provider || getConfig('llm.provider') || 'openai';
1183
+ if (!model) {
1184
+ const current = getConfiguredModel(provider);
1185
+ const fallback = getProviderDefaultModel(provider);
1186
+ info(`Current model for ${provider}: ${current || '(not set)'}`);
1187
+ if (fallback) {
1188
+ info(`Provider default: ${fallback}`);
1189
+ }
1190
+ return;
1191
+ }
1192
+
1193
+ if (opts.provider) {
1194
+ setConfig('llm.provider', provider);
1195
+ setConfig('llmProvider', provider);
1196
+ }
1197
+ setConfig('llm.model', model);
1198
+ if (provider === 'ollama') {
1199
+ setConfig('ollamaModel', model);
1200
+ }
1201
+ success(`LLM model for ${provider}: ${model}`);
1202
+ });
1203
+
1156
1204
  config
1157
1205
  .command('set <key> <value>')
1158
1206
  .description('Set config value')
@@ -1170,7 +1218,7 @@ export function cli(argv) {
1170
1218
  });
1171
1219
 
1172
1220
  // ═══════════════════════════════════════
1173
- // DASHBOARD (default) commands + optional AI
1221
+ // DASHBOARD (default) - commands + optional AI
1174
1222
  // ═══════════════════════════════════════
1175
1223
  program
1176
1224
  .command('dashboard', { isDefault: true })
@@ -1198,7 +1246,7 @@ export function cli(argv) {
1198
1246
 
1199
1247
  // ── AI nudge or chat prompt ──
1200
1248
  if (hasLLM) {
1201
- console.log(theme.gold(' 💬 AI is ready run ') + theme.label('darksol ai chat') + theme.gold(' or just ') + theme.label('darksol chat'));
1249
+ console.log(theme.gold(' 💬 AI is ready - run ') + theme.label('darksol ai chat') + theme.gold(' or just ') + theme.label('darksol chat'));
1202
1250
  console.log(theme.dim(' "swap 0.1 ETH for USDC" • "what\'s AERO at?" • any question'));
1203
1251
  console.log('');
1204
1252
  } else {
@@ -1222,7 +1270,7 @@ export function cli(argv) {
1222
1270
  const { info, error: showError } = await import('./ui/components.js');
1223
1271
 
1224
1272
  console.log('');
1225
- info(`"${input}" isn't a command asking AI...`);
1273
+ info(`"${input}" isn't a command - asking AI...`);
1226
1274
  console.log('');
1227
1275
 
1228
1276
  try {
@@ -1376,6 +1424,154 @@ async function chatResponse(llm, input) {
1376
1424
  }
1377
1425
  }
1378
1426
 
1427
+ function pokerModeFromOpts(opts = {}) {
1428
+ return opts.real ? 'real' : 'free';
1429
+ }
1430
+
1431
+ function renderPokerCards(cards, hidden = false) {
1432
+ const suitMap = { s: '♠', h: '♥', d: '♦', c: '♣' };
1433
+ const colorize = (card, text) => {
1434
+ const suit = card[1];
1435
+ return suit === 'h' || suit === 'd' ? theme.error(text) : theme.bright(text);
1436
+ };
1437
+
1438
+ const source = hidden ? ['??', '??'] : cards;
1439
+ const rows = ['', '', '', '', ''];
1440
+
1441
+ for (const card of source) {
1442
+ if (card === '??') {
1443
+ rows[0] += `${theme.dim('┌─────┐')} `;
1444
+ rows[1] += `${theme.dim('│░░░░░│')} `;
1445
+ rows[2] += `${theme.dim('│░░▓░░│')} `;
1446
+ rows[3] += `${theme.dim('│░░░░░│')} `;
1447
+ rows[4] += `${theme.dim('└─────┘')} `;
1448
+ continue;
1449
+ }
1450
+
1451
+ const rank = card[0] === 'T' ? '10' : card[0];
1452
+ const suit = suitMap[card[1]];
1453
+ rows[0] += `${theme.dim('┌─────┐')} `;
1454
+ rows[1] += `${theme.dim('│')}${colorize(card, rank.padEnd(2, ' '))}${theme.dim(' │')} `;
1455
+ rows[2] += `${theme.dim('│ ')}${colorize(card, suit)}${theme.dim(' │')} `;
1456
+ rows[3] += `${theme.dim('│ ')}${colorize(card, rank.padStart(2, ' '))}${theme.dim('│')} `;
1457
+ rows[4] += `${theme.dim('└─────┘')} `;
1458
+ }
1459
+
1460
+ rows.forEach((row) => console.log(` ${row}`));
1461
+ }
1462
+
1463
+ function showPokerState(status) {
1464
+ showSection(`POKER ARENA - ${status.mode === 'real' ? 'REAL MODE' : 'FREE MODE'}`);
1465
+ kvDisplay([
1466
+ ['Street', status.street.toUpperCase()],
1467
+ ['Dealer', status.dealer],
1468
+ ['Pot', `${status.pot} chips`],
1469
+ ['Current Bet', `${status.currentBet} chips`],
1470
+ ['Your Stack', `${status.player.stack} chips`],
1471
+ ['House Stack', `${status.house.stack} chips`],
1472
+ ['To Act', status.currentActor || '-'],
1473
+ ]);
1474
+
1475
+ console.log('');
1476
+ console.log(` ${theme.label('House')}`);
1477
+ renderPokerCards(status.house.hole, status.house.holeHidden);
1478
+ console.log('');
1479
+ console.log(` ${theme.label('Board')}`);
1480
+ if (status.community.length) renderPokerCards(status.community);
1481
+ else console.log(` ${theme.dim(' No community cards yet')}`);
1482
+ console.log('');
1483
+ console.log(` ${theme.label('You')}`);
1484
+ renderPokerCards(status.player.hole);
1485
+ console.log('');
1486
+
1487
+ if (status.street === 'finished') {
1488
+ const result = status.winner === 'player'
1489
+ ? theme.success('WIN')
1490
+ : status.winner === 'house'
1491
+ ? theme.error('LOSS')
1492
+ : theme.warning('PUSH');
1493
+ kvDisplay([
1494
+ ['Result', result],
1495
+ ['Summary', status.summary || '-'],
1496
+ ['Your Hand', status.player.hand?.name || '-'],
1497
+ ['House Hand', status.house.hand?.name || '-'],
1498
+ ['Payout', status.mode === 'real' && status.winner === 'player' ? `$${status.payoutUsdc} USDC` : status.mode === 'real' ? '$0 USDC' : 'free mode'],
1499
+ ]);
1500
+ console.log('');
1501
+ } else if (status.availableActions?.length) {
1502
+ info(`Actions: ${status.availableActions.join(', ')}`);
1503
+ }
1504
+ }
1505
+
1506
+ async function playPokerCli(opts = {}) {
1507
+ const inquirer = (await import('inquirer')).default;
1508
+ const mode = pokerModeFromOpts(opts);
1509
+ const spin = spinner(`Opening ${mode === 'real' ? 'real-mode' : 'free-mode'} poker table...`).start();
1510
+
1511
+ try {
1512
+ let status = await pokerNewGame({ mode });
1513
+ spin.succeed('Table ready');
1514
+
1515
+ while (status && status.street !== 'finished') {
1516
+ console.log('');
1517
+ showPokerState(status);
1518
+
1519
+ if (status.currentActor !== 'player') {
1520
+ status = pokerStatus(status.id);
1521
+ continue;
1522
+ }
1523
+
1524
+ const { action } = await inquirer.prompt([{
1525
+ type: 'list',
1526
+ name: 'action',
1527
+ message: theme.gold('Pick your action:'),
1528
+ choices: status.availableActions.map((item) => ({
1529
+ name: item === 'all-in' ? 'all-in' : item,
1530
+ value: item,
1531
+ })),
1532
+ }]);
1533
+
1534
+ status = await pokerAction(status.id, action);
1535
+ }
1536
+
1537
+ console.log('');
1538
+ showPokerState(status);
1539
+ } catch (err) {
1540
+ spin.fail('Poker table unavailable');
1541
+ error(err.message);
1542
+ }
1543
+ }
1544
+
1545
+ function showPokerCliStatus() {
1546
+ showMiniBanner();
1547
+ const status = pokerStatus();
1548
+ if (!status) {
1549
+ info('No active poker game');
1550
+ return;
1551
+ }
1552
+ showPokerState(status);
1553
+ }
1554
+
1555
+ function showPokerCliHistory() {
1556
+ showMiniBanner();
1557
+ const items = pokerHistory();
1558
+ if (!items.length) {
1559
+ info('No poker hands played yet');
1560
+ return;
1561
+ }
1562
+
1563
+ showSection('POKER HISTORY');
1564
+ items.slice(0, 10).forEach((item) => {
1565
+ const verdict = item.winner === 'player'
1566
+ ? theme.success('W')
1567
+ : item.winner === 'house'
1568
+ ? theme.error('L')
1569
+ : theme.warning('P');
1570
+ console.log(` ${verdict} ${theme.gold(item.mode.toUpperCase().padEnd(5))} ${theme.bright(item.summary)}`);
1571
+ });
1572
+ console.log('');
1573
+ }
1574
+
1379
1575
  function showCommandList() {
1380
1576
  console.log('');
1381
1577
  showSection('COMMANDS');
@@ -1399,10 +1595,11 @@ function showCommandList() {
1399
1595
  ['script', 'Execution scripts & strategies'],
1400
1596
  ['market', 'Market intel & token data'],
1401
1597
  ['oracle', 'On-chain random oracle'],
1402
- ['casino', 'The Clawsino betting'],
1598
+ ['casino', 'The Clawsino - betting'],
1599
+ ['poker', 'GTO Poker Arena — heads-up holdem'],
1403
1600
  ['cards', 'Prepaid Visa/MC cards'],
1404
1601
  ['builders', 'ERC-8021 builder index'],
1405
- ['mail', 'AgentMail email for your agent'],
1602
+ ['mail', 'AgentMail - email for your agent'],
1406
1603
  ['facilitator', 'x402 payment facilitator'],
1407
1604
  ['skills', 'Agent skill directory'],
1408
1605
  ['serve', 'Launch web terminal in browser'],
package/src/llm/engine.js CHANGED
@@ -3,18 +3,19 @@ import { getKeyFromEnv, getKey } from '../config/keys.js';
3
3
  import { getConfig } from '../config/store.js';
4
4
  import { SessionMemory, extractMemories, searchMemories } from '../memory/index.js';
5
5
  import { formatSystemPrompt as formatSoulSystemPrompt } from '../soul/index.js';
6
+ import { getProviderDefaultModel } from './models.js';
6
7
 
7
8
  const PROVIDERS = {
8
9
  openai: {
9
10
  url: 'https://api.openai.com/v1/chat/completions',
10
- defaultModel: 'gpt-4o',
11
+ defaultModel: getProviderDefaultModel('openai'),
11
12
  authHeader: (key) => ({ Authorization: `Bearer ${key}` }),
12
13
  parseResponse: (data) => data.choices?.[0]?.message?.content,
13
14
  parseUsage: (data) => data.usage,
14
15
  },
15
16
  anthropic: {
16
17
  url: 'https://api.anthropic.com/v1/messages',
17
- defaultModel: 'claude-sonnet-4-20250514',
18
+ defaultModel: getProviderDefaultModel('anthropic'),
18
19
  authHeader: (key) => ({ 'x-api-key': key, 'anthropic-version': '2023-06-01' }),
19
20
  buildBody: (model, messages, systemPrompt) => ({
20
21
  model,
@@ -30,7 +31,7 @@ const PROVIDERS = {
30
31
  },
31
32
  openrouter: {
32
33
  url: 'https://openrouter.ai/api/v1/chat/completions',
33
- defaultModel: 'anthropic/claude-sonnet-4-20250514',
34
+ defaultModel: getProviderDefaultModel('openrouter'),
34
35
  authHeader: (key) => ({
35
36
  Authorization: `Bearer ${key}`,
36
37
  'HTTP-Referer': 'https://darksol.net',
@@ -41,21 +42,21 @@ const PROVIDERS = {
41
42
  },
42
43
  minimax: {
43
44
  url: 'https://api.minimax.io/v1/chat/completions',
44
- defaultModel: 'MiniMax-M2.5',
45
+ defaultModel: getProviderDefaultModel('minimax'),
45
46
  authHeader: (key) => ({ Authorization: `Bearer ${key}` }),
46
47
  parseResponse: (data) => data.choices?.[0]?.message?.content,
47
48
  parseUsage: (data) => data.usage,
48
49
  },
49
50
  ollama: {
50
51
  url: null,
51
- defaultModel: 'llama3.1',
52
+ defaultModel: getProviderDefaultModel('ollama'),
52
53
  authHeader: () => ({}),
53
54
  parseResponse: (data) => data.choices?.[0]?.message?.content || data.message?.content,
54
55
  parseUsage: () => ({ input: 0, output: 0 }),
55
56
  },
56
57
  bankr: {
57
58
  url: 'https://llm.bankr.bot/v1/chat/completions',
58
- defaultModel: 'claude-sonnet-4.6',
59
+ defaultModel: getProviderDefaultModel('bankr'),
59
60
  authHeader: (key) => ({ 'X-API-Key': key }),
60
61
  parseResponse: (data) => data.choices?.[0]?.message?.content,
61
62
  parseUsage: (data) => data.usage,
@@ -99,9 +100,7 @@ export class LLMEngine {
99
100
  throw new Error(`Unknown LLM provider: ${this.provider}. Supported: ${Object.keys(PROVIDERS).join(', ')}`);
100
101
  }
101
102
 
102
- if (!this.model) {
103
- this.model = providerConfig.defaultModel;
104
- }
103
+ this.model = this.model || providerConfig.defaultModel || getProviderDefaultModel(this.provider);
105
104
 
106
105
  if (this.provider === 'ollama') {
107
106
  const host = this.apiKey || getConfig('llm.ollamaHost') || 'http://localhost:11434';
@@ -0,0 +1,67 @@
1
+ import { getConfig } from '../config/store.js';
2
+
3
+ export const MODEL_CATALOG = {
4
+ openai: {
5
+ defaultModel: 'gpt-5.4',
6
+ choices: [
7
+ { value: 'gpt-5.4', label: 'gpt-5.4', desc: 'flagship, complex reasoning' },
8
+ { value: 'gpt-5-mini', label: 'gpt-5-mini', desc: 'fast, lower cost' },
9
+ { value: 'gpt-4o', label: 'gpt-4o', desc: 'previous gen, still good' },
10
+ { value: 'o3', label: 'o3', desc: 'reasoning model' },
11
+ ],
12
+ },
13
+ anthropic: {
14
+ defaultModel: 'claude-sonnet-4-6',
15
+ choices: [
16
+ { value: 'claude-opus-4-6', label: 'claude-opus-4-6', desc: 'most intelligent, agents+coding' },
17
+ { value: 'claude-sonnet-4-6', label: 'claude-sonnet-4-6', desc: 'best speed/intelligence balance' },
18
+ { value: 'claude-haiku-4-5', label: 'claude-haiku-4-5', desc: 'fastest, near-frontier' },
19
+ ],
20
+ },
21
+ openrouter: {
22
+ defaultModel: 'anthropic/claude-sonnet-4-6',
23
+ choices: [
24
+ { value: 'anthropic/claude-sonnet-4-6', label: 'anthropic/claude-sonnet-4-6', desc: 'popular pick' },
25
+ { value: 'openai/gpt-5.4', label: 'openai/gpt-5.4', desc: 'popular pick' },
26
+ { value: 'google/gemini-2.5-pro', label: 'google/gemini-2.5-pro', desc: 'popular pick' },
27
+ { value: 'meta-llama/llama-4-maverick', label: 'meta-llama/llama-4-maverick', desc: 'popular pick' },
28
+ { value: 'deepseek/deepseek-r1', label: 'deepseek/deepseek-r1', desc: 'popular pick' },
29
+ ],
30
+ allowCustom: true,
31
+ },
32
+ minimax: {
33
+ defaultModel: 'MiniMax-M2.5',
34
+ choices: [
35
+ { value: 'MiniMax-M2.5', label: 'MiniMax-M2.5', desc: 'flagship, 204K context, ~60 tps' },
36
+ { value: 'MiniMax-M2.5-highspeed', label: 'MiniMax-M2.5-highspeed', desc: 'same perf, ~100 tps' },
37
+ { value: 'MiniMax-M2.1', label: 'MiniMax-M2.1', desc: 'code-focused' },
38
+ { value: 'MiniMax-M2.1-highspeed', label: 'MiniMax-M2.1-highspeed', desc: 'code-focused, faster' },
39
+ { value: 'MiniMax-M2', label: 'MiniMax-M2', desc: 'agentic, advanced reasoning' },
40
+ ],
41
+ },
42
+ ollama: {
43
+ defaultModel: 'llama3.1',
44
+ textInput: true,
45
+ },
46
+ bankr: {
47
+ defaultModel: 'claude-sonnet-4.6',
48
+ managed: true,
49
+ },
50
+ };
51
+
52
+ export function getProviderDefaultModel(provider) {
53
+ return MODEL_CATALOG[provider]?.defaultModel || null;
54
+ }
55
+
56
+ export function getConfiguredProvider(fallback = 'openai') {
57
+ return getConfig('llm.provider') || fallback;
58
+ }
59
+
60
+ export function getConfiguredModel(provider = getConfiguredProvider()) {
61
+ const configured = getConfig('llm.model');
62
+ return configured || getProviderDefaultModel(provider);
63
+ }
64
+
65
+ export function getModelSelectionMeta(provider = getConfiguredProvider()) {
66
+ return MODEL_CATALOG[provider] || { defaultModel: null };
67
+ }