tmpa-cli 1.0.8 → 1.0.10
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/index.js +89 -30
- package/package.json +1 -1
package/bin/index.js
CHANGED
|
@@ -15,7 +15,7 @@ const C = {
|
|
|
15
15
|
c3: '\x1b[38;2;99;102;241m',
|
|
16
16
|
c4: '\x1b[38;2;168;85;247m',
|
|
17
17
|
green: '\x1b[38;2;34;197;94m',
|
|
18
|
-
cyan: '\x1b[38;2;
|
|
18
|
+
cyan: '\x1b[38;2;6;182;212m',
|
|
19
19
|
yellow: '\x1b[38;2;234;179;8m',
|
|
20
20
|
red: '\x1b[38;2;239;68;68m',
|
|
21
21
|
gray: '\x1b[38;2;148;163;184m',
|
|
@@ -58,12 +58,12 @@ ${C.c4} ██║ ██║ ╚═╝ ██║██║ ██║ █
|
|
|
58
58
|
${C.c4} ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝${C.reset}
|
|
59
59
|
${C.darkGray}────────────────────────────────────────────────────────────${C.reset}
|
|
60
60
|
${C.reset}The Multi Platform AI ${C.green}[Interactive Mode]${C.reset}
|
|
61
|
-
${C.gray}/config : Set API | /models :
|
|
61
|
+
${C.gray}/config : Set API | /models : Interactive Model Selector | /uninstall : Remove | /exit : Exit${C.reset}
|
|
62
62
|
${C.darkGray}────────────────────────────────────────────────────────────${C.reset}
|
|
63
63
|
`);
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
-
|
|
66
|
+
let rl = readline.createInterface({
|
|
67
67
|
input: process.stdin,
|
|
68
68
|
output: process.stdout
|
|
69
69
|
});
|
|
@@ -86,7 +86,7 @@ function askConfig(callback) {
|
|
|
86
86
|
rl.question(`${C.yellow}[>] Enter API Key for ${selected.name}:${C.reset} `, (apiKey) => {
|
|
87
87
|
if (choice.trim() === '7') {
|
|
88
88
|
rl.question(`${C.yellow}[>] Enter Custom Base URL / Endpoint:${C.reset} `, (customUrl) => {
|
|
89
|
-
rl.question(`${C.yellow}[>] Enter Model Name:${C.reset} `, (customModel) => {
|
|
89
|
+
rl.question(`${C.yellow}[>] Enter Specific Model Name:${C.reset} `, (customModel) => {
|
|
90
90
|
config.provider = 'Custom';
|
|
91
91
|
config.apiKey = apiKey.trim();
|
|
92
92
|
config.endpoint = customUrl.trim();
|
|
@@ -102,29 +102,105 @@ function askConfig(callback) {
|
|
|
102
102
|
config.endpoint = selected.endpoint;
|
|
103
103
|
config.model = selected.defaultModel;
|
|
104
104
|
saveConfig(config);
|
|
105
|
-
console.log(`${C.green}[+] Connected to ${selected.name}
|
|
105
|
+
console.log(`${C.green}[+] Connected to ${selected.name}. Default model: ${selected.defaultModel}${C.reset}\n`);
|
|
106
106
|
if (callback) callback();
|
|
107
107
|
}
|
|
108
108
|
});
|
|
109
109
|
});
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
+
function renderInteractiveMenu(models, selectedIndex, pageOffset, pageSize) {
|
|
113
|
+
console.clear();
|
|
114
|
+
console.log(`${C.cyan}=== Select Model for ${config.provider} ===${C.reset}`);
|
|
115
|
+
console.log(`${C.gray}Use Up/Down Arrow keys to navigate, press ENTER to select.${C.reset}`);
|
|
116
|
+
console.log(`${C.darkGray}────────────────────────────────────────────────────────────${C.reset}`);
|
|
117
|
+
|
|
118
|
+
const visibleModels = models.slice(pageOffset, pageOffset + pageSize);
|
|
119
|
+
|
|
120
|
+
visibleModels.forEach((m, idx) => {
|
|
121
|
+
const realIndex = pageOffset + idx;
|
|
122
|
+
const isSelected = realIndex === selectedIndex;
|
|
123
|
+
const bullet = isSelected ? `${C.green}●${C.reset}` : `${C.gray}○${C.reset}`;
|
|
124
|
+
const modelText = isSelected ? `${C.green}${C.c1}${m.id}${C.reset}` : `${C.gray}${m.id}${C.reset}`;
|
|
125
|
+
console.log(` ${bullet} ${modelText}`);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
console.log(`${C.darkGray}────────────────────────────────────────────────────────────${C.reset}`);
|
|
129
|
+
console.log(`${C.yellow}Item ${selectedIndex + 1} of ${models.length}${C.reset} ${C.darkGray}| Current Active: ${config.model}${C.reset}\n`);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function interactiveModelSelect(models) {
|
|
133
|
+
let selectedIndex = 0;
|
|
134
|
+
let pageOffset = 0;
|
|
135
|
+
const pageSize = 10; // Menampilkan 10 item per halaman agar rapi
|
|
136
|
+
|
|
137
|
+
// Matikan readline sementara untuk mengaktifkan keypress listener
|
|
138
|
+
rl.close();
|
|
139
|
+
readline.emitKeypressEvents(process.stdin);
|
|
140
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(true);
|
|
141
|
+
|
|
142
|
+
renderInteractiveMenu(models, selectedIndex, pageOffset, pageSize);
|
|
143
|
+
|
|
144
|
+
return new Promise((resolve) => {
|
|
145
|
+
const onKeypress = (str, key) => {
|
|
146
|
+
if (key.name === 'up') {
|
|
147
|
+
if (selectedIndex > 0) {
|
|
148
|
+
selectedIndex--;
|
|
149
|
+
if (selectedIndex < pageOffset) pageOffset--;
|
|
150
|
+
}
|
|
151
|
+
renderInteractiveMenu(models, selectedIndex, pageOffset, pageSize);
|
|
152
|
+
} else if (key.name === 'down') {
|
|
153
|
+
if (selectedIndex < models.length - 1) {
|
|
154
|
+
selectedIndex++;
|
|
155
|
+
if (selectedIndex >= pageOffset + pageSize) pageOffset++;
|
|
156
|
+
}
|
|
157
|
+
renderInteractiveMenu(models, selectedIndex, pageOffset, pageSize);
|
|
158
|
+
} else if (key.name === 'return' || key.name === 'enter') {
|
|
159
|
+
cleanup();
|
|
160
|
+
const chosen = models[selectedIndex].id;
|
|
161
|
+
config.model = chosen;
|
|
162
|
+
saveConfig(config);
|
|
163
|
+
console.log(`\n${C.green}[+] Successfully switched to model: ${config.model}${C.reset}\n`);
|
|
164
|
+
resolve();
|
|
165
|
+
} else if (key.ctrl && key.name === 'c') {
|
|
166
|
+
cleanup();
|
|
167
|
+
process.exit(0);
|
|
168
|
+
} else if (key.name === 'escape') {
|
|
169
|
+
cleanup();
|
|
170
|
+
console.log(`\n${C.gray}Model selection cancelled.${C.reset}\n`);
|
|
171
|
+
resolve();
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
function cleanup() {
|
|
176
|
+
process.stdin.removeListener('keypress', onKeypress);
|
|
177
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(false);
|
|
178
|
+
// Restart readline interface untuk percakapan selanjutnya
|
|
179
|
+
rl = readline.createInterface({
|
|
180
|
+
input: process.stdin,
|
|
181
|
+
output: process.stdout
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
process.stdin.on('keypress', onKeypress);
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
112
189
|
async function fetchAvailableModels() {
|
|
113
190
|
if (!config.apiKey) {
|
|
114
191
|
console.log(`${C.red}[x] API Key is not set. Use /config first.${C.reset}\n`);
|
|
115
192
|
return startPrompt();
|
|
116
193
|
}
|
|
117
194
|
|
|
118
|
-
console.log(`${C.yellow}[...] Fetching models
|
|
195
|
+
console.log(`${C.yellow}[...] Fetching models list from ${config.provider || 'Provider'}...${C.reset}`);
|
|
119
196
|
|
|
120
197
|
try {
|
|
121
198
|
let url = config.endpoint;
|
|
122
|
-
if (url.
|
|
123
|
-
console.log(`${C.yellow}[!]
|
|
199
|
+
if (url.includes('googleapis.com')) {
|
|
200
|
+
console.log(`${C.yellow}[!] Gemini Default REST API uses fixed model: ${config.model}${C.reset}\n`);
|
|
124
201
|
return startPrompt();
|
|
125
202
|
}
|
|
126
203
|
|
|
127
|
-
// Auto-detect /models endpoint
|
|
128
204
|
let modelsUrl = url.endsWith('/models') ? url : `${url.replace(/\/chat\/completions$/, '')}/models`;
|
|
129
205
|
|
|
130
206
|
const response = await fetch(modelsUrl, {
|
|
@@ -137,26 +213,11 @@ async function fetchAvailableModels() {
|
|
|
137
213
|
|
|
138
214
|
const data = await response.json();
|
|
139
215
|
|
|
140
|
-
if (data.data && Array.isArray(data.data)) {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
modelList.forEach((m, idx) => {
|
|
144
|
-
console.log(` ${C.yellow}${idx + 1}.${C.reset} ${m.id}`);
|
|
145
|
-
});
|
|
146
|
-
console.log(`${C.darkGray}------------------------------------------------------------${C.reset}`);
|
|
147
|
-
|
|
148
|
-
rl.question(`\n${C.yellow}[>] Enter Model Name to use (or press Enter to keep current: ${config.model}):${C.reset} `, (newModel) => {
|
|
149
|
-
if (newModel.trim()) {
|
|
150
|
-
config.model = newModel.trim();
|
|
151
|
-
saveConfig(config);
|
|
152
|
-
console.log(`${C.green}[+] Active model updated to: ${config.model}${C.reset}\n`);
|
|
153
|
-
} else {
|
|
154
|
-
console.log(`${C.gray}Keeping current model: ${config.model}${C.reset}\n`);
|
|
155
|
-
}
|
|
156
|
-
startPrompt();
|
|
157
|
-
});
|
|
216
|
+
if (data.data && Array.isArray(data.data) && data.data.length > 0) {
|
|
217
|
+
await interactiveModelSelect(data.data);
|
|
218
|
+
startPrompt();
|
|
158
219
|
} else {
|
|
159
|
-
console.log(`${C.red}[x]
|
|
220
|
+
console.log(`${C.red}[x] Could not retrieve models list automatically from ${config.provider}.${C.reset}`);
|
|
160
221
|
startPrompt();
|
|
161
222
|
}
|
|
162
223
|
} catch (err) {
|
|
@@ -178,12 +239,10 @@ async function handleChat(prompt) {
|
|
|
178
239
|
let headers = { 'Content-Type': 'application/json' };
|
|
179
240
|
let bodyData = {};
|
|
180
241
|
|
|
181
|
-
// Auto-Detect Request Format (Google Gemini vs OpenAI Compatible)
|
|
182
242
|
if (url.includes('googleapis.com')) {
|
|
183
243
|
url = `${url}?key=${config.apiKey}`;
|
|
184
244
|
bodyData = { contents: [{ parts: [{ text: prompt }] }] };
|
|
185
245
|
} else {
|
|
186
|
-
// OpenAI Compatible (OpenRouter, Groq, NVIDIA, OpenAI, dll)
|
|
187
246
|
if (!url.endsWith('/chat/completions')) {
|
|
188
247
|
url = `${url.replace(/\/$/, '')}/chat/completions`;
|
|
189
248
|
}
|