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.
@@ -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.4';
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.4",
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 ANTHROPIC_API_KEY (from the user's shell or a real
20
- // Anthropic login) so it doesn't conflict with our auth-token, and so that
21
- // Claude Code's welcome screen doesn't show the user's real Anthropic org.
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
- ANTHROPIC_AUTH_TOKEN: PLACEHOLDER_TOKEN,
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
- const passthroughArgs = process.argv.slice(2).filter(a => !['doctor', 'upgrade'].includes(a));
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
- if (left > 100) {
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(` Обнулится в ~21:00 МСК`));
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(' (бесплатно, без логина)'));
@@ -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: 1500, label: 'Google Gemini 2.5 Flash (free tier)' },
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
- const FRIENDLY_429 = (provider, used) => ({
70
- type: 'error',
71
- error: {
72
- type: 'rate_limit_error',
73
- message:
74
- provider === 'gemini'
75
- ? `Закончились бесплатные запросы Google Gemini на сегодня (использовано ${used} из 1500).\n\n` +
76
- `Квота обнулится в ~10:00 PT (~21:00 МСК).\n\n` +
77
- `Что делать сейчас:\n` +
78
- ` • Подожди до завтра, и продолжи\n` +
79
- ` • Или подключи второй Google-аккаунт через krasavacode setup-gemini\n` +
80
- ` • Или временно вернись на Pollinations: удали ~/.krasavacode/gemini.env`
81
- : `Pollinations на минуту перегружен. Подожди ~30 секунд и нажми Enter ещё раз.\n` +
82
- `Или подключи Gemini для стабильности: krasavacode setup-gemini`,
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: try to replace body with a friendly message (non-streaming).
117
- // If body is streaming/SSE we still let it through — Claude Code shows it.
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 body = JSON.stringify(FRIENDLY_429(provider, used));
122
- const headers = { ...upRes.headers, 'content-type': 'application/json' };
123
- delete headers['content-length'];
124
- delete headers['content-encoding'];
125
- res.writeHead(429, headers);
126
- upRes.resume(); // drain the original
127
- res.end(body);
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