@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.
- package/README.md +22 -1
- package/index.js +146 -38
- 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
|
|
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
|
|
52
|
+
return normalizeConfig(data);
|
|
33
53
|
} catch {}
|
|
34
54
|
}
|
|
35
|
-
return
|
|
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.
|
|
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
|
|
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
|
-
|
|
981
|
-
|
|
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
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
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
|
|
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
|
|
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);
|