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 +7 -5
- package/package.json +1 -1
- package/src/app.js +63 -2
- package/src/config/gemini.js +18 -2
- package/src/services/command.service.js +49 -8
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
|
|
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
|
|
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
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
|
-
|
|
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
|
-
|
|
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 {
|
package/src/config/gemini.js
CHANGED
|
@@ -53,7 +53,7 @@ function extractResponseText(response) {
|
|
|
53
53
|
.join('');
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
function
|
|
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 =
|
|
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
|
|
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 =
|
|
207
|
-
model.
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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
|
-
|
|
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(
|
|
288
|
+
return messageResult(
|
|
289
|
+
'success',
|
|
290
|
+
`Active model changed to "${modelName}" (version ${formatModelVersion(model)}).`
|
|
291
|
+
);
|
|
251
292
|
}
|
|
252
293
|
|
|
253
294
|
handleSetSystem(state, rawArgs) {
|