gemqq 0.5.8

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 (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +74 -0
  3. package/index.js +304 -0
  4. package/package.json +58 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Charles McBrian
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,74 @@
1
+ # gemqq
2
+
3
+ [![npm version](https://img.shields.io/npm/v/gemqq.svg)](https://www.npmjs.com/package/gemqq)
4
+ [![Build Status](https://github.com/quadrigasoftware/gemqq/actions/workflows/test.yml/badge.svg)](https://github.com/quadrigasoftware/gemqq/actions)
5
+
6
+ Gemini Quick Question is a one-shot wrapper for gemini-cli featuring interactive editor support, markdown rendering in terminal, and real-time token usage statistics. gemqq does not have memory nor context, it simply answers your 'quick questions' quickly.
7
+
8
+ ## Installation
9
+
10
+ ```bash
11
+ npm install -g gemqq
12
+ ```
13
+
14
+ ## Usage
15
+
16
+ ```bash
17
+ gemqq [OPTIONS] [PROMPT...]
18
+ ```
19
+
20
+ ### Options
21
+
22
+ - `-e, --edit`: Open prompt in default editor (`EDITOR` or `VISUAL`).
23
+ - `-r, --raw`: Output raw text (disable markdown rendering via `glow`).
24
+ - `-c, --copy`: Copy response to system clipboard.
25
+ - `-m, --model NAME`: Specify a custom model.
26
+ - `--style NAME`: Specify a `glow` style (e.g., `auto`, `dark`, `light`). Default is `auto`.
27
+ - `--no-stats`: Suppress token usage statistics and timing info.
28
+ - `--project`: Enable full project workspace context. By default, `gemqq` isolates file context to zero by running in a temporary directory. Use this flag when you want Gemini to see your codebase.
29
+ - `--pro`: Use `gemini-3-pro-preview` model.
30
+ - `--flash`: Use `gemini-3-flash-preview` model (Default).
31
+ - `--debug`: Enable debug mode.
32
+ - `-h, --help`: Show help message.
33
+
34
+ ### Token Statistics
35
+
36
+ By default, `gemqq` displays token usage and execution time:
37
+ `(Done in 5.9s, 4551 tokens (4502i / 2o))`
38
+
39
+ - **i**: Input Tokens (Prompt)
40
+ - **o**: Output Tokens (Candidates)
41
+
42
+ Use `--no-stats` to hide this information.
43
+
44
+ ### Context Isolation
45
+
46
+ By default, `gemqq` executes the Gemini CLI in a temporary directory to isolate your file context to zero. This prevents Gemini from automatically snapshotting your current directory tree, which significantly reduces token usage and improves privacy for general queries.
47
+
48
+ If you need Gemini to analyze your codebase or reference local files, use the `--project` flag to run in your current working directory with full context enabled.
49
+
50
+ ### Examples
51
+
52
+ ```terminaloutput
53
+ gemqq difference between gemini 3.0, 3.1. make a table
54
+ cat file.txt | gemqq summarize this
55
+ gemqq -e --pro
56
+ gemqq C++ operator precedence and keywords
57
+ gemqq how do I update git submodules in parent
58
+ gemqq --pro "Analyze the subtext of Roy's final speech in Blade Runner"
59
+ ```
60
+
61
+ > **Note:** Prompts do not require quotes unless they contain special shell characters like `?`, `*`, `&`, `;`, or `|`. If your prompt includes these, you should either quote it or escape the characters.
62
+
63
+ ## Testing
64
+
65
+ The project includes automated integration and unit tests using [Vitest](https://vitest.dev/).
66
+
67
+ ```bash
68
+ npm test
69
+ ```
70
+
71
+ ## Dependencies
72
+
73
+ - [Gemini CLI](https://github.com/google/gemini-cli)
74
+ - [Glow](https://github.com/charmbracelet/glow) (for markdown rendering)
package/index.js ADDED
@@ -0,0 +1,304 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Copyright (c) 2026 Charles McBrian
5
+ * Licensed under the MIT License. See LICENSE file in the project root for full license information.
6
+ */
7
+
8
+ import { program } from 'commander';
9
+ import ora from 'ora';
10
+ import { execa } from 'execa';
11
+ import clipboardy from 'clipboardy';
12
+ import chalk from 'chalk';
13
+ import fs from 'fs';
14
+ import path from 'path';
15
+ import { tmpdir } from 'os';
16
+ import { spawnSync, spawn } from 'child_process';
17
+
18
+ const __filename = new URL(import.meta.url).pathname;
19
+ const __dirname = path.dirname(__filename);
20
+
21
+ // Read version from package.json
22
+ const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, 'package.json'), 'utf8'));
23
+
24
+ // Helper to check command existence
25
+ function commandExists(cmd) {
26
+ try {
27
+ spawnSync(cmd, ['--version'], { stdio: 'ignore' });
28
+ return true;
29
+ } catch (e) {
30
+ return false;
31
+ }
32
+ }
33
+
34
+ const hasGlow = commandExists('glow');
35
+ const hasGemini = commandExists('gemini');
36
+
37
+ program
38
+ .name('gemqq')
39
+ .description("Gemini Quick Question is a one-shot wrapper for gemini-cli featuring interactive editor support, markdown rendering in terminal, and real-time token usage statistics. gemqq does not have memory nor context, it simply answers your 'quick questions' quickly.")
40
+ .version(pkg.version)
41
+ .argument('[prompt...]', 'Prompt for the model')
42
+ .option('-e, --edit', 'Open prompt in default editor')
43
+ .option('-r, --raw', 'Output raw text (disable markdown rendering)')
44
+ .option('-c, --copy', 'Copy response to system clipboard')
45
+ .option('-m, --model <name>', 'Specify a custom model')
46
+ .option('--style <name>', 'Specify a glow style (e.g., auto, dark, light)', 'auto')
47
+ .option('--no-stats', 'Do not show token usage statistics')
48
+ .option('--project', 'Enable full project workspace context (may send more data)')
49
+ .option('--pro', 'Use gemini-3-pro-preview model')
50
+ .option('--flash', 'Use gemini-3-flash-preview model (Default)')
51
+ .option('--debug', 'Enable debug mode')
52
+ .addHelpText('after', `
53
+ Examples:
54
+ gemqq difference between gemini 3.0, 3.1. make a table
55
+ cat file.txt | gemqq summarize this
56
+ gemqq -e --pro
57
+ gemqq C++ operator precedence and keywords
58
+ gemqq how do I update git submodules in parent
59
+ gemqq --pro "Analyze the subtext of Roy's final speech in Blade Runner"
60
+ `)
61
+ .action(async (promptParts, options) => {
62
+ let currentTempDir = null;
63
+ let currentSpinner = null;
64
+ const isTest = process.env.NODE_ENV === 'test';
65
+
66
+ const cleanup = () => {
67
+ if (currentSpinner && !isTest) {
68
+ currentSpinner.stop();
69
+ }
70
+ if (currentTempDir && fs.existsSync(currentTempDir)) {
71
+ try {
72
+ fs.rmSync(currentTempDir, { recursive: true, force: true });
73
+ } catch (e) {}
74
+ }
75
+ };
76
+
77
+ const signalHandler = () => {
78
+ cleanup();
79
+ process.exit(130);
80
+ };
81
+
82
+ process.on('SIGINT', signalHandler);
83
+ process.on('SIGTERM', signalHandler);
84
+
85
+ if (!hasGemini) {
86
+ cleanup();
87
+ process.off('SIGINT', signalHandler);
88
+ process.off('SIGTERM', signalHandler);
89
+ console.error(chalk.bold('Error:') + " 'gemini' CLI not found.");
90
+ console.error("Please install it using npm:\n");
91
+ console.error(chalk.bold(" npm install -g @google/gemini-cli\n"));
92
+ process.exit(1);
93
+ }
94
+
95
+ let initialPrompt = promptParts.join(' ');
96
+ let pipedInput = '';
97
+
98
+ if (!process.stdin.isTTY && (process.env.NODE_ENV !== 'test' || process.env.HAS_PIPED_INPUT)) {
99
+ // Read from stdin
100
+ const chunks = [];
101
+ try {
102
+ for await (const chunk of process.stdin) {
103
+ chunks.push(chunk);
104
+ }
105
+ pipedInput = Buffer.concat(chunks).toString();
106
+ } catch (e) {
107
+ if (options.debug) console.error(chalk.red('[DEBUG] Stdin read error:'), e);
108
+ }
109
+ }
110
+
111
+ if (!initialPrompt && !pipedInput && !options.edit) {
112
+ program.help();
113
+ return;
114
+ }
115
+
116
+ let fullPrompt = initialPrompt;
117
+ if (pipedInput) {
118
+ if (fullPrompt) {
119
+ fullPrompt = `${fullPrompt}
120
+
121
+ ---
122
+ ${pipedInput}`;
123
+ } else {
124
+ fullPrompt = pipedInput;
125
+ }
126
+ }
127
+
128
+ if (options.edit) {
129
+ const tmpFile = path.join(tmpdir(), `gemqq-prompt-${Date.now()}.txt`);
130
+ fs.writeFileSync(tmpFile, fullPrompt || '');
131
+
132
+ const editor = process.env.EDITOR || process.env.VISUAL || 'nano';
133
+ spawnSync(editor, [tmpFile], { stdio: 'inherit' });
134
+
135
+ const editedPrompt = fs.readFileSync(tmpFile, 'utf8').trim();
136
+ if (editedPrompt !== (fullPrompt || '').trim()) {
137
+ console.error(chalk.gray(`--- Updated Prompt ---
138
+ ${editedPrompt}
139
+ ----------------------
140
+ `));
141
+ }
142
+ fullPrompt = editedPrompt;
143
+ fs.unlinkSync(tmpFile);
144
+ } else {
145
+ if (fullPrompt) {
146
+ fullPrompt = `${fullPrompt}. Do not tell me what you are doing, only provide the answer.`;
147
+ }
148
+ }
149
+
150
+ if (!fullPrompt && !options.edit) {
151
+ program.help();
152
+ return;
153
+ }
154
+
155
+ const allowedTools = ['*'];
156
+
157
+ const model = options.model || (options.pro ? 'gemini-3-pro-preview' : 'gemini-3-flash-preview');
158
+
159
+ const args = ['-s', '--allowed-tools', JSON.stringify(allowedTools), '--model', model, '--output-format', 'json'];
160
+ if (options.debug) args.push('-d');
161
+ args.push(fullPrompt);
162
+
163
+ if (options.debug) {
164
+ console.error(chalk.bold('[DEBUG] Executing:') + ` gemini ${args.map(a => `'${a}'`).join(' ')}`);
165
+ }
166
+
167
+ currentSpinner = isTest ? { start: () => currentSpinner, stop: () => {} } : ora('Gemini is thinking...').start();
168
+ const startTime = Date.now();
169
+
170
+ let sysPromptFile = null;
171
+ if (!options.project) {
172
+ try {
173
+ currentTempDir = fs.mkdtempSync(path.join(tmpdir(), 'gemqq-run-'));
174
+ sysPromptFile = path.join(currentTempDir, `gemqq-sysprompt-${Date.now()}.md`);
175
+ fs.writeFileSync(sysPromptFile, 'You are a helpful AI assistant. Answer the user\'s prompt concisely. Do not tell me what you are doing. Do not use this directory or any local context. Do not upload any local context.');
176
+ } catch (e) {
177
+ if (options.debug) console.error(chalk.red('[DEBUG] Failed to create isolation directory:'), e);
178
+ }
179
+ }
180
+
181
+ try {
182
+ const execEnv = {
183
+ ...process.env,
184
+ FORCE_COLOR: 'true'
185
+ };
186
+
187
+ // Apply the system prompt override to bypass workspace snapshotting
188
+ if (sysPromptFile) {
189
+ execEnv.GEMINI_SYSTEM_MD = sysPromptFile;
190
+ }
191
+
192
+ const { stdout, stderr } = await execa('gemini', args, {
193
+ env: execEnv,
194
+ cwd: currentTempDir || process.cwd()
195
+ });
196
+
197
+ cleanup();
198
+
199
+ if (stderr) {
200
+ const filteredStderr = stderr
201
+ .split('\n')
202
+ .filter(line => !line.match(/Loaded cached credentials|Hook registry initialized|Error executing tool/))
203
+ .join('\n');
204
+ if (filteredStderr) console.error(filteredStderr);
205
+ }
206
+
207
+ let modelOutput = stdout;
208
+ let statsString = '';
209
+
210
+ try {
211
+ const result = JSON.parse(stdout);
212
+ modelOutput = result.response || '';
213
+
214
+ if (result.stats && result.stats.models) {
215
+ let promptTokens = 0;
216
+ let candidateTokens = 0;
217
+ let totalTokens = 0;
218
+ let cachedTokens = 0;
219
+
220
+ for (const modelKey in result.stats.models) {
221
+ const m = result.stats.models[modelKey];
222
+ if (m.tokens) {
223
+ promptTokens += m.tokens.prompt || 0;
224
+ candidateTokens += m.tokens.candidates || 0;
225
+ totalTokens += m.tokens.total || 0;
226
+ cachedTokens += m.tokens.cached || 0;
227
+ }
228
+ }
229
+
230
+ statsString = `, ${totalTokens} tokens (${promptTokens}i / ${candidateTokens}o`;
231
+ if (cachedTokens > 0) {
232
+ statsString += `, ${cachedTokens} cached`;
233
+ }
234
+ statsString += ')';
235
+ }
236
+ } catch (e) {
237
+ if (options.debug) {
238
+ console.error(chalk.red('[DEBUG] Failed to parse JSON output:'), e);
239
+ }
240
+ }
241
+
242
+ const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
243
+
244
+ let statusMsg = ` (Done in ${elapsed}s${statsString})`;
245
+ if (options.copy) {
246
+ try {
247
+ await clipboardy.write(modelOutput);
248
+ statusMsg = ` (Copied to clipboard, ${elapsed}s${statsString})`;
249
+ } catch (e) {
250
+ statusMsg = ` (Failed to copy to clipboard, ${elapsed}s${statsString})`;
251
+ }
252
+ }
253
+
254
+ if (options.stats) {
255
+ console.error(chalk.gray(statusMsg));
256
+ }
257
+
258
+ if (options.raw || !hasGlow) {
259
+ process.stdout.write(modelOutput);
260
+ } else {
261
+ // Render with glow
262
+ try {
263
+ await execa('glow', ['--style', options.style], {
264
+ input: modelOutput,
265
+ stdout: 'inherit',
266
+ stderr: 'inherit'
267
+ });
268
+ } catch (glowError) {
269
+ if (options.debug) {
270
+ console.error(chalk.red('[DEBUG] Glow failed:'), glowError);
271
+ }
272
+ process.stdout.write(modelOutput);
273
+ }
274
+ }
275
+ } catch (error) {
276
+ cleanup();
277
+
278
+ if (error.signal === 'SIGINT' || error.signal === 'SIGTERM') {
279
+ process.exit(130);
280
+ }
281
+
282
+ // Filter stderr similar to bash script
283
+ const filteredStderr = (error.stderr || '')
284
+ .split('\n')
285
+ .filter(line => !line.match(/Loaded cached credentials|Hook registry initialized|Error executing tool/))
286
+ .join('\n');
287
+
288
+ const stderrLower = filteredStderr.toLowerCase();
289
+ // Check for common authentication error keywords
290
+ if (stderrLower.includes('login') || stderrLower.includes('authenticate') || stderrLower.includes('credentials')) {
291
+ console.error(chalk.red('\nAuthentication Error:'));
292
+ console.error('It appears you are not logged in or your credentials have expired.');
293
+ console.error('Please run the following command to log in:');
294
+ console.error(chalk.bold('\n gemini login\n'));
295
+ process.exit(1);
296
+ }
297
+
298
+ console.error(chalk.red('\nError calling Gemini CLI:'));
299
+ console.error(filteredStderr || error.message);
300
+ process.exit(1);
301
+ }
302
+ });
303
+
304
+ program.parse();
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "gemqq",
3
+ "version": "0.5.8",
4
+ "description": "Gemini Quick Question is a one-shot CLI wrapper for Google's Gemini, featuring interactive editor support, markdown rendering, and real-time token usage statistics. gemqq does not have memory nor context, it simply answers your 'quick questions' and prompts.",
5
+ "main": "index.js",
6
+ "bin": {
7
+ "gemqq": "index.js"
8
+ },
9
+ "type": "module",
10
+ "files": [
11
+ "index.js",
12
+ "README.md",
13
+ "LICENSE"
14
+ ],
15
+ "publishConfig": {
16
+ "access": "public"
17
+ },
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/quadrigasoftware/gemqq.git"
21
+ },
22
+ "bugs": {
23
+ "url": "https://github.com/quadrigasoftware/gemqq/issues"
24
+ },
25
+ "homepage": "https://github.com/quadrigasoftware/gemqq#readme",
26
+ "engines": {
27
+ "node": ">=20.0.0"
28
+ },
29
+ "scripts": {
30
+ "test": "vitest run"
31
+ },
32
+ "keywords": [
33
+ "gemini",
34
+ "google-gemini",
35
+ "cli",
36
+ "ai",
37
+ "llm",
38
+ "wrapper",
39
+ "markdown",
40
+ "terminal",
41
+ "productivity",
42
+ "developer-tools",
43
+ "one-shot"
44
+ ],
45
+ "author": "Charles McBrian",
46
+ "license": "MIT",
47
+ "sideEffects": false,
48
+ "dependencies": {
49
+ "chalk": "^5.6.2",
50
+ "clipboardy": "^5.3.0",
51
+ "commander": "^14.0.3",
52
+ "execa": "^9.6.1",
53
+ "ora": "^9.3.0"
54
+ },
55
+ "devDependencies": {
56
+ "vitest": "^4.0.18"
57
+ }
58
+ }