@rlabs-inc/gemini-mcp 0.6.3 → 0.7.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 +46 -43
- package/dist/cli/commands/config.d.ts +8 -0
- package/dist/cli/commands/config.js +147 -0
- package/dist/cli/commands/image.d.ts +7 -0
- package/dist/cli/commands/image.js +133 -0
- package/dist/cli/commands/query.d.ts +7 -0
- package/dist/cli/commands/query.js +94 -0
- package/dist/cli/commands/research.d.ts +7 -0
- package/dist/cli/commands/research.js +147 -0
- package/dist/cli/commands/search.d.ts +7 -0
- package/dist/cli/commands/search.js +152 -0
- package/dist/cli/commands/speak.d.ts +7 -0
- package/dist/cli/commands/speak.js +168 -0
- package/dist/cli/commands/tokens.d.ts +8 -0
- package/dist/cli/commands/tokens.js +105 -0
- package/dist/cli/commands/video.d.ts +7 -0
- package/dist/cli/commands/video.js +154 -0
- package/dist/cli/config.d.ts +23 -0
- package/dist/cli/config.js +89 -0
- package/dist/cli/index.d.ts +6 -0
- package/dist/cli/index.js +180 -0
- package/dist/cli/ui/box.d.ts +20 -0
- package/dist/cli/ui/box.js +112 -0
- package/dist/cli/ui/colors.d.ts +46 -0
- package/dist/cli/ui/colors.js +106 -0
- package/dist/cli/ui/index.d.ts +21 -0
- package/dist/cli/ui/index.js +42 -0
- package/dist/cli/ui/progress.d.ts +37 -0
- package/dist/cli/ui/progress.js +125 -0
- package/dist/cli/ui/spinner.d.ts +42 -0
- package/dist/cli/ui/spinner.js +96 -0
- package/dist/cli/ui/theme.d.ts +48 -0
- package/dist/cli/ui/theme.js +200 -0
- package/dist/index.d.ts +6 -3
- package/dist/index.js +26 -218
- package/dist/server.d.ts +7 -0
- package/dist/server.js +221 -0
- package/dist/tools/deep-research.js +2 -2
- package/package.json +9 -3
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Research Command
|
|
3
|
+
*
|
|
4
|
+
* Deep research agent for comprehensive investigation.
|
|
5
|
+
* gemini research "your research question"
|
|
6
|
+
*/
|
|
7
|
+
import { parseArgs } from 'node:util';
|
|
8
|
+
import { initGeminiClient, startDeepResearch, checkDeepResearch } from '../../gemini-client.js';
|
|
9
|
+
import { setupLogger } from '../../utils/logger.js';
|
|
10
|
+
import { spinner, progress, print, printError, printSuccess, printMuted, printWarning, t, header, box } from '../ui/index.js';
|
|
11
|
+
function showHelp() {
|
|
12
|
+
const theme = t();
|
|
13
|
+
print(header('gemini research', 'Deep research agent'));
|
|
14
|
+
print('');
|
|
15
|
+
print(theme.colors.primary('Usage:'));
|
|
16
|
+
print(` gemini research ${theme.colors.muted('"your research question"')}`);
|
|
17
|
+
print('');
|
|
18
|
+
print(theme.colors.primary('Options:'));
|
|
19
|
+
print(` ${theme.colors.highlight('--format, -f')} ${theme.colors.muted('Output format: report, outline, brief (default: report)')}`);
|
|
20
|
+
print(` ${theme.colors.highlight('--wait, -w')} ${theme.colors.muted('Wait for completion (can take 5-60 mins)')}`);
|
|
21
|
+
print(` ${theme.colors.highlight('--help, -h')} ${theme.colors.muted('Show this help')}`);
|
|
22
|
+
print('');
|
|
23
|
+
print(theme.colors.primary('Examples:'));
|
|
24
|
+
print(theme.colors.muted(' gemini research "MCP ecosystem and best practices"'));
|
|
25
|
+
print(theme.colors.muted(' gemini research "AI coding assistants comparison" --format outline'));
|
|
26
|
+
print(theme.colors.muted(' gemini research "Bun vs Node.js performance" --wait'));
|
|
27
|
+
print('');
|
|
28
|
+
print(theme.colors.warning(`${theme.symbols.warning} Deep research typically takes 5-20 minutes (max 60 min)`));
|
|
29
|
+
}
|
|
30
|
+
export async function researchCommand(argv) {
|
|
31
|
+
const { values, positionals } = parseArgs({
|
|
32
|
+
args: argv,
|
|
33
|
+
options: {
|
|
34
|
+
help: { type: 'boolean', short: 'h', default: false },
|
|
35
|
+
format: { type: 'string', short: 'f', default: 'report' },
|
|
36
|
+
wait: { type: 'boolean', short: 'w', default: false },
|
|
37
|
+
},
|
|
38
|
+
allowPositionals: true,
|
|
39
|
+
});
|
|
40
|
+
if (values.help) {
|
|
41
|
+
showHelp();
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
// Get query from positional args
|
|
45
|
+
const query = positionals.join(' ');
|
|
46
|
+
if (!query) {
|
|
47
|
+
printError('No research question provided');
|
|
48
|
+
printMuted('Usage: gemini research "your question"');
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
const theme = t();
|
|
52
|
+
const s = spinner();
|
|
53
|
+
const format = values.format;
|
|
54
|
+
const shouldWait = values.wait;
|
|
55
|
+
try {
|
|
56
|
+
// Suppress logger output for CLI
|
|
57
|
+
setupLogger('quiet');
|
|
58
|
+
// Initialize Gemini client
|
|
59
|
+
s.start('Connecting to Gemini...');
|
|
60
|
+
await initGeminiClient();
|
|
61
|
+
// Build the research prompt with format
|
|
62
|
+
let researchPrompt = query;
|
|
63
|
+
if (format && format !== 'report') {
|
|
64
|
+
researchPrompt = `${query}\n\nFormat the output as: ${format}`;
|
|
65
|
+
}
|
|
66
|
+
// Start the research
|
|
67
|
+
s.update('Starting deep research agent...');
|
|
68
|
+
const result = await startDeepResearch(researchPrompt);
|
|
69
|
+
s.success('Research started!');
|
|
70
|
+
print('');
|
|
71
|
+
// Show research info
|
|
72
|
+
const infoLines = [
|
|
73
|
+
`${theme.colors.primary('Research ID:')} ${result.id}`,
|
|
74
|
+
`${theme.colors.primary('Query:')} ${query.substring(0, 60)}${query.length > 60 ? '...' : ''}`,
|
|
75
|
+
`${theme.colors.primary('Format:')} ${format}`,
|
|
76
|
+
`${theme.colors.primary('Status:')} ${theme.colors.warning('In Progress')}`,
|
|
77
|
+
];
|
|
78
|
+
print(box(infoLines, { title: 'Deep Research' }));
|
|
79
|
+
print('');
|
|
80
|
+
if (!shouldWait) {
|
|
81
|
+
// Not waiting - give instructions
|
|
82
|
+
print(theme.colors.info(`${theme.symbols.info} Research is running in the background.`));
|
|
83
|
+
print('');
|
|
84
|
+
print('To check status:');
|
|
85
|
+
print(theme.colors.muted(` gemini research-status ${result.id}`));
|
|
86
|
+
print('');
|
|
87
|
+
print(theme.colors.muted('Research typically takes 5-20 minutes (max 60 min).'));
|
|
88
|
+
print(theme.colors.muted('Results will be saved to your configured output directory.'));
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
// Wait for completion
|
|
92
|
+
print(theme.colors.info(`${theme.symbols.info} Waiting for research to complete...`));
|
|
93
|
+
print(theme.colors.muted('This may take 5-60 minutes. Press Ctrl+C to exit (research continues in background).'));
|
|
94
|
+
print('');
|
|
95
|
+
const p = progress({ total: 100, showEta: false });
|
|
96
|
+
p.start('Researching');
|
|
97
|
+
let lastStatus = 'pending';
|
|
98
|
+
let attempts = 0;
|
|
99
|
+
const maxAttempts = 180; // 30 seconds * 180 = 90 minutes max
|
|
100
|
+
const pollInterval = 30000; // 30 seconds
|
|
101
|
+
while (attempts < maxAttempts) {
|
|
102
|
+
await new Promise(resolve => setTimeout(resolve, pollInterval));
|
|
103
|
+
attempts++;
|
|
104
|
+
// Update progress (fake progress since we don't know actual %)
|
|
105
|
+
const fakeProgress = Math.min(95, attempts * 2);
|
|
106
|
+
p.update(fakeProgress, `Researching (${Math.floor(attempts * 0.5)}m)`);
|
|
107
|
+
try {
|
|
108
|
+
const status = await checkDeepResearch(result.id);
|
|
109
|
+
if (status.status === 'completed') {
|
|
110
|
+
p.done('Research complete!');
|
|
111
|
+
print('');
|
|
112
|
+
// Show the results
|
|
113
|
+
if (status.outputs && status.outputs.length > 0) {
|
|
114
|
+
const resultText = status.outputs[status.outputs.length - 1].text || 'No text output';
|
|
115
|
+
print(theme.colors.primary('Research Results:'));
|
|
116
|
+
print('');
|
|
117
|
+
print(resultText);
|
|
118
|
+
}
|
|
119
|
+
if (status.savedPath) {
|
|
120
|
+
print('');
|
|
121
|
+
printSuccess(`Full response saved to: ${status.savedPath}`);
|
|
122
|
+
}
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
else if (status.status === 'failed') {
|
|
126
|
+
p.fail('Research failed');
|
|
127
|
+
printError(status.error || 'Unknown error');
|
|
128
|
+
process.exit(1);
|
|
129
|
+
}
|
|
130
|
+
lastStatus = status.status;
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
// Polling error - continue trying
|
|
134
|
+
console.error('Polling error:', error);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
// Timed out
|
|
138
|
+
p.fail('Research timed out');
|
|
139
|
+
printWarning('Research is still running. Check status later:');
|
|
140
|
+
print(theme.colors.muted(` gemini research-status ${result.id}`));
|
|
141
|
+
}
|
|
142
|
+
catch (error) {
|
|
143
|
+
s.error('Research failed');
|
|
144
|
+
printError(error instanceof Error ? error.message : String(error));
|
|
145
|
+
process.exit(1);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Search Command
|
|
3
|
+
*
|
|
4
|
+
* Real-time web search powered by Gemini + Google Search.
|
|
5
|
+
* gemini search "your query"
|
|
6
|
+
*/
|
|
7
|
+
import { parseArgs } from 'node:util';
|
|
8
|
+
import { GoogleGenAI } from '@google/genai';
|
|
9
|
+
import { setupLogger } from '../../utils/logger.js';
|
|
10
|
+
import { spinner, print, printError, printMuted, t, header } from '../ui/index.js';
|
|
11
|
+
function showHelp() {
|
|
12
|
+
const theme = t();
|
|
13
|
+
print(header('gemini search', 'Real-time web search'));
|
|
14
|
+
print('');
|
|
15
|
+
print(theme.colors.primary('Usage:'));
|
|
16
|
+
print(` gemini search ${theme.colors.muted('"your query"')}`);
|
|
17
|
+
print('');
|
|
18
|
+
print(theme.colors.primary('Options:'));
|
|
19
|
+
print(` ${theme.colors.highlight('--no-citations')} ${theme.colors.muted('Hide inline citations')}`);
|
|
20
|
+
print(` ${theme.colors.highlight('--help, -h')} ${theme.colors.muted('Show this help')}`);
|
|
21
|
+
print('');
|
|
22
|
+
print(theme.colors.primary('Examples:'));
|
|
23
|
+
print(theme.colors.muted(' gemini search "latest news about AI"'));
|
|
24
|
+
print(theme.colors.muted(' gemini search "weather in São Paulo"'));
|
|
25
|
+
print(theme.colors.muted(' gemini search "MCP Model Context Protocol"'));
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Add inline citations to text based on grounding metadata
|
|
29
|
+
*/
|
|
30
|
+
function addCitations(text, supports, chunks) {
|
|
31
|
+
if (!supports || !chunks || supports.length === 0) {
|
|
32
|
+
return text;
|
|
33
|
+
}
|
|
34
|
+
// Sort supports by endIndex in descending order to avoid shifting issues
|
|
35
|
+
const sortedSupports = [...supports].sort((a, b) => (b.segment?.endIndex ?? 0) - (a.segment?.endIndex ?? 0));
|
|
36
|
+
let result = text;
|
|
37
|
+
for (const support of sortedSupports) {
|
|
38
|
+
const endIndex = support.segment?.endIndex;
|
|
39
|
+
if (endIndex === undefined || !support.groundingChunkIndices?.length) {
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
const citationLinks = support.groundingChunkIndices
|
|
43
|
+
.map((i) => {
|
|
44
|
+
const uri = chunks[i]?.web?.uri;
|
|
45
|
+
const title = chunks[i]?.web?.title;
|
|
46
|
+
if (uri) {
|
|
47
|
+
return `[${title || i + 1}](${uri})`;
|
|
48
|
+
}
|
|
49
|
+
return null;
|
|
50
|
+
})
|
|
51
|
+
.filter(Boolean);
|
|
52
|
+
if (citationLinks.length > 0) {
|
|
53
|
+
const citationString = ' ' + citationLinks.join(', ');
|
|
54
|
+
result = result.slice(0, endIndex) + citationString + result.slice(endIndex);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return result;
|
|
58
|
+
}
|
|
59
|
+
export async function searchCommand(argv) {
|
|
60
|
+
const { values, positionals } = parseArgs({
|
|
61
|
+
args: argv,
|
|
62
|
+
options: {
|
|
63
|
+
help: { type: 'boolean', short: 'h', default: false },
|
|
64
|
+
'no-citations': { type: 'boolean', default: false },
|
|
65
|
+
},
|
|
66
|
+
allowPositionals: true,
|
|
67
|
+
});
|
|
68
|
+
if (values.help) {
|
|
69
|
+
showHelp();
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
// Get query from positional args
|
|
73
|
+
const query = positionals.join(' ');
|
|
74
|
+
if (!query) {
|
|
75
|
+
printError('No search query provided');
|
|
76
|
+
printMuted('Usage: gemini search "your query"');
|
|
77
|
+
process.exit(1);
|
|
78
|
+
}
|
|
79
|
+
const theme = t();
|
|
80
|
+
const s = spinner();
|
|
81
|
+
const returnCitations = !values['no-citations'];
|
|
82
|
+
try {
|
|
83
|
+
// Suppress logger output for CLI
|
|
84
|
+
setupLogger('quiet');
|
|
85
|
+
const apiKey = process.env.GEMINI_API_KEY;
|
|
86
|
+
if (!apiKey) {
|
|
87
|
+
throw new Error('GEMINI_API_KEY not set');
|
|
88
|
+
}
|
|
89
|
+
s.start('Searching the web...');
|
|
90
|
+
const genAI = new GoogleGenAI({ apiKey });
|
|
91
|
+
const model = process.env.GEMINI_PRO_MODEL || 'gemini-3-pro-preview';
|
|
92
|
+
// Execute with Google Search tool enabled
|
|
93
|
+
const response = await genAI.models.generateContent({
|
|
94
|
+
model,
|
|
95
|
+
contents: query,
|
|
96
|
+
config: {
|
|
97
|
+
tools: [{ googleSearch: {} }],
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
const candidate = response.candidates?.[0];
|
|
101
|
+
if (!candidate) {
|
|
102
|
+
throw new Error('No response from search');
|
|
103
|
+
}
|
|
104
|
+
let responseText = response.text || '';
|
|
105
|
+
const groundingMetadata = candidate.groundingMetadata;
|
|
106
|
+
// Build response with citations if requested
|
|
107
|
+
const sources = [];
|
|
108
|
+
if (returnCitations && groundingMetadata) {
|
|
109
|
+
const supports = groundingMetadata.groundingSupports || [];
|
|
110
|
+
const chunks = groundingMetadata.groundingChunks || [];
|
|
111
|
+
if (supports.length > 0 && chunks.length > 0) {
|
|
112
|
+
responseText = addCitations(responseText, supports, chunks);
|
|
113
|
+
}
|
|
114
|
+
// Collect unique sources
|
|
115
|
+
const seenUrls = new Set();
|
|
116
|
+
for (const chunk of chunks) {
|
|
117
|
+
if (chunk.web?.uri && !seenUrls.has(chunk.web.uri)) {
|
|
118
|
+
seenUrls.add(chunk.web.uri);
|
|
119
|
+
sources.push({
|
|
120
|
+
title: chunk.web.title || 'Source',
|
|
121
|
+
url: chunk.web.uri,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
s.success('Search complete');
|
|
127
|
+
print('');
|
|
128
|
+
// Display the response
|
|
129
|
+
print(responseText);
|
|
130
|
+
print('');
|
|
131
|
+
// Display sources if available
|
|
132
|
+
if (sources.length > 0) {
|
|
133
|
+
print(theme.colors.muted('─'.repeat(40)));
|
|
134
|
+
print(theme.colors.primary('Sources:'));
|
|
135
|
+
for (const source of sources) {
|
|
136
|
+
print(` ${theme.symbols.bullet} ${theme.colors.info(source.title)}`);
|
|
137
|
+
print(` ${theme.colors.muted(source.url)}`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
// Show search queries used
|
|
141
|
+
if (groundingMetadata?.webSearchQueries?.length) {
|
|
142
|
+
print('');
|
|
143
|
+
print(theme.colors.muted(`Searches: ${groundingMetadata.webSearchQueries.join(', ')}`));
|
|
144
|
+
}
|
|
145
|
+
print('');
|
|
146
|
+
}
|
|
147
|
+
catch (error) {
|
|
148
|
+
s.error('Search failed');
|
|
149
|
+
printError(error instanceof Error ? error.message : String(error));
|
|
150
|
+
process.exit(1);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Speak Command
|
|
3
|
+
*
|
|
4
|
+
* Text-to-speech with multiple voices.
|
|
5
|
+
* gemini speak "your text" --voice Kore
|
|
6
|
+
*/
|
|
7
|
+
import { parseArgs } from 'node:util';
|
|
8
|
+
import { GoogleGenAI, Modality } from '@google/genai';
|
|
9
|
+
import { setupLogger } from '../../utils/logger.js';
|
|
10
|
+
import { getOutputDir } from '../../gemini-client.js';
|
|
11
|
+
import { spinner, print, printError, printSuccess, printMuted, t, header, box } from '../ui/index.js';
|
|
12
|
+
import { join } from 'node:path';
|
|
13
|
+
// Available voices
|
|
14
|
+
const VOICES = [
|
|
15
|
+
'Zephyr', 'Puck', 'Charon', 'Kore', 'Fenrir', 'Leda', 'Orus', 'Aoede',
|
|
16
|
+
'Callirrhoe', 'Autonoe', 'Enceladus', 'Iapetus', 'Umbriel', 'Algieba',
|
|
17
|
+
'Despina', 'Erinome', 'Algenib', 'Rasalgethi', 'Laomedeia', 'Achernar',
|
|
18
|
+
'Alnilam', 'Schedar', 'Gacrux', 'Pulcherrima', 'Achird', 'Zubenelgenubi',
|
|
19
|
+
'Vindemiatrix', 'Sadachbia', 'Sadaltager', 'Sulafat'
|
|
20
|
+
];
|
|
21
|
+
function showHelp() {
|
|
22
|
+
const theme = t();
|
|
23
|
+
print(header('gemini speak', 'Text-to-speech'));
|
|
24
|
+
print('');
|
|
25
|
+
print(theme.colors.primary('Usage:'));
|
|
26
|
+
print(` gemini speak ${theme.colors.muted('"your text"')} [options]`);
|
|
27
|
+
print('');
|
|
28
|
+
print(theme.colors.primary('Options:'));
|
|
29
|
+
print(` ${theme.colors.highlight('--voice, -v')} ${theme.colors.muted('Voice to use (default: Kore)')}`);
|
|
30
|
+
print(` ${theme.colors.highlight('--output, -o')} ${theme.colors.muted('Output file path')}`);
|
|
31
|
+
print(` ${theme.colors.highlight('--style, -s')} ${theme.colors.muted('Speaking style (e.g., "cheerfully", "sadly")')}`);
|
|
32
|
+
print(` ${theme.colors.highlight('--list-voices')} ${theme.colors.muted('List all available voices')}`);
|
|
33
|
+
print(` ${theme.colors.highlight('--help, -h')} ${theme.colors.muted('Show this help')}`);
|
|
34
|
+
print('');
|
|
35
|
+
print(theme.colors.primary('Popular Voices:'));
|
|
36
|
+
print(theme.colors.muted(' Kore - Firm, authoritative'));
|
|
37
|
+
print(theme.colors.muted(' Puck - Upbeat, energetic'));
|
|
38
|
+
print(theme.colors.muted(' Zephyr - Bright, clear'));
|
|
39
|
+
print(theme.colors.muted(' Charon - Informative, calm'));
|
|
40
|
+
print(theme.colors.muted(' Aoede - Breezy, light'));
|
|
41
|
+
print('');
|
|
42
|
+
print(theme.colors.primary('Examples:'));
|
|
43
|
+
print(theme.colors.muted(' gemini speak "Hello world!" --voice Puck'));
|
|
44
|
+
print(theme.colors.muted(' gemini speak "Important message" -v Kore -o message.mp3'));
|
|
45
|
+
print(theme.colors.muted(' gemini speak "Exciting news!" --style "enthusiastically"'));
|
|
46
|
+
print(theme.colors.muted(' gemini speak --list-voices'));
|
|
47
|
+
}
|
|
48
|
+
function listVoices() {
|
|
49
|
+
const theme = t();
|
|
50
|
+
print(header('Available Voices', '30 TTS voices'));
|
|
51
|
+
print('');
|
|
52
|
+
// Group voices into columns
|
|
53
|
+
const cols = 5;
|
|
54
|
+
const rows = Math.ceil(VOICES.length / cols);
|
|
55
|
+
for (let row = 0; row < rows; row++) {
|
|
56
|
+
let line = ' ';
|
|
57
|
+
for (let col = 0; col < cols; col++) {
|
|
58
|
+
const idx = row + col * rows;
|
|
59
|
+
if (idx < VOICES.length) {
|
|
60
|
+
const voice = VOICES[idx];
|
|
61
|
+
line += voice.padEnd(16);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
print(theme.colors.muted(line));
|
|
65
|
+
}
|
|
66
|
+
print('');
|
|
67
|
+
}
|
|
68
|
+
export async function speakCommand(argv) {
|
|
69
|
+
const { values, positionals } = parseArgs({
|
|
70
|
+
args: argv,
|
|
71
|
+
options: {
|
|
72
|
+
help: { type: 'boolean', short: 'h', default: false },
|
|
73
|
+
voice: { type: 'string', short: 'v', default: 'Kore' },
|
|
74
|
+
output: { type: 'string', short: 'o' },
|
|
75
|
+
style: { type: 'string', short: 's' },
|
|
76
|
+
'list-voices': { type: 'boolean', default: false },
|
|
77
|
+
},
|
|
78
|
+
allowPositionals: true,
|
|
79
|
+
});
|
|
80
|
+
if (values.help) {
|
|
81
|
+
showHelp();
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (values['list-voices']) {
|
|
85
|
+
listVoices();
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
// Get text from positional args
|
|
89
|
+
const text = positionals.join(' ');
|
|
90
|
+
if (!text) {
|
|
91
|
+
printError('No text provided');
|
|
92
|
+
printMuted('Usage: gemini speak "your text"');
|
|
93
|
+
process.exit(1);
|
|
94
|
+
}
|
|
95
|
+
const theme = t();
|
|
96
|
+
const s = spinner();
|
|
97
|
+
const voice = values.voice;
|
|
98
|
+
const style = values.style;
|
|
99
|
+
// Validate voice
|
|
100
|
+
if (!VOICES.includes(voice)) {
|
|
101
|
+
printError(`Unknown voice: ${voice}`);
|
|
102
|
+
printMuted(`Run 'gemini speak --list-voices' to see available voices`);
|
|
103
|
+
process.exit(1);
|
|
104
|
+
}
|
|
105
|
+
try {
|
|
106
|
+
// Suppress logger output for CLI
|
|
107
|
+
setupLogger('quiet');
|
|
108
|
+
const apiKey = process.env.GEMINI_API_KEY;
|
|
109
|
+
if (!apiKey) {
|
|
110
|
+
throw new Error('GEMINI_API_KEY not set');
|
|
111
|
+
}
|
|
112
|
+
s.start(`Generating speech with ${voice}...`);
|
|
113
|
+
const genAI = new GoogleGenAI({ apiKey });
|
|
114
|
+
// Build prompt with optional style
|
|
115
|
+
let prompt = text;
|
|
116
|
+
if (style) {
|
|
117
|
+
prompt = `Say this ${style}: "${text}"`;
|
|
118
|
+
}
|
|
119
|
+
// Generate speech
|
|
120
|
+
const response = await genAI.models.generateContent({
|
|
121
|
+
model: 'gemini-2.5-flash-preview-tts',
|
|
122
|
+
contents: prompt,
|
|
123
|
+
config: {
|
|
124
|
+
responseModalities: [Modality.AUDIO],
|
|
125
|
+
speechConfig: {
|
|
126
|
+
voiceConfig: {
|
|
127
|
+
prebuiltVoiceConfig: {
|
|
128
|
+
voiceName: voice,
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
// Extract audio data
|
|
135
|
+
const candidate = response.candidates?.[0];
|
|
136
|
+
if (!candidate?.content?.parts?.[0]) {
|
|
137
|
+
throw new Error('No audio generated');
|
|
138
|
+
}
|
|
139
|
+
const part = candidate.content.parts[0];
|
|
140
|
+
if (!('inlineData' in part) || !part.inlineData?.data) {
|
|
141
|
+
throw new Error('No audio data in response');
|
|
142
|
+
}
|
|
143
|
+
// Determine output path
|
|
144
|
+
const outputPath = values.output ||
|
|
145
|
+
join(getOutputDir(), `speech-${Date.now()}.mp3`);
|
|
146
|
+
// Save audio file
|
|
147
|
+
const audioBuffer = Buffer.from(part.inlineData.data, 'base64');
|
|
148
|
+
await Bun.write(outputPath, audioBuffer);
|
|
149
|
+
s.success('Speech generated!');
|
|
150
|
+
print('');
|
|
151
|
+
// Show info
|
|
152
|
+
const infoLines = [
|
|
153
|
+
`${theme.colors.primary('Voice:')} ${voice}`,
|
|
154
|
+
`${theme.colors.primary('Text:')} ${text.substring(0, 50)}${text.length > 50 ? '...' : ''}`,
|
|
155
|
+
style ? `${theme.colors.primary('Style:')} ${style}` : null,
|
|
156
|
+
`${theme.colors.primary('File:')} ${outputPath}`,
|
|
157
|
+
`${theme.colors.primary('Size:')} ${(audioBuffer.length / 1024).toFixed(1)} KB`,
|
|
158
|
+
].filter(Boolean);
|
|
159
|
+
print(box(infoLines, { title: 'Text-to-Speech' }));
|
|
160
|
+
print('');
|
|
161
|
+
printSuccess(`Audio saved to: ${outputPath}`);
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
s.error('Speech generation failed');
|
|
165
|
+
printError(error instanceof Error ? error.message : String(error));
|
|
166
|
+
process.exit(1);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tokens Command
|
|
3
|
+
*
|
|
4
|
+
* Count tokens in text or files.
|
|
5
|
+
* gemini tokens "your text"
|
|
6
|
+
* gemini tokens @file.txt
|
|
7
|
+
*/
|
|
8
|
+
import { parseArgs } from 'node:util';
|
|
9
|
+
import { initGeminiClient, countTokens } from '../../gemini-client.js';
|
|
10
|
+
import { setupLogger } from '../../utils/logger.js';
|
|
11
|
+
import { spinner, print, printError, printMuted, t, header, box } from '../ui/index.js';
|
|
12
|
+
function showHelp() {
|
|
13
|
+
const theme = t();
|
|
14
|
+
print(header('gemini tokens', 'Count tokens in text or files'));
|
|
15
|
+
print('');
|
|
16
|
+
print(theme.colors.primary('Usage:'));
|
|
17
|
+
print(` gemini tokens ${theme.colors.muted('"your text"')}`);
|
|
18
|
+
print(` gemini tokens ${theme.colors.muted('@file.txt')}`);
|
|
19
|
+
print('');
|
|
20
|
+
print(theme.colors.primary('Options:'));
|
|
21
|
+
print(` ${theme.colors.highlight('--model, -m')} ${theme.colors.muted('Model for tokenization: pro, flash (default: flash)')}`);
|
|
22
|
+
print(` ${theme.colors.highlight('--help, -h')} ${theme.colors.muted('Show this help')}`);
|
|
23
|
+
print('');
|
|
24
|
+
print(theme.colors.primary('Examples:'));
|
|
25
|
+
print(theme.colors.muted(' gemini tokens "Hello, world!"'));
|
|
26
|
+
print(theme.colors.muted(' gemini tokens @README.md'));
|
|
27
|
+
print(theme.colors.muted(' gemini tokens @src/index.ts --model pro'));
|
|
28
|
+
}
|
|
29
|
+
export async function tokensCommand(argv) {
|
|
30
|
+
const { values, positionals } = parseArgs({
|
|
31
|
+
args: argv,
|
|
32
|
+
options: {
|
|
33
|
+
help: { type: 'boolean', short: 'h', default: false },
|
|
34
|
+
model: { type: 'string', short: 'm', default: 'flash' },
|
|
35
|
+
},
|
|
36
|
+
allowPositionals: true,
|
|
37
|
+
});
|
|
38
|
+
if (values.help) {
|
|
39
|
+
showHelp();
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
// Get input from positional args
|
|
43
|
+
const input = positionals.join(' ');
|
|
44
|
+
if (!input) {
|
|
45
|
+
printError('No text or file provided');
|
|
46
|
+
printMuted('Usage: gemini tokens "your text" or gemini tokens @file.txt');
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
const theme = t();
|
|
50
|
+
const s = spinner();
|
|
51
|
+
try {
|
|
52
|
+
// Suppress logger output for CLI
|
|
53
|
+
setupLogger('quiet');
|
|
54
|
+
// Check if input is a file reference
|
|
55
|
+
let text;
|
|
56
|
+
let source;
|
|
57
|
+
if (input.startsWith('@')) {
|
|
58
|
+
const filePath = input.slice(1);
|
|
59
|
+
s.start(`Reading ${filePath}...`);
|
|
60
|
+
const file = Bun.file(filePath);
|
|
61
|
+
const exists = await file.exists();
|
|
62
|
+
if (!exists) {
|
|
63
|
+
s.error(`File not found: ${filePath}`);
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
text = await file.text();
|
|
67
|
+
source = filePath;
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
text = input;
|
|
71
|
+
source = 'text input';
|
|
72
|
+
}
|
|
73
|
+
// Initialize Gemini client
|
|
74
|
+
s.update('Connecting to Gemini...');
|
|
75
|
+
await initGeminiClient();
|
|
76
|
+
// Count tokens
|
|
77
|
+
s.update('Counting tokens...');
|
|
78
|
+
const model = values.model;
|
|
79
|
+
const result = await countTokens(text, model);
|
|
80
|
+
s.success('Token count complete');
|
|
81
|
+
print('');
|
|
82
|
+
// Display results in a nice box
|
|
83
|
+
const lines = [
|
|
84
|
+
`${theme.colors.primary('Source:')} ${source}`,
|
|
85
|
+
`${theme.colors.primary('Model:')} ${model}`,
|
|
86
|
+
`${theme.colors.primary('Tokens:')} ${theme.colors.highlight(result.totalTokens.toLocaleString())}`,
|
|
87
|
+
];
|
|
88
|
+
// Add character count for context
|
|
89
|
+
lines.push(`${theme.colors.muted('Characters:')} ${text.length.toLocaleString()}`);
|
|
90
|
+
// Estimate cost (rough estimates based on typical pricing)
|
|
91
|
+
// Input: ~$0.075 per 1M tokens for Flash, ~$1.25 per 1M tokens for Pro
|
|
92
|
+
const costPer1M = model === 'flash' ? 0.075 : 1.25;
|
|
93
|
+
const estimatedCost = (result.totalTokens / 1_000_000) * costPer1M;
|
|
94
|
+
if (estimatedCost > 0.0001) {
|
|
95
|
+
lines.push(`${theme.colors.muted('Est. cost:')} $${estimatedCost.toFixed(6)}`);
|
|
96
|
+
}
|
|
97
|
+
print(box(lines, { title: 'Token Count' }));
|
|
98
|
+
print('');
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
s.error('Token count failed');
|
|
102
|
+
printError(error instanceof Error ? error.message : String(error));
|
|
103
|
+
process.exit(1);
|
|
104
|
+
}
|
|
105
|
+
}
|