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 +11 -2
- package/package.json +4 -4
- package/src/cli/slash-commands.js +2 -1
- package/src/config.js +31 -15
- package/src/index.js +8 -6
- package/src/providers/openai-compatible.js +24 -7
- package/src/ui/terminal.js +2 -2
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
|
|
47
|
-
export DRACO_MODEL="
|
|
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
|
|
4
|
-
"description": "Draco — a
|
|
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
|
-
"
|
|
19
|
-
"
|
|
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 *;
|
|
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
|
|
16
|
-
const
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
'
|
|
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(
|
|
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)
|
|
124
|
-
warnings.push(
|
|
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.
|
|
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:
|
|
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
|
-
'
|
|
37
|
-
'
|
|
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.
|
|
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(
|
|
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
|
|
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 = '
|
|
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(
|
|
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(
|
|
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(
|
|
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:
|
|
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
|
|
276
|
-
// 429 = rate-limited but authenticated
|
|
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);
|
package/src/ui/terminal.js
CHANGED
|
@@ -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:
|
|
13
|
-
process.stdout.write(
|
|
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`);
|