@semalt-ai/code 1.3.0 → 1.4.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.
Files changed (3) hide show
  1. package/README.md +22 -1
  2. package/index.js +146 -38
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -95,7 +95,10 @@ semalt-code [command] [options]
95
95
  Runs a shell command with approval prompts.
96
96
 
97
97
  - `semalt-code models`
98
- Lists models exposed by the configured API.
98
+ Lists all saved model profiles.
99
+
100
+ - `semalt-code models add`
101
+ Opens an interactive flow to add an API base URL, API key, and model ID as a reusable model profile.
99
102
 
100
103
  - `semalt-code init`
101
104
  Creates or updates the local config file.
@@ -134,7 +137,9 @@ Available interactive commands:
134
137
 
135
138
  - `/help`
136
139
  - `/file <path>`
140
+ - `/model`
137
141
  - `/model <name>`
142
+ - `/models`
138
143
  - `/clear`
139
144
  - `/compact`
140
145
  - `/cost`
@@ -205,6 +210,22 @@ semalt-code shell -a "npm test"
205
210
  semalt-code models
206
211
  ```
207
212
 
213
+ ### Add a saved model profile
214
+
215
+ ```bash
216
+ semalt-code models add
217
+ ```
218
+
219
+ The CLI will ask for:
220
+
221
+ - API Base URL
222
+ - API Key
223
+ - Model ID
224
+
225
+ Each saved profile is appended to the profile list.
226
+
227
+ Saved profiles can then be selected inside chat mode with `/model` or `/models`.
228
+
208
229
  ### Show the current version
209
230
 
210
231
  ```bash
package/index.js CHANGED
@@ -21,23 +21,43 @@ const DEFAULT_CONFIG = {
21
21
  temperature: 0.7,
22
22
  max_tokens: 4096,
23
23
  stream: true,
24
+ models: [],
24
25
  };
25
26
 
26
27
  const CONFIG_PATH = path.join(os.homedir(), '.semalt-ai', 'config.json');
27
28
 
29
+ function normalizeConfig(cfg = {}) {
30
+ const merged = { ...DEFAULT_CONFIG, ...cfg };
31
+ merged.models = Array.isArray(cfg.models)
32
+ ? cfg.models
33
+ .filter((entry) => entry &&
34
+ typeof entry.api_base === 'string' &&
35
+ typeof entry.api_key === 'string' &&
36
+ typeof entry.model === 'string' &&
37
+ entry.api_base.trim() &&
38
+ entry.model.trim())
39
+ .map((entry) => ({
40
+ api_base: entry.api_base.trim(),
41
+ api_key: entry.api_key,
42
+ model: entry.model.trim(),
43
+ }))
44
+ : [];
45
+ return merged;
46
+ }
47
+
28
48
  function loadConfig() {
29
49
  if (fs.existsSync(CONFIG_PATH)) {
30
50
  try {
31
51
  const data = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8'));
32
- return { ...DEFAULT_CONFIG, ...data };
52
+ return normalizeConfig(data);
33
53
  } catch {}
34
54
  }
35
- return { ...DEFAULT_CONFIG };
55
+ return normalizeConfig();
36
56
  }
37
57
 
38
58
  function saveConfig(cfg) {
39
59
  fs.mkdirSync(path.dirname(CONFIG_PATH), { recursive: true });
40
- fs.writeFileSync(CONFIG_PATH, JSON.stringify(cfg, null, 2));
60
+ fs.writeFileSync(CONFIG_PATH, JSON.stringify(normalizeConfig(cfg), null, 2));
41
61
  }
42
62
 
43
63
  let config = loadConfig();
@@ -424,6 +444,52 @@ function apiUrl(urlPath) {
424
444
  return `${normalizedBase}${normalizedPath}`;
425
445
  }
426
446
 
447
+ function describeModelProfile(profile) {
448
+ return `${profile.model} @ ${profile.api_base}`;
449
+ }
450
+
451
+ function setActiveModelProfile(profile) {
452
+ config.api_base = profile.api_base;
453
+ config.api_key = profile.api_key;
454
+ config.default_model = profile.model;
455
+ saveConfig(config);
456
+ }
457
+
458
+ function chooseSavedModelProfile(rl, currentModel, cwd, onDone) {
459
+ if (!config.models.length) {
460
+ console.log(` ${FG_RED}✗${RST} ${FG_GRAY}No saved model profiles. Use semalt-code models add first.${RST}`);
461
+ onDone(currentModel);
462
+ return;
463
+ }
464
+
465
+ console.log();
466
+ console.log(` ${FG_TEAL}${BOLD}◆ Saved Models${RST}`);
467
+ console.log(` ${FG_DARK}${'─'.repeat(40)}${RST}`);
468
+ config.models.forEach((profile, index) => {
469
+ const active = profile.api_base === config.api_base &&
470
+ profile.api_key === config.api_key &&
471
+ profile.model === currentModel;
472
+ const marker = active ? `${FG_GREEN}●${RST}` : `${FG_DARK}○${RST}`;
473
+ console.log(` ${marker} ${FG_CYAN}${index + 1}.${RST} ${describeModelProfile(profile)}`);
474
+ });
475
+ console.log();
476
+
477
+ rl.question(` ${FG_TEAL}${BOLD}Select model>${RST} `, (answer) => {
478
+ const selected = Number((answer || '').trim());
479
+ if (!Number.isInteger(selected) || selected < 1 || selected > config.models.length) {
480
+ console.log(` ${FG_RED}✗${RST} ${FG_GRAY}Invalid selection${RST}`);
481
+ onDone(currentModel);
482
+ return;
483
+ }
484
+
485
+ const profile = config.models[selected - 1];
486
+ setActiveModelProfile(profile);
487
+ console.log(` ${FG_GREEN}✓${RST} ${FG_GRAY}Model profile → ${describeModelProfile(profile)}${RST}`);
488
+ printStatusBar(profile.model, cwd);
489
+ onDone(profile.model);
490
+ });
491
+ }
492
+
427
493
  function estimateTokens(text) {
428
494
  return Math.floor((text || '').length / 4);
429
495
  }
@@ -789,6 +855,7 @@ async function cmdChat(opts) {
789
855
  printBanner();
790
856
  const cwd = process.cwd();
791
857
  let currentModel = opts.model || config.default_model;
858
+ let isRunningAgent = false;
792
859
 
793
860
  printStatusBar(currentModel, cwd);
794
861
  printHelpHints();
@@ -807,8 +874,15 @@ async function cmdChat(opts) {
807
874
  process.exit(0);
808
875
  });
809
876
 
877
+ rl.on('SIGINT', () => {
878
+ if (isRunningAgent) return;
879
+ console.log(`\n ${FG_YELLOW}Use Ctrl+D or type exit to quit.${RST}`);
880
+ rl.prompt(true);
881
+ });
882
+
810
883
  async function prompt() {
811
- rl.question(` ${FG_TEAL}${BOLD}>${RST} `, async (input) => {
884
+ rl.setPrompt(` ${FG_TEAL}${BOLD}>${RST} `);
885
+ rl.question(rl.getPrompt(), async (input) => {
812
886
  const text = (input || '').trim();
813
887
 
814
888
  if (!text) return prompt();
@@ -823,7 +897,9 @@ async function cmdChat(opts) {
823
897
  console.log(`
824
898
  ${FG_BLUE}${BOLD}Commands:${RST}
825
899
  ${FG_CYAN}/file <path>${RST} ${FG_GRAY}Load file or dir into context${RST}
826
- ${FG_CYAN}/model <name>${RST} ${FG_GRAY}Switch model${RST}
900
+ ${FG_CYAN}/model${RST} ${FG_GRAY}Choose saved model profile${RST}
901
+ ${FG_CYAN}/model <name>${RST} ${FG_GRAY}Switch model manually${RST}
902
+ ${FG_CYAN}/models${RST} ${FG_GRAY}Choose saved model profile${RST}
827
903
  ${FG_CYAN}/clear${RST} ${FG_GRAY}Clear conversation${RST}
828
904
  ${FG_CYAN}/compact${RST} ${FG_GRAY}Show token usage${RST}
829
905
  ${FG_CYAN}/shell <cmd>${RST} ${FG_GRAY}Run shell command directly${RST}
@@ -844,6 +920,14 @@ async function cmdChat(opts) {
844
920
  return prompt();
845
921
  }
846
922
 
923
+ if (text === '/model' || text === '/models') {
924
+ chooseSavedModelProfile(rl, currentModel, cwd, (nextModel) => {
925
+ currentModel = nextModel;
926
+ prompt();
927
+ });
928
+ return;
929
+ }
930
+
847
931
  if (text.startsWith('/model ')) {
848
932
  currentModel = text.slice(7).trim();
849
933
  console.log(` ${FG_GREEN}✓${RST} ${FG_GRAY}Model → ${currentModel}${RST}`);
@@ -889,7 +973,9 @@ async function cmdChat(opts) {
889
973
  console.log(` ${FG_DARK}${'─'.repeat(Math.min(cols, 70) - 4)}${RST}`);
890
974
 
891
975
  rl.pause();
976
+ isRunningAgent = true;
892
977
  messages = await runAgentLoop(messages, currentModel);
978
+ isRunningAgent = false;
893
979
  rl.resume();
894
980
 
895
981
  console.log(` ${FG_DARK}${'━'.repeat(Math.min(cols, 70) - 4)}${RST}`);
@@ -977,41 +1063,60 @@ async function cmdShell(opts, commandArgs) {
977
1063
  }
978
1064
 
979
1065
  async function cmdModels() {
980
- const url = apiUrl('/v1/models');
981
- let res;
982
- try {
983
- res = await httpRequest(url, {
984
- method: 'GET',
985
- headers: { 'Authorization': `Bearer ${config.api_key}` },
986
- timeout: 10000,
987
- }, null);
988
- } catch (e) {
989
- console.log(` ${FG_RED}✗ ${e.message}${RST}`);
1066
+ if (!config.models.length) {
1067
+ console.log(` ${FG_RED}✗${RST} ${FG_GRAY}No saved model profiles. Use semalt-code models add first.${RST}`);
990
1068
  return;
991
1069
  }
992
1070
 
993
- return new Promise((resolve) => {
994
- let data = '';
995
- res.setEncoding('utf8');
996
- res.on('data', chunk => data += chunk);
997
- res.on('end', () => {
998
- try {
999
- const parsed = JSON.parse(data);
1000
- console.log();
1001
- console.log(` ${FG_TEAL}${BOLD} Available Models${RST}`);
1002
- console.log(` ${FG_DARK}${'─'.repeat(40)}${RST}`);
1003
- for (const m of (parsed.data || [])) console.log(` ${FG_GREEN}●${RST} ${m.id}`);
1004
- console.log();
1005
- } catch (e) {
1006
- console.log(` ${FG_RED}✗ ${e.message}${RST}`);
1007
- }
1008
- resolve();
1009
- });
1010
- res.on('error', (e) => {
1011
- console.log(` ${FG_RED}✗ ${e.message}${RST}`);
1012
- resolve();
1013
- });
1071
+ console.log();
1072
+ console.log(` ${FG_TEAL}${BOLD}◆ Saved Models${RST}`);
1073
+ console.log(` ${FG_DARK}${''.repeat(40)}${RST}`);
1074
+ config.models.forEach((profile, index) => {
1075
+ const active = profile.api_base === config.api_base &&
1076
+ profile.api_key === config.api_key &&
1077
+ profile.model === config.default_model;
1078
+ const marker = active ? `${FG_GREEN}●${RST}` : `${FG_DARK}○${RST}`;
1079
+ console.log(` ${marker} ${FG_CYAN}${index + 1}.${RST} ${describeModelProfile(profile)}`);
1014
1080
  });
1081
+ console.log();
1082
+ }
1083
+
1084
+ async function cmdModelsAdd() {
1085
+ const rl = readline.createInterface({
1086
+ input: process.stdin,
1087
+ output: process.stdout,
1088
+ terminal: true,
1089
+ });
1090
+
1091
+ function ask(question) {
1092
+ return new Promise((resolve) => {
1093
+ rl.question(question, (answer) => resolve((answer || '').trim()));
1094
+ });
1095
+ }
1096
+
1097
+ console.log();
1098
+ console.log(` ${FG_TEAL}${BOLD}◆ Add Model Profile${RST}`);
1099
+ console.log(` ${FG_DARK}${'─'.repeat(40)}${RST}`);
1100
+
1101
+ const apiBase = await ask(` ${FG_CYAN}API Base URL:${RST} `);
1102
+ const apiKey = await ask(` ${FG_CYAN}API Key:${RST} `);
1103
+ const modelId = await ask(` ${FG_CYAN}Model ID:${RST} `);
1104
+ rl.close();
1105
+
1106
+ if (!apiBase || !modelId) {
1107
+ console.log(`\n ${FG_RED}✗${RST} ${FG_GRAY}API Base URL and Model ID are required.${RST}\n`);
1108
+ return;
1109
+ }
1110
+
1111
+ const profile = {
1112
+ api_base: apiBase,
1113
+ api_key: apiKey || 'any',
1114
+ model: modelId,
1115
+ };
1116
+
1117
+ config.models.push(profile);
1118
+ setActiveModelProfile(profile);
1119
+ console.log(`\n ${FG_GREEN}✓${RST} Saved model profile: ${describeModelProfile(profile)}\n`);
1015
1120
  }
1016
1121
 
1017
1122
  function cmdInit(opts) {
@@ -1022,6 +1127,7 @@ function cmdInit(opts) {
1022
1127
  temperature: 0.7,
1023
1128
  max_tokens: 4096,
1024
1129
  stream: true,
1130
+ models: config.models,
1025
1131
  };
1026
1132
  saveConfig(cfg);
1027
1133
  config = cfg;
@@ -1075,7 +1181,8 @@ Commands:
1075
1181
  code <prompt> Generate code from a prompt
1076
1182
  edit <file> <instruction> Edit a file with AI
1077
1183
  shell <command> Run and optionally analyze a shell command
1078
- models List available models
1184
+ models List saved model profiles
1185
+ models add Add a saved model profile
1079
1186
  init Initialize config
1080
1187
 
1081
1188
  Options:
@@ -1111,7 +1218,8 @@ Config: ${CONFIG_PATH}
1111
1218
  const { opts, positional } = parseArgs(rawArgs.slice(1));
1112
1219
  await cmdShell(opts, positional);
1113
1220
  } else if (command === 'models') {
1114
- await cmdModels();
1221
+ if (rawArgs[1] === 'add') await cmdModelsAdd();
1222
+ else await cmdModels();
1115
1223
  } else if (command === 'init') {
1116
1224
  const { opts } = parseArgs(rawArgs.slice(1));
1117
1225
  cmdInit(opts);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@semalt-ai/code",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
4
  "description": "Self-hosted AI Coding Assistant CLI",
5
5
  "main": "index.js",
6
6
  "bin": {