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.
Files changed (2) hide show
  1. package/bin/index.js +89 -30
  2. 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;6x;182;212m',
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 : View Models | /uninstall : Remove | /exit : Exit${C.reset}
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
- const rl = readline.createInterface({
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} (${selected.defaultModel})${C.reset}\n`);
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 for ${config.provider || 'Provider'}...${C.reset}`);
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.endsWith('/generateContent')) {
123
- console.log(`${C.yellow}[!] Google Gemini default endpoint uses fixed model: ${config.model}${C.reset}\n`);
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
- console.log(`\n${C.cyan}=== Available Models ===${C.reset}`);
142
- const modelList = data.data.slice(0, 20); // Limit top 20 models
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] Unable to fetch models automatically from this provider.${C.reset}\n`);
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tmpa-cli",
3
- "version": "1.0.8",
3
+ "version": "1.0.10",
4
4
  "description": "",
5
5
  "main": "bin/index.js",
6
6
  "bin": {