hasina-gemini-cli 1.0.0 → 1.0.1

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
@@ -174,10 +174,10 @@ npm start
174
174
  | `/exit` | Exit the app cleanly |
175
175
  | `/clear` | Clear in-memory history for the current session |
176
176
  | `/history` | Show recent conversation messages |
177
- | `/models` | Open a numbered Gemini model chooser |
177
+ | `/models` | Open a numbered Gemini model chooser with backend version info |
178
178
  | `/save` | Persist the current session to local session storage |
179
179
  | `/new` | Start a fresh conversation session |
180
- | `/model` | Show the currently active model |
180
+ | `/model` | Show the currently active model with backend version details |
181
181
  | `/use-model <model_name>` | Switch the active Gemini model at runtime |
182
182
  | `/system` | Show the active system prompt |
183
183
  | `/set-system <text>` | Override the current system prompt for this session |
@@ -196,11 +196,13 @@ Info > Type a message to chat, use /models to choose a model, or /help to list c
196
196
  You > /models
197
197
  Command > Choose a Gemini Model
198
198
  01. gemini-2.5-flash [active]
199
- Gemini 2.5 Flash | input 1M | output 65.5K
199
+ Gemini 2.5 Flash
200
+ version=2.5-flash | input=1M | output=65.5K
200
201
  02. gemini-2.5-pro
201
- Gemini 2.5 Pro | input 1M | output 65.5K
202
+ Gemini 2.5 Pro
203
+ version=2.5-pro | input=1M | output=65.5K
202
204
  Model > 2
203
- Success > Active model changed to "gemini-2.5-pro".
205
+ Success > Active model changed to "gemini-2.5-pro" (version 2.5-pro).
204
206
 
205
207
  You > Explain event loops in Node.js.
206
208
  Gemini > The Node.js event loop coordinates timers, I/O callbacks, microtasks,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hasina-gemini-cli",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "Production-ready terminal AI chat application powered by the official Gemini API SDK.",
5
5
  "main": "src/index.js",
6
6
  "bin": {
package/src/app.js CHANGED
@@ -13,6 +13,32 @@ function normalizeAssistantText(text) {
13
13
  return text.trim();
14
14
  }
15
15
 
16
+ function normalizeIntentText(text) {
17
+ return String(text)
18
+ .normalize('NFD')
19
+ .replace(/[\u0300-\u036f]/g, '')
20
+ .toLowerCase()
21
+ .trim();
22
+ }
23
+
24
+ function isModelIdentityQuestion(text) {
25
+ const normalized = normalizeIntentText(text);
26
+ const patterns = [
27
+ /identifiant.*modele/,
28
+ /id.*modele/,
29
+ /modele.*exact/,
30
+ /quel.*modele.*(utilise|actif|es)/,
31
+ /donne.*modele/,
32
+ /model.*identifier/,
33
+ /exact.*model.*id/,
34
+ /what.*model.*are.*you/,
35
+ /which.*model.*are.*you/,
36
+ /model.*id/,
37
+ ];
38
+
39
+ return patterns.some((pattern) => pattern.test(normalized));
40
+ }
41
+
16
42
  class App {
17
43
  constructor({ config, provider, printer }) {
18
44
  this.config = config;
@@ -177,12 +203,14 @@ class App {
177
203
  }
178
204
 
179
205
  if (selectedModel.id === this.state.model) {
180
- this.printer.printInfo(`"${selectedModel.id}" is already the active model.`);
206
+ const versionSuffix = selectedModel.version ? ` (version ${selectedModel.version})` : '';
207
+ this.printer.printInfo(`"${selectedModel.id}" is already the active model${versionSuffix}.`);
181
208
  return false;
182
209
  }
183
210
 
184
211
  this.state.model = selectedModel.id;
185
- this.printer.printSuccess(`Active model changed to "${selectedModel.id}".`);
212
+ const versionSuffix = selectedModel.version ? ` (version ${selectedModel.version})` : '';
213
+ this.printer.printSuccess(`Active model changed to "${selectedModel.id}"${versionSuffix}.`);
186
214
  return false;
187
215
  }
188
216
 
@@ -206,6 +234,12 @@ class App {
206
234
  }
207
235
 
208
236
  async handleChatTurn(userInput) {
237
+ const handledLocally = await this.tryHandleLocalModelIdentityQuestion(userInput);
238
+
239
+ if (handledLocally) {
240
+ return;
241
+ }
242
+
209
243
  const loading = this.printer.createLoadingIndicator();
210
244
  let streamStarted = false;
211
245
 
@@ -257,6 +291,33 @@ class App {
257
291
  }
258
292
  }
259
293
 
294
+ async tryHandleLocalModelIdentityQuestion(userInput) {
295
+ if (!isModelIdentityQuestion(userInput)) {
296
+ return false;
297
+ }
298
+
299
+ let message = `Identifiant du modele (local): ${this.state.model}.`;
300
+
301
+ if (typeof this.provider.getModelInfo === 'function') {
302
+ try {
303
+ const modelInfo = await this.provider.getModelInfo(this.state.model);
304
+
305
+ if (modelInfo?.version) {
306
+ message = `Identifiant du modele (local): ${modelInfo.id} (version ${modelInfo.version}).`;
307
+ } else if (modelInfo?.id) {
308
+ message = `Identifiant du modele (local): ${modelInfo.id}.`;
309
+ }
310
+ } catch (_error) {
311
+ // Ignore lookup failures and use the active model from local state.
312
+ }
313
+ }
314
+
315
+ this.printer.printAssistant(message);
316
+ this.state.historyService.addMessage('user', userInput);
317
+ this.state.historyService.addMessage('assistant', message);
318
+ return true;
319
+ }
320
+
260
321
  async shutdown() {
261
322
  if (this.readline) {
262
323
  try {
@@ -53,7 +53,7 @@ function extractResponseText(response) {
53
53
  .join('');
54
54
  }
55
55
 
56
- function normalizeListedModel(model) {
56
+ function normalizeModelInfo(model) {
57
57
  const id = String(model?.name || '').replace(/^models\//, '').trim();
58
58
 
59
59
  if (!id) {
@@ -62,7 +62,9 @@ function normalizeListedModel(model) {
62
62
 
63
63
  return {
64
64
  id,
65
+ apiName: String(model.name || `models/${id}`),
65
66
  displayName: model.displayName || id,
67
+ version: typeof model.version === 'string' && model.version.trim() ? model.version.trim() : null,
66
68
  description: model.description || '',
67
69
  inputTokenLimit: Number.isFinite(model.inputTokenLimit) ? model.inputTokenLimit : null,
68
70
  outputTokenLimit: Number.isFinite(model.outputTokenLimit) ? model.outputTokenLimit : null,
@@ -194,6 +196,20 @@ function createGeminiProvider({ apiKey }) {
194
196
  }
195
197
  },
196
198
 
199
+ async getModelInfo(model) {
200
+ const normalizedModel = normalizeModelName(model);
201
+
202
+ try {
203
+ const response = await client.models.get({ model: normalizedModel });
204
+ return normalizeModelInfo(response);
205
+ } catch (error) {
206
+ throw createFriendlyGeminiError(
207
+ error,
208
+ `Unable to load details for model "${normalizedModel}".`
209
+ );
210
+ }
211
+ },
212
+
197
213
  async listModels({ currentModel, pageSize = 100 } = {}) {
198
214
  try {
199
215
  const pager = await client.models.list({
@@ -204,7 +220,7 @@ function createGeminiProvider({ apiKey }) {
204
220
  const models = [];
205
221
 
206
222
  for await (const model of pager) {
207
- const normalized = normalizeListedModel(model);
223
+ const normalized = normalizeModelInfo(model);
208
224
 
209
225
  if (!isChatCapableModel(normalized)) {
210
226
  continue;
@@ -68,6 +68,10 @@ function formatTokenCount(value) {
68
68
  return String(value);
69
69
  }
70
70
 
71
+ function formatModelVersion(model) {
72
+ return model?.version || 'unknown';
73
+ }
74
+
71
75
  class CommandService {
72
76
  constructor({ sessionService, provider }) {
73
77
  this.sessionService = sessionService;
@@ -129,7 +133,7 @@ class CommandService {
129
133
  return this.handleNew(state);
130
134
 
131
135
  case 'model':
132
- return messageResult('info', `Active model: ${state.model}`);
136
+ return this.handleModel(state);
133
137
 
134
138
  case 'use-model':
135
139
  return this.handleUseModel(state, command.rawArgs);
@@ -203,16 +207,48 @@ class CommandService {
203
207
  const header = `${String(index + 1).padStart(2, '0')}. ${model.id}${
204
208
  tags.length ? ` [${tags.join(', ')}]` : ''
205
209
  }`;
206
- const details = `${compactText(model.displayName, 48)} | input ${formatTokenCount(
207
- model.inputTokenLimit
208
- )} | output ${formatTokenCount(model.outputTokenLimit)}`;
209
-
210
- return `${header}\n ${details}`;
210
+ const details = [
211
+ compactText(model.displayName, 64),
212
+ `version=${formatModelVersion(model)} | input=${formatTokenCount(
213
+ model.inputTokenLimit
214
+ )} | output=${formatTokenCount(model.outputTokenLimit)}`,
215
+ ].map((line) => ` ${line}`).join('\n');
216
+
217
+ return `${header}\n${details}`;
211
218
  });
212
219
 
213
220
  return modelPickerResult('Choose a Gemini Model', lines, models);
214
221
  }
215
222
 
223
+ async handleModel(state) {
224
+ if (typeof this.provider.getModelInfo !== 'function') {
225
+ return messageResult('info', `Active model: ${state.model}`);
226
+ }
227
+
228
+ const model = await this.provider.getModelInfo(state.model);
229
+ const tags = [];
230
+
231
+ if (model.isPreview) {
232
+ tags.push('preview');
233
+ }
234
+
235
+ if (model.isLatest) {
236
+ tags.push('latest');
237
+ }
238
+
239
+ const lines = [
240
+ `id=${model.id}${tags.length ? ` [${tags.join(', ')}]` : ''}`,
241
+ `display=${model.displayName}`,
242
+ `version=${formatModelVersion(model)}`,
243
+ `api=${model.apiName}`,
244
+ `input=${formatTokenCount(model.inputTokenLimit)} | output=${formatTokenCount(
245
+ model.outputTokenLimit
246
+ )}`,
247
+ ];
248
+
249
+ return blockResult('Active Model', lines);
250
+ }
251
+
216
252
  async handleSave(state) {
217
253
  const savedSession = await this.sessionService.saveSession({
218
254
  id: state.sessionId,
@@ -244,10 +280,15 @@ class CommandService {
244
280
  assertCommandArgument(rawArgs, 'Usage: /use-model <model_name>')
245
281
  );
246
282
 
247
- await this.provider.validateModel(modelName);
283
+ const model = typeof this.provider.getModelInfo === 'function'
284
+ ? await this.provider.getModelInfo(modelName)
285
+ : await this.provider.validateModel(modelName);
248
286
  state.model = modelName;
249
287
 
250
- return messageResult('success', `Active model changed to "${modelName}".`);
288
+ return messageResult(
289
+ 'success',
290
+ `Active model changed to "${modelName}" (version ${formatModelVersion(model)}).`
291
+ );
251
292
  }
252
293
 
253
294
  handleSetSystem(state, rawArgs) {