krasavacode 0.3.4 → 0.3.6
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/bin/krasavacode.js +1 -1
- package/package.json +1 -1
- package/src/launch.js +20 -8
- package/src/metrics-proxy.js +71 -26
package/bin/krasavacode.js
CHANGED
|
@@ -8,7 +8,7 @@ import { runDoctor } from '../src/doctor.js';
|
|
|
8
8
|
import { runSetupGemini } from '../src/setup-gemini.js';
|
|
9
9
|
|
|
10
10
|
// Hardcoded so it works inside Bun --compile (no FS access to package.json)
|
|
11
|
-
const VERSION = '0.3.
|
|
11
|
+
const VERSION = '0.3.6';
|
|
12
12
|
|
|
13
13
|
const cmd = process.argv[2];
|
|
14
14
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "krasavacode",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.6",
|
|
4
4
|
"description": "KRASAVACODE — однокнопочный бесплатный вайбкодинг для учеников. Claude Code на бесплатных провайдерах через локальный gateway. Сам ставит Node при необходимости.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/src/launch.js
CHANGED
|
@@ -16,18 +16,21 @@ export async function launchClaude(paths, hub /*, detection */) {
|
|
|
16
16
|
// Organization · API Usage Billing" header on the welcome screen.
|
|
17
17
|
await mkdir(CLAUDE_CONFIG_DIR, { recursive: true });
|
|
18
18
|
|
|
19
|
-
// Drop any pre-existing
|
|
20
|
-
//
|
|
21
|
-
//
|
|
19
|
+
// Drop any pre-existing Anthropic creds from the shell/Keychain so the
|
|
20
|
+
// welcome screen doesn't greet the student with the real Anthropic owner's
|
|
21
|
+
// name and "API Usage Billing".
|
|
22
22
|
const cleanEnv = { ...process.env };
|
|
23
23
|
delete cleanEnv.ANTHROPIC_API_KEY;
|
|
24
|
+
delete cleanEnv.ANTHROPIC_AUTH_TOKEN;
|
|
24
25
|
delete cleanEnv.ANTHROPIC_VERTEX_PROJECT_ID;
|
|
25
26
|
delete cleanEnv.ANTHROPIC_BEDROCK_BASE_URL;
|
|
26
27
|
|
|
27
28
|
const env = {
|
|
28
29
|
...cleanEnv,
|
|
29
30
|
ANTHROPIC_BASE_URL: hub.baseUrl,
|
|
30
|
-
|
|
31
|
+
// --bare mode requires ANTHROPIC_API_KEY (OAuth token is ignored).
|
|
32
|
+
// Our proxy doesn't actually validate this — any non-empty value works.
|
|
33
|
+
ANTHROPIC_API_KEY: PLACEHOLDER_TOKEN,
|
|
31
34
|
// Isolate config/credentials: own dir, separate from ~/.claude/
|
|
32
35
|
ANTHROPIC_CONFIG_DIR: CLAUDE_CONFIG_DIR,
|
|
33
36
|
DISABLE_AUTOUPDATER: '1',
|
|
@@ -38,7 +41,14 @@ export async function launchClaude(paths, hub /*, detection */) {
|
|
|
38
41
|
ANTHROPIC_MODEL: process.env.ANTHROPIC_MODEL || 'claude-sonnet-4-5',
|
|
39
42
|
};
|
|
40
43
|
|
|
41
|
-
|
|
44
|
+
// --bare mode: skips Keychain reads, plugin sync, auto-memory, attribution,
|
|
45
|
+
// CLAUDE.md auto-discovery — i.e. everything that would leak the user's
|
|
46
|
+
// real Anthropic identity into the welcome screen.
|
|
47
|
+
// Set KRASAVACODE_BARE=0 to disable for debugging.
|
|
48
|
+
const useBare = process.env.KRASAVACODE_BARE !== '0';
|
|
49
|
+
const passthroughArgs = process.argv.slice(2)
|
|
50
|
+
.filter(a => !['doctor', 'upgrade', 'setup-gemini', 'gemini'].includes(a));
|
|
51
|
+
if (useBare && !passthroughArgs.includes('--bare')) passthroughArgs.unshift('--bare');
|
|
42
52
|
|
|
43
53
|
const W = 64;
|
|
44
54
|
const line = (txt) => {
|
|
@@ -54,14 +64,16 @@ export async function launchClaude(paths, hub /*, detection */) {
|
|
|
54
64
|
if (geminiOn) {
|
|
55
65
|
console.log(line(' ✓ Модель: Google Gemini 2.5 Flash'));
|
|
56
66
|
const left = quota.perDay - quota.used;
|
|
57
|
-
|
|
67
|
+
const warn = Math.floor(quota.perDay / 5); // 20%
|
|
68
|
+
if (left > warn) {
|
|
58
69
|
console.log(line(` Сегодня осталось: ${left} из ${quota.perDay} запросов`));
|
|
59
70
|
} else if (left > 0) {
|
|
60
|
-
console.log(line(` ⚠️ Осталось ${left} из ${quota.perDay} — обнулится
|
|
71
|
+
console.log(line(` ⚠️ Осталось ${left} из ${quota.perDay} — обнулится в ~11:00 МСК`));
|
|
61
72
|
} else {
|
|
62
73
|
console.log(line(` ❌ Лимит на сегодня исчерпан (${quota.used} из ${quota.perDay})`));
|
|
63
|
-
console.log(line(` Обнулится в ~
|
|
74
|
+
console.log(line(` Обнулится в ~11:00 МСК. krasavacode setup-gemini`));
|
|
64
75
|
}
|
|
76
|
+
console.log(line(' 1 твой вопрос ≈ 3–10 запросов (Claude использует tools)'));
|
|
65
77
|
} else {
|
|
66
78
|
console.log(line(' · Модель: gpt-oss-20b через Pollinations'));
|
|
67
79
|
console.log(line(' (бесплатно, без логина)'));
|
package/src/metrics-proxy.js
CHANGED
|
@@ -8,8 +8,10 @@ import { isGeminiConfigured } from './setup-gemini.js';
|
|
|
8
8
|
const ROOT = join(homedir(), '.krasavacode');
|
|
9
9
|
const USAGE_FILE = join(ROOT, 'usage.json');
|
|
10
10
|
|
|
11
|
+
// Google free tier (2026): https://ai.google.dev/gemini-api/docs/rate-limits
|
|
12
|
+
// Gemini 2.5 Flash free: 10 RPM, 250k TPM, 250 RPD (request-per-day).
|
|
11
13
|
const FREE_QUOTA = {
|
|
12
|
-
gemini: { perDay:
|
|
14
|
+
gemini: { perDay: 250, rpm: 10, label: 'Google Gemini 2.5 Flash (free tier)' },
|
|
13
15
|
pollinations: { perDay: null, label: 'Pollinations (free)' },
|
|
14
16
|
};
|
|
15
17
|
|
|
@@ -66,22 +68,60 @@ function getFreePort() {
|
|
|
66
68
|
});
|
|
67
69
|
}
|
|
68
70
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
error: {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
71
|
+
function formatGeminiQuotaReason(upstreamBody) {
|
|
72
|
+
// Google's 429 body looks like:
|
|
73
|
+
// { "error": { "code": 429, "message": "...",
|
|
74
|
+
// "details": [{"@type": ".../QuotaFailure",
|
|
75
|
+
// "violations": [{"quotaMetric":"...generate_content_free_tier_requests",
|
|
76
|
+
// "quotaId":"...PerDay..."}]}] }}
|
|
77
|
+
try {
|
|
78
|
+
const parsed = JSON.parse(upstreamBody);
|
|
79
|
+
const violations = parsed.error?.details?.find(d => d['@type']?.includes('QuotaFailure'))?.violations || [];
|
|
80
|
+
if (violations.length === 0) return null;
|
|
81
|
+
|
|
82
|
+
const v = violations[0];
|
|
83
|
+
const id = v.quotaId || v.quotaMetric || '';
|
|
84
|
+
const isPerMinute = /PerMinute/i.test(id);
|
|
85
|
+
const isPerDay = /PerDay/i.test(id);
|
|
86
|
+
const isTokens = /Token|input_token|output_token/i.test(id);
|
|
87
|
+
|
|
88
|
+
if (isPerMinute) return 'Слишком много запросов в минуту (лимит — 10 запросов/мин). Подожди 30–60 секунд и продолжай.';
|
|
89
|
+
if (isPerDay && isTokens) return 'Закончился дневной лимит входных токенов Gemini (≈250k/день).';
|
|
90
|
+
if (isPerDay) return 'Закончилась дневная квота запросов к Gemini (≈250 запросов/день для 2.5-flash).';
|
|
91
|
+
return `Google Gemini ограничил запрос: ${id}`;
|
|
92
|
+
} catch { return null; }
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const FRIENDLY_429 = (provider, used, upstreamBody) => {
|
|
96
|
+
if (provider === 'gemini') {
|
|
97
|
+
const reason = formatGeminiQuotaReason(upstreamBody) ||
|
|
98
|
+
`Google ограничил запрос (использовано ${used} запросов сегодня через нас).`;
|
|
99
|
+
return {
|
|
100
|
+
type: 'error',
|
|
101
|
+
error: {
|
|
102
|
+
type: 'rate_limit_error',
|
|
103
|
+
message:
|
|
104
|
+
`${reason}\n\n` +
|
|
105
|
+
`Квоты обнуляются в полночь по тихоокеанскому времени (≈11:00 МСК).\n` +
|
|
106
|
+
`На один твой вопрос Claude Code делает 3–10 запросов (читает файлы, использует инструменты),\n` +
|
|
107
|
+
`поэтому реальный счёт у Google быстрее, чем в нашем счётчике.\n\n` +
|
|
108
|
+
`Что делать:\n` +
|
|
109
|
+
` • Подожди минуту (если упёрлись в RPM) или до завтра (если в дневной)\n` +
|
|
110
|
+
` • Подключи второй Google-аккаунт: krasavacode setup-gemini\n` +
|
|
111
|
+
` • Временно вернись на Pollinations (без квот): удали ~/.krasavacode/gemini.env`,
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
return {
|
|
116
|
+
type: 'error',
|
|
117
|
+
error: {
|
|
118
|
+
type: 'rate_limit_error',
|
|
119
|
+
message:
|
|
120
|
+
`Pollinations на минуту перегружен. Подожди ~30 секунд и попробуй ещё раз.\n` +
|
|
121
|
+
`Или подключи Gemini: krasavacode setup-gemini`,
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
};
|
|
85
125
|
|
|
86
126
|
/**
|
|
87
127
|
* Proxy: Claude Code → metrics-proxy (this) → ccr → upstream.
|
|
@@ -113,18 +153,23 @@ export async function startMetricsProxy(upstreamBaseUrl) {
|
|
|
113
153
|
bump().catch(e => debug && console.error('[metrics] bump fail', e));
|
|
114
154
|
}
|
|
115
155
|
|
|
116
|
-
// 429:
|
|
117
|
-
//
|
|
156
|
+
// 429: replace body with a friendly Russian message that includes
|
|
157
|
+
// a parsed reason from Google's QuotaFailure details.
|
|
118
158
|
if (upRes.statusCode === 429 && !/text\/event-stream/.test(upRes.headers['content-type'] || '')) {
|
|
119
159
|
const used = await getTodayUsage();
|
|
120
160
|
const provider = (await isGeminiConfigured()) ? 'gemini' : 'pollinations';
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
161
|
+
const chunks = [];
|
|
162
|
+
upRes.on('data', d => chunks.push(d));
|
|
163
|
+
upRes.on('end', () => {
|
|
164
|
+
const upstreamBody = Buffer.concat(chunks).toString('utf8');
|
|
165
|
+
if (debug) console.error('[metrics] 429 upstream body:', upstreamBody.slice(0, 500));
|
|
166
|
+
const friendly = JSON.stringify(FRIENDLY_429(provider, used, upstreamBody));
|
|
167
|
+
const headers = { ...upRes.headers, 'content-type': 'application/json' };
|
|
168
|
+
delete headers['content-length'];
|
|
169
|
+
delete headers['content-encoding'];
|
|
170
|
+
res.writeHead(429, headers);
|
|
171
|
+
res.end(friendly);
|
|
172
|
+
});
|
|
128
173
|
return;
|
|
129
174
|
}
|
|
130
175
|
|