draco-cli 3.0.3 → 3.1.0

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
@@ -43,11 +43,20 @@ draco --model=gpt-4 "refactor this function"
43
43
 
44
44
  **Environment Variables:**
45
45
  ```bash
46
- export OPENCODE_ZEN_KEY="your-api-key"
47
- export DRACO_MODEL="nemotron-3-ultra-free"
46
+ export GEMINI_API_KEY="your-google-ai-studio-key" # https://aistudio.google.com/apikey
47
+ export DRACO_MODEL="gemini-3.8-flash" # default
48
48
  export DRACO_APPROVALS="suggest" # suggest | auto-edit | full-auto
49
49
  ```
50
50
 
51
+ **Models (Gemini free tier):**
52
+
53
+ | Model | Best for |
54
+ |-------|----------|
55
+ | `gemini-3.8-flash` | **Default.** Fast, balanced, multimodal |
56
+ | `gemini-3.1-pro-preview` | Complex reasoning & coding |
57
+ | `gemini-3.5-flash-lite` | High-frequency lightweight tasks |
58
+ | `gemini-2.5-flash` / `gemini-2.5-pro` | Previous-generation free tier |
59
+
51
60
  ## Project Structure
52
61
 
53
62
  ```
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "draco-cli",
3
- "version": "3.0.3",
4
- "description": "Draco — a zero-login, terminal-first AI coding agent for the OpenCode Zen API.",
3
+ "version": "3.1.0",
4
+ "description": "Draco — a terminal-first AI coding agent powered by the Gemini API (Google AI Studio).",
5
5
  "license": "MIT",
6
6
  "author": "Anuj",
7
7
  "bin": {
@@ -15,8 +15,8 @@
15
15
  "ai",
16
16
  "cli",
17
17
  "coding-agent",
18
- "opencode",
19
- "zen",
18
+ "gemini",
19
+ "google-ai-studio",
20
20
  "terminal",
21
21
  "agent"
22
22
  ],
@@ -72,7 +72,7 @@ async function handle(line, rt) {
72
72
  process.stdout.write(`${marker}${id}\n`);
73
73
  }
74
74
  process.stdout.write(
75
- `${ui.c.gray}(active marked with *; some accounts use an "opencode/" prefix — switch with /model opencode/<id>)${ui.c.reset}\n`
75
+ `${ui.c.gray}(active marked with *; switch with /model <id>)${ui.c.reset}\n`
76
76
  );
77
77
  } catch (e) {
78
78
  ui.error(
@@ -233,6 +233,7 @@ async function handle(line, rt) {
233
233
  const mcpCount = rt.mcp ? rt.mcp.listTools().length : 0;
234
234
  const lines = [
235
235
  `version: ${cfg.version}`,
236
+ `provider: ${cfg.provider || 'Google AI Studio'}`,
236
237
  `endpoint: ${cfg.apiBase}`,
237
238
  `model: ${rt.agent.activeModel}`,
238
239
  `approvals: ${cfg.approvals}`,
package/src/config.js CHANGED
@@ -12,15 +12,23 @@ try {
12
12
  // keep fallback version
13
13
  }
14
14
 
15
- const DEFAULT_API_BASE = 'https://opencode.ai';
16
- const DEFAULT_MODEL = 'nemotron-3-ultra-free';
17
- const DEFAULT_FALLBACK_CHAIN = [
18
- 'nemotron-3-ultra-free',
19
- 'deepseek-v4-flash-free',
20
- 'big-pickle',
21
- 'mimo-v2.5-free',
15
+ const PROVIDER_NAME = 'Google AI Studio';
16
+ const DEFAULT_API_BASE = 'https://generativelanguage.googleapis.com/v1beta/openai';
17
+ const DEFAULT_API_PATH = '';
18
+ const DEFAULT_MODEL = 'gemini-3.8-flash';
19
+ const DEFAULT_FALLBACK_CHAIN = ['gemini-3.8-flash', 'gemini-3.1-pro-preview'];
20
+ const KNOWN_FREE_MODELS = [
21
+ 'gemini-3.8-flash',
22
+ 'gemini-3.7-flash',
23
+ 'gemini-3.6-flash',
24
+ 'gemini-3.5-flash',
25
+ 'gemini-3.5-flash-lite',
26
+ 'gemini-3.1-pro-preview',
27
+ 'gemini-3.1-flash-lite',
28
+ 'gemini-2.5-flash',
29
+ 'gemini-2.5-flash-lite',
30
+ 'gemini-2.5-pro',
22
31
  ];
23
- const KNOWN_FREE_MODELS = DEFAULT_FALLBACK_CHAIN.slice();
24
32
 
25
33
  const CHAT_TIMEOUT_MS = 60000;
26
34
  const MODELS_TIMEOUT_MS = 15000;
@@ -117,11 +125,15 @@ function validateConfig(config) {
117
125
  const warnings = [];
118
126
 
119
127
  if (!config.apiKey) {
120
- warnings.push('No API key configured. Set OPENCODE_ZEN_KEY or enter when prompted.');
128
+ warnings.push(
129
+ 'No API key configured. Get one at https://aistudio.google.com/apikey and set GEMINI_API_KEY.'
130
+ );
121
131
  }
122
-
123
- if (config.model && !KNOWN_FREE_MODELS.includes(config.model) && !config.model.includes('/')) {
124
- warnings.push(`Model "${config.model}" is not in the known free models list.`);
132
+
133
+ if (config.model && !KNOWN_FREE_MODELS.includes(config.model)) {
134
+ warnings.push(
135
+ `Model "${config.model}" is not on the Gemini free-tier list. It may be paid-only or invalid.`
136
+ );
125
137
  }
126
138
 
127
139
  if (config.fallbackChain && config.fallbackChain.length === 0) {
@@ -160,10 +172,10 @@ function resolveConfig(cliFlags) {
160
172
  ? persisted.fallbackChain
161
173
  : DEFAULT_FALLBACK_CHAIN.slice();
162
174
 
163
- let apiKey = env.OPENCODE_ZEN_KEY || '';
175
+ let apiKey = env.GEMINI_API_KEY || env.GOOGLE_API_KEY || '';
164
176
  let keySource = null;
165
177
  if (apiKey) {
166
- keySource = 'environment';
178
+ keySource = env.GEMINI_API_KEY ? 'environment' : 'environment (GOOGLE_API_KEY)';
167
179
  } else if (typeof persisted.apiKey === 'string' && persisted.apiKey.trim()) {
168
180
  apiKey = persisted.apiKey.trim();
169
181
  keySource = 'saved';
@@ -171,9 +183,11 @@ function resolveConfig(cliFlags) {
171
183
 
172
184
  const config = {
173
185
  version: VERSION,
186
+ provider: PROVIDER_NAME,
174
187
  apiKey,
175
188
  keySource,
176
- apiBase: (env.DRACO_API_BASE || DEFAULT_API_BASE).replace(/\/+$/, ''),
189
+ apiBase: DEFAULT_API_BASE,
190
+ apiPath: DEFAULT_API_PATH,
177
191
  model,
178
192
  yolo,
179
193
  approvals,
@@ -194,6 +208,8 @@ module.exports = {
194
208
  APPROVAL_MODES,
195
209
  DEFAULT_APPROVALS,
196
210
  DEFAULT_API_BASE,
211
+ DEFAULT_API_PATH,
212
+ PROVIDER_NAME,
197
213
  DEFAULT_MODEL,
198
214
  DEFAULT_FALLBACK_CHAIN,
199
215
  KNOWN_FREE_MODELS,
package/src/index.js CHANGED
@@ -33,11 +33,11 @@ function printUsage() {
33
33
  'One-shot mode: draco "your prompt here" — answer once, then exit',
34
34
  '',
35
35
  'Environment:',
36
- ' OPENCODE_ZEN_KEY Optional. Skips the first-run key prompt.',
37
- ' DRACO_MODEL Default model override (default: nemotron-3-ultra-free)',
36
+ ' GEMINI_API_KEY Google AI Studio key (skips the first-run prompt)',
37
+ ' GOOGLE_API_KEY Alias of GEMINI_API_KEY',
38
+ ' DRACO_MODEL Default model override (default: gemini-3.8-flash)',
38
39
  ' DRACO_APPROVALS suggest | auto-edit | full-auto',
39
40
  ' DRACO_YOLO "true" to auto-approve shell commands',
40
- ' DRACO_API_BASE Override the API endpoint',
41
41
  ' DRACO_SEARCH_PROVIDER Search backend when a key is set (default: brave)',
42
42
  ' DRACO_SEARCH_API_KEY Optional. Use Brave instead of built-in DuckDuckGo',
43
43
  ].join('\n') + '\n'
@@ -55,9 +55,11 @@ function installGlobalHandlers() {
55
55
 
56
56
  async function ensureApiKey(cfg, nextLine) {
57
57
  if (cfg.apiKey) return true;
58
- ui.info('No API key found. Get a free one at https://opencode.ai');
58
+ ui.info('No API key found. Create a free one at https://aistudio.google.com/apikey');
59
59
  for (let attempt = 0; attempt < 5; attempt++) {
60
- process.stdout.write(`${ui.c.yellow}?${ui.c.reset} Enter your OpenCode Zen API key: `);
60
+ process.stdout.write(
61
+ `${ui.c.yellow}?${ui.c.reset} Paste your Google AI Studio API key: `
62
+ );
61
63
  const line = await nextLine();
62
64
  if (line === null) return false;
63
65
  const key = line.trim();
@@ -205,7 +207,7 @@ async function main(argv) {
205
207
 
206
208
  const hasKey = await ensureApiKey(cfg, nextLine);
207
209
  if (!hasKey) {
208
- ui.error('No API key provided. Set OPENCODE_ZEN_KEY or enter it when prompted.');
210
+ ui.error('No API key provided. Set GEMINI_API_KEY or enter it when prompted.');
209
211
  process.exit(1);
210
212
  }
211
213
 
@@ -5,7 +5,15 @@ const {
5
5
  MODELS_TIMEOUT_MS,
6
6
  } = require('../config.js');
7
7
 
8
- const DEFAULT_PROBE_MODEL = 'nemotron-3-ultra-free';
8
+ const DEFAULT_PROBE_MODEL = 'gemini-3.8-flash';
9
+
10
+ function endpoint(cfg, suffix) {
11
+ return `${cfg.apiBase}${cfg.apiPath || ''}/${suffix}`;
12
+ }
13
+
14
+ function probeModel(cfg) {
15
+ return (cfg.model && cfg.model.trim()) || DEFAULT_PROBE_MODEL;
16
+ }
9
17
 
10
18
  class ApiError extends Error {
11
19
  constructor(status, message, bodyText) {
@@ -64,7 +72,7 @@ async function listModels(cfg) {
64
72
  try {
65
73
  let res;
66
74
  try {
67
- res = await fetch(`${cfg.apiBase}/zen/v1/models`, {
75
+ res = await fetch(endpoint(cfg, 'models'), {
68
76
  signal: controller.signal,
69
77
  headers: { Authorization: `Bearer ${cfg.apiKey}` },
70
78
  });
@@ -132,7 +140,7 @@ async function streamChatCompletion(opts) {
132
140
 
133
141
  let res;
134
142
  try {
135
- res = await fetch(`${cfg.apiBase}/zen/v1/chat/completions`, {
143
+ res = await fetch(endpoint(cfg, 'chat/completions'), {
136
144
  method: 'POST',
137
145
  signal: controller.signal,
138
146
  headers: {
@@ -253,7 +261,7 @@ async function probeApiKey(cfg, apiKey) {
253
261
  try {
254
262
  let res;
255
263
  try {
256
- res = await fetch(`${cfg.apiBase}/zen/v1/chat/completions`, {
264
+ res = await fetch(endpoint(cfg, 'chat/completions'), {
257
265
  method: 'POST',
258
266
  signal: controller.signal,
259
267
  headers: {
@@ -261,7 +269,7 @@ async function probeApiKey(cfg, apiKey) {
261
269
  'Content-Type': 'application/json',
262
270
  },
263
271
  body: JSON.stringify({
264
- model: DEFAULT_PROBE_MODEL,
272
+ model: probeModel(cfg),
265
273
  messages: [{ role: 'user', content: 'ping' }],
266
274
  max_tokens: 1,
267
275
  }),
@@ -272,13 +280,22 @@ async function probeApiKey(cfg, apiKey) {
272
280
  }
273
281
  return { verdict: 'unreachable', detail: e.message };
274
282
  }
275
- if (res.ok || res.status === 429 || res.status === 400) {
276
- // 429 = rate-limited but authenticated; 400 ModelError = auth fine, model busy
283
+ if (res.ok || res.status === 429) {
284
+ // 429 = rate-limited but authenticated
277
285
  return { verdict: 'ok', status: res.status };
278
286
  }
279
287
  if (res.status === 401 || res.status === 403) {
280
288
  return { verdict: 'invalid', status: res.status };
281
289
  }
290
+ if (res.status === 400) {
291
+ // Gemini reports a bad key as 400 INVALID_ARGUMENT, so inspect the body.
292
+ // A real model/param error still means the key itself was accepted.
293
+ const body = await safeBodyText(res);
294
+ if (/api key|api_key|API_KEY|unauthenticated|permission/i.test(body)) {
295
+ return { verdict: 'invalid', status: res.status };
296
+ }
297
+ return { verdict: 'ok', status: res.status };
298
+ }
282
299
  return { verdict: 'unknown', status: res.status };
283
300
  } finally {
284
301
  clearTimeout(timer);
@@ -9,8 +9,8 @@ const c = theme.c;
9
9
  function splash(cfg) {
10
10
  process.stdout.write(theme.welcomeBanner(cfg) + '\n');
11
11
  process.stdout.write('\n');
12
- process.stdout.write(`Endpoint: OpenCode Zen API Server\n`);
13
- process.stdout.write(`Model: Free Dynamic Routing Tier\n`);
12
+ process.stdout.write(`Endpoint: ${cfg.provider || 'Google AI Studio'} (Gemini API)\n`);
13
+ process.stdout.write(`${c.gray}Free-tier models via aistudio.google.com${c.reset}\n`);
14
14
  process.stdout.write('\n');
15
15
  process.stdout.write(`${c.bold}Workspace:${c.reset} ${cfg.workspaceRoot}\n`);
16
16
  process.stdout.write(`${c.gray}Active model: ${cfg.model}${c.reset}\n`);