gemini-coder 0.1.1 → 0.1.3

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
@@ -24,7 +24,25 @@ npm start
24
24
 
25
25
  ## Authentication
26
26
 
27
- This tool expects a local `gemini.json` service account file in the project root when run from source. Do not publish that file with the package.
27
+ Server-backed auth is supported. Run the Gemini Coder server on a trusted machine that has access to Google credentials, then point client machines at it with `GEMINI_CODER_SERVER_URL`.
28
+
29
+ Server example:
30
+
31
+ ```bash
32
+ export GEMINI_CODER_SERVER_TOKEN=your-shared-secret
33
+ export GOOGLE_APPLICATION_CREDENTIALS=/path/to/gemini.json
34
+ npm run serve
35
+ ```
36
+
37
+ Client example:
38
+
39
+ ```bash
40
+ export GEMINI_CODER_SERVER_URL=http://your-server:8787
41
+ export GEMINI_CODER_SERVER_TOKEN=your-shared-secret
42
+ gemini-coder chat
43
+ ```
44
+
45
+ For local source runs, the CLI still supports a `gemini.json` file in the workspace root as a fallback.
28
46
 
29
47
  ## Notes
30
48
 
package/dist/cli.js CHANGED
@@ -10,15 +10,15 @@ const sessionManager = new SessionManager();
10
10
  program
11
11
  .name('gemini')
12
12
  .description('Gemini Coder Pro CLI - Advanced AI Coding Agent')
13
- .version('0.1.1');
13
+ .version('0.1.3');
14
14
  program
15
15
  .command('chat', { isDefault: true })
16
16
  .description('Start an interactive chat session')
17
17
  .option('-p, --prompt <query>', 'Start with an initial prompt')
18
18
  .option('-c, --continue', 'Continue the most recent session')
19
- .option('-m, --model <name>', 'Specify the model to use', 'gemini-3.1-pro-preview')
19
+ .option('-m, --model <name>', 'Specify the model to use', 'gemini-3.5-flash')
20
20
  .action(async (options) => {
21
- printBootScreen('Gemini Coder Pro', 'v0.1.1');
21
+ printBootScreen('Gemini Coder Pro', 'v0.1.3');
22
22
  let session;
23
23
  if (options.continue) {
24
24
  session = await sessionManager.getLatestSession();
package/dist/core/ai.js CHANGED
@@ -1,33 +1,12 @@
1
- import { GoogleGenAI } from '@google/genai';
2
- import fs from 'fs';
3
- import path from 'path';
1
+ import { createGoogleClient } from './auth.js';
2
+ import { createRemoteClient } from './remote-client.js';
4
3
  import { fileURLToPath } from 'url';
5
- function findProjectRoot(startPath) {
6
- let currentPath = startPath;
7
- while (currentPath !== path.parse(currentPath).root) {
8
- if (fs.existsSync(path.join(currentPath, 'package.json'))) {
9
- return currentPath;
10
- }
11
- currentPath = path.dirname(currentPath);
12
- }
13
- throw new Error('Could not find project root containing package.json');
14
- }
4
+ import path from 'path';
15
5
  const __filename = fileURLToPath(import.meta.url);
16
6
  const __dirname = path.dirname(__filename);
17
- const projectRoot = findProjectRoot(__dirname);
18
- const credentialsPath = path.join(projectRoot, 'gemini.json');
19
- if (!fs.existsSync(credentialsPath)) {
20
- console.error(`Error: gemini.json not found at expected path: ${credentialsPath}`);
21
- process.exit(1);
22
- }
23
- const config = JSON.parse(fs.readFileSync(credentialsPath, 'utf8'));
24
- const location = config.location ?? process.env.GOOGLE_CLOUD_LOCATION ?? 'global';
25
- export const client = new GoogleGenAI({
26
- project: config.project_id,
27
- location,
28
- vertexai: true,
29
- googleAuthOptions: {
30
- keyFile: credentialsPath,
31
- scopes: ['https://www.googleapis.com/auth/cloud-platform'],
32
- }
33
- });
7
+ const projectRoot = path.resolve(__dirname, '..', '..');
8
+ const serverUrl = process.env.GEMINI_CODER_SERVER_URL;
9
+ const serverToken = process.env.GEMINI_CODER_SERVER_TOKEN;
10
+ export const client = serverUrl
11
+ ? createRemoteClient(serverUrl, serverToken)
12
+ : createGoogleClient(projectRoot);
@@ -0,0 +1,78 @@
1
+ import { GoogleGenAI } from '@google/genai';
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ function findFileUpward(startPath, fileName) {
5
+ let currentPath = startPath;
6
+ while (currentPath !== path.parse(currentPath).root) {
7
+ const candidatePath = path.join(currentPath, fileName);
8
+ if (fs.existsSync(candidatePath)) {
9
+ return candidatePath;
10
+ }
11
+ currentPath = path.dirname(currentPath);
12
+ }
13
+ return null;
14
+ }
15
+ function loadCredentialsFromFile(filePath) {
16
+ try {
17
+ const config = JSON.parse(fs.readFileSync(filePath, 'utf8'));
18
+ return {
19
+ project: config.project_id,
20
+ location: config.location,
21
+ };
22
+ }
23
+ catch {
24
+ return {};
25
+ }
26
+ }
27
+ export function resolveAuthConfig(baseDir) {
28
+ const envCredentialsPath = process.env.GOOGLE_APPLICATION_CREDENTIALS;
29
+ const envProject = process.env.GOOGLE_CLOUD_PROJECT ?? process.env.GCLOUD_PROJECT ?? process.env.GOOGLE_PROJECT_ID;
30
+ const envLocation = process.env.GOOGLE_CLOUD_LOCATION;
31
+ if (envCredentialsPath && fs.existsSync(envCredentialsPath)) {
32
+ const fromEnvFile = loadCredentialsFromFile(envCredentialsPath);
33
+ const project = envProject ?? fromEnvFile.project;
34
+ if (!project) {
35
+ throw new Error('GOOGLE_APPLICATION_CREDENTIALS is set, but the file does not contain project_id and no Google Cloud project env var was provided.');
36
+ }
37
+ return {
38
+ project,
39
+ location: envLocation ?? fromEnvFile.location ?? 'global',
40
+ keyFile: envCredentialsPath,
41
+ };
42
+ }
43
+ const localCredentialsPath = findFileUpward(baseDir, 'gemini.json');
44
+ if (localCredentialsPath) {
45
+ const fromLocalFile = loadCredentialsFromFile(localCredentialsPath);
46
+ const project = envProject ?? fromLocalFile.project;
47
+ if (!project) {
48
+ throw new Error(`Found ${localCredentialsPath}, but it does not contain project_id and no Google Cloud project env var was provided.`);
49
+ }
50
+ return {
51
+ project,
52
+ location: envLocation ?? fromLocalFile.location ?? 'global',
53
+ keyFile: localCredentialsPath,
54
+ };
55
+ }
56
+ if (envProject) {
57
+ return {
58
+ project: envProject,
59
+ location: envLocation ?? 'global',
60
+ };
61
+ }
62
+ throw new Error('No auth source found. Set GOOGLE_APPLICATION_CREDENTIALS, or place gemini.json in the workspace/server root, or configure GOOGLE_CLOUD_PROJECT for ADC.');
63
+ }
64
+ export function createGoogleClient(baseDir) {
65
+ const auth = resolveAuthConfig(baseDir);
66
+ const googleAuthOptions = auth.keyFile
67
+ ? {
68
+ keyFile: auth.keyFile,
69
+ scopes: ['https://www.googleapis.com/auth/cloud-platform'],
70
+ }
71
+ : undefined;
72
+ return new GoogleGenAI({
73
+ project: auth.project,
74
+ location: auth.location,
75
+ vertexai: true,
76
+ ...(googleAuthOptions ? { googleAuthOptions } : {}),
77
+ });
78
+ }
@@ -110,7 +110,7 @@ export class Orchestrator {
110
110
  mode = OrchestratorMode.NORMAL;
111
111
  model;
112
112
  appliedEdits = [];
113
- constructor(session, sessionManager, model = 'gemini-3.1-pro-preview') {
113
+ constructor(session, sessionManager, model = 'gemini-3.5-flash') {
114
114
  this.session = session;
115
115
  this.sessionManager = sessionManager;
116
116
  this.model = model;
@@ -174,7 +174,19 @@ export class Orchestrator {
174
174
  }
175
175
  async chat() {
176
176
  while (true) {
177
- const userInput = await rl.question(getPromptText(this.mode));
177
+ let userInput = '';
178
+ try {
179
+ userInput = await rl.question(getPromptText(this.mode));
180
+ }
181
+ catch (error) {
182
+ if (error?.code === 'ABORT_ERR' ||
183
+ error?.name === 'AbortError' ||
184
+ error?.code === 'ERR_USE_AFTER_CLOSE') {
185
+ console.log(chalk.yellow('\nSession interrupted. Exiting...'));
186
+ break;
187
+ }
188
+ throw error;
189
+ }
178
190
  if (userInput.trim() === '')
179
191
  continue;
180
192
  if (userInput.toLowerCase() === 'exit')
@@ -189,6 +201,7 @@ export class Orchestrator {
189
201
  this.session.updatedAt = new Date().toISOString();
190
202
  await this.sessionManager.saveSession(this.session);
191
203
  }
204
+ rl.close();
192
205
  }
193
206
  async handleSlashCommand(command) {
194
207
  const [cmd, ...args] = command.slice(1).split(' ');
@@ -260,12 +273,21 @@ export class Orchestrator {
260
273
  contents,
261
274
  config: {
262
275
  tools: [{ functionDeclarations }],
276
+ maxOutputTokens: 8192,
263
277
  }
264
278
  }));
265
279
  }
266
280
  catch (err) {
267
281
  spinner.fail(chalk.red('API Error'));
268
- console.error(chalk.red(`\n[API Error]: ${err.message || err}`));
282
+ const message = String(err?.message || err);
283
+ const isOauthDnsIssue = message.includes('oauth2.googleapis.com') ||
284
+ message.includes('ENOTFOUND') ||
285
+ message.includes('EAI_AGAIN') ||
286
+ message.includes('ECONNRESET');
287
+ console.error(chalk.red(`\n[API Error]: ${message}`));
288
+ if (isOauthDnsIssue) {
289
+ console.error(chalk.yellow('\n[Hint]: The CLI could not reach oauth2.googleapis.com to exchange the service-account token. Check network access, DNS, firewall, or proxy settings on this machine, then retry.'));
290
+ }
269
291
  return;
270
292
  }
271
293
  let responseParts = [];
@@ -283,7 +305,7 @@ export class Orchestrator {
283
305
  if (parts.length > 0) {
284
306
  responseParts.push(...parts);
285
307
  const textParts = parts
286
- .map(part => ('text' in part ? part.text : undefined))
308
+ .map((part) => ('text' in part ? part.text : undefined))
287
309
  .filter((text) => typeof text === 'string' && text.length > 0)
288
310
  .join('');
289
311
  if (textParts) {
@@ -0,0 +1,49 @@
1
+ async function* readNdjsonStream(response) {
2
+ if (!response.body) {
3
+ return;
4
+ }
5
+ const reader = response.body.getReader();
6
+ const decoder = new TextDecoder();
7
+ let buffer = '';
8
+ while (true) {
9
+ const { value, done } = await reader.read();
10
+ if (done)
11
+ break;
12
+ buffer += decoder.decode(value, { stream: true });
13
+ let lineBreakIndex = buffer.indexOf('\n');
14
+ while (lineBreakIndex !== -1) {
15
+ const line = buffer.slice(0, lineBreakIndex).trim();
16
+ buffer = buffer.slice(lineBreakIndex + 1);
17
+ if (line.length > 0) {
18
+ yield JSON.parse(line);
19
+ }
20
+ lineBreakIndex = buffer.indexOf('\n');
21
+ }
22
+ }
23
+ const remaining = buffer.trim();
24
+ if (remaining.length > 0) {
25
+ yield JSON.parse(remaining);
26
+ }
27
+ }
28
+ export function createRemoteClient(serverUrl, token) {
29
+ const normalizedServerUrl = serverUrl.replace(/\/+$/, '');
30
+ return {
31
+ models: {
32
+ generateContentStream: async (request) => {
33
+ const response = await fetch(`${normalizedServerUrl}/v1/generateContentStream`, {
34
+ method: 'POST',
35
+ headers: {
36
+ 'Content-Type': 'application/json',
37
+ ...(token ? { Authorization: `Bearer ${token}` } : {}),
38
+ },
39
+ body: JSON.stringify(request),
40
+ });
41
+ if (!response.ok) {
42
+ const errorText = await response.text();
43
+ throw new Error(`Remote auth server error (${response.status}): ${errorText}`);
44
+ }
45
+ return readNdjsonStream(response);
46
+ },
47
+ },
48
+ };
49
+ }
package/dist/server.js ADDED
@@ -0,0 +1,60 @@
1
+ import { createServer } from 'http';
2
+ import { URL } from 'url';
3
+ import { createGoogleClient } from './core/auth.js';
4
+ const serverPort = Number(process.env.GEMINI_CODER_SERVER_PORT ?? 8787);
5
+ const serverHost = process.env.GEMINI_CODER_SERVER_HOST ?? '127.0.0.1';
6
+ const requiredToken = process.env.GEMINI_CODER_SERVER_TOKEN?.trim();
7
+ const serverRoot = process.cwd();
8
+ const aiClient = createGoogleClient(serverRoot);
9
+ function sendJson(response, statusCode, payload) {
10
+ response.statusCode = statusCode;
11
+ response.setHeader('Content-Type', 'application/json; charset=utf-8');
12
+ response.end(JSON.stringify(payload));
13
+ }
14
+ function readRequestBody(request) {
15
+ return new Promise((resolve, reject) => {
16
+ const chunks = [];
17
+ request.on('data', (chunk) => chunks.push(Buffer.from(chunk)));
18
+ request.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
19
+ request.on('error', reject);
20
+ });
21
+ }
22
+ const server = createServer(async (request, response) => {
23
+ try {
24
+ const requestUrl = new URL(request.url ?? '/', `http://${request.headers.host ?? `${serverHost}:${serverPort}`}`);
25
+ if (requestUrl.pathname === '/health') {
26
+ sendJson(response, 200, { ok: true });
27
+ return;
28
+ }
29
+ if (requestUrl.pathname !== '/v1/generateContentStream' || request.method !== 'POST') {
30
+ sendJson(response, 404, { error: 'Not found' });
31
+ return;
32
+ }
33
+ if (requiredToken) {
34
+ const authHeader = request.headers.authorization ?? '';
35
+ if (authHeader !== `Bearer ${requiredToken}`) {
36
+ sendJson(response, 401, { error: 'Unauthorized' });
37
+ return;
38
+ }
39
+ }
40
+ const rawBody = await readRequestBody(request);
41
+ const payload = JSON.parse(rawBody || '{}');
42
+ response.statusCode = 200;
43
+ response.setHeader('Content-Type', 'application/x-ndjson; charset=utf-8');
44
+ response.setHeader('Cache-Control', 'no-cache');
45
+ response.setHeader('Connection', 'keep-alive');
46
+ const stream = await aiClient.models.generateContentStream(payload);
47
+ for await (const chunk of stream) {
48
+ response.write(`${JSON.stringify(chunk)}\n`);
49
+ }
50
+ response.end();
51
+ }
52
+ catch (error) {
53
+ sendJson(response, 500, {
54
+ error: error?.message || String(error),
55
+ });
56
+ }
57
+ });
58
+ server.listen(serverPort, serverHost, () => {
59
+ console.log(`Gemini Coder server listening on http://${serverHost}:${serverPort}`);
60
+ });
@@ -10,6 +10,28 @@ export const QUICK_ACTIONS = [
10
10
  function getBoxWidth() {
11
11
  return Math.max(60, Math.min(process.stdout.columns || 80, 96));
12
12
  }
13
+ function wrapContent(content, maxWidth) {
14
+ const rawLines = content.split(/\r?\n/);
15
+ const wrapped = [];
16
+ for (const rawLine of rawLines) {
17
+ const line = rawLine.replace(/\t/g, ' ');
18
+ if (line.length === 0) {
19
+ wrapped.push('');
20
+ continue;
21
+ }
22
+ let remaining = line;
23
+ while (remaining.length > maxWidth) {
24
+ let breakAt = remaining.lastIndexOf(' ', maxWidth);
25
+ if (breakAt <= 0) {
26
+ breakAt = maxWidth;
27
+ }
28
+ wrapped.push(remaining.slice(0, breakAt).trimEnd());
29
+ remaining = remaining.slice(breakAt).trimStart();
30
+ }
31
+ wrapped.push(remaining);
32
+ }
33
+ return wrapped;
34
+ }
13
35
  function boxLine(content, width) {
14
36
  const innerWidth = width - 2;
15
37
  const text = content.length > innerWidth ? content.slice(0, innerWidth) : content;
@@ -24,8 +46,12 @@ export function printBox(title, lines, accent = 'blue') {
24
46
  const left = '─'.repeat(Math.floor(titlePad / 2));
25
47
  const right = '─'.repeat(titlePad - left.length);
26
48
  console.log(borderColor(`┌${left}${titleText}${right}┐`));
49
+ const contentWidth = width - 3;
27
50
  for (const line of lines) {
28
- console.log(borderColor(boxLine(` ${line}`, width)));
51
+ const wrappedLines = wrapContent(line, Math.max(1, contentWidth));
52
+ for (const wrappedLine of wrappedLines) {
53
+ console.log(borderColor(boxLine(` ${wrappedLine}`, width)));
54
+ }
29
55
  }
30
56
  console.log(borderColor(`└${'─'.repeat(width - 2)}┘`));
31
57
  }
@@ -45,8 +71,11 @@ export function getPromptText(mode) {
45
71
  return chalk.bold(`${promptColor('>')} `);
46
72
  }
47
73
  export function printAssistantResponse(text) {
48
- const lines = text.split(/\r?\n/);
49
- printBox('Gemini', lines.length > 0 ? lines : [''], 'blue');
74
+ const width = Math.max(40, Math.min(process.stdout.columns || 80, 80));
75
+ const line = ''.repeat(width - 11);
76
+ console.log(chalk.blue.bold(`\n┌── Gemini ${line}`));
77
+ console.log(text);
78
+ console.log(chalk.blue.bold(`└──${'─'.repeat(width - 3)}`));
50
79
  }
51
80
  export function printModeChange(mode) {
52
81
  const label = mode === OrchestratorMode.PLAN ? chalk.magenta('Plan mode') : chalk.green('Normal mode');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gemini-coder",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "type": "module",
5
5
  "main": "dist/cli.js",
6
6
  "bin": {
@@ -13,7 +13,9 @@
13
13
  "scripts": {
14
14
  "build": "tsc -p tsconfig.json",
15
15
  "dev": "tsx src/cli.ts chat",
16
+ "dev:server": "tsx src/server.ts",
16
17
  "start": "node dist/cli.js chat",
18
+ "serve": "node dist/server.js",
17
19
  "typecheck": "tsc -p tsconfig.json --noEmit"
18
20
  },
19
21
  "dependencies": {