smoothie-code 1.0.0 → 1.2.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.
@@ -1,318 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import { readFileSync, writeFileSync, existsSync } from 'fs';
4
- import { fileURLToPath } from 'url';
5
- import { dirname, join } from 'path';
6
- import { createInterface, Interface } from 'readline';
7
-
8
- const __dirname = dirname(fileURLToPath(import.meta.url));
9
- const PROJECT_ROOT = join(__dirname, '..');
10
-
11
- interface OpenRouterModel {
12
- id: string;
13
- name?: string;
14
- context_length?: number;
15
- }
16
-
17
- interface ModelEntry {
18
- id: string;
19
- label: string;
20
- }
21
-
22
- interface Config {
23
- openrouter_models: ModelEntry[];
24
- auto_blend?: boolean;
25
- }
26
-
27
- const FALLBACK_MODELS: ModelEntry[] = [
28
- { id: 'google/gemini-2.5-pro-preview', label: 'Gemini 2.5 Pro' },
29
- { id: 'deepseek/deepseek-r2', label: 'DeepSeek R2' },
30
- { id: 'x-ai/grok-3', label: 'Grok 3' },
31
- { id: 'qwen/qwen-2.5-coder-32b-instruct', label: 'Qwen 2.5 Coder' },
32
- { id: 'mistralai/codestral-2501', label: 'Codestral' },
33
- ];
34
-
35
- const CODING_PROVIDERS = [
36
- 'anthropic', 'google', 'openai', 'x-ai', 'deepseek',
37
- 'qwen', 'mistralai', 'meta-llama', 'cohere', 'amazon',
38
- ];
39
-
40
- function loadEnv(): void {
41
- try {
42
- const env = readFileSync(join(PROJECT_ROOT, '.env'), 'utf8');
43
- for (const line of env.split('\n')) {
44
- const [key, ...val] = line.split('=');
45
- if (key && val.length) process.env[key.trim()] = val.join('=').trim();
46
- }
47
- } catch {}
48
- }
49
-
50
- function formatLabel(model: OpenRouterModel): string {
51
- if (model.name) return model.name;
52
- const raw = model.id.includes('/') ? model.id.split('/').slice(1).join('/') : model.id;
53
- return raw.replace(/-/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
54
- }
55
-
56
- function loadConfig(configPath: string): Config {
57
- try {
58
- return JSON.parse(readFileSync(configPath, 'utf8')) as Config;
59
- } catch {
60
- return { openrouter_models: [] };
61
- }
62
- }
63
-
64
- function saveConfig(configPath: string, config: Config): void {
65
- writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n', 'utf8');
66
- }
67
-
68
- async function fetchModels(apiKey: string): Promise<OpenRouterModel[] | null> {
69
- try {
70
- const res = await fetch('https://openrouter.ai/api/v1/models?order=throughput', {
71
- headers: { Authorization: `Bearer ${apiKey}` },
72
- });
73
- if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`);
74
- const json = (await res.json()) as { data?: OpenRouterModel[] };
75
- return json.data || [];
76
- } catch (err) {
77
- const message = err instanceof Error ? err.message : String(err);
78
- console.warn(`\n Could not fetch models (${message}). Using defaults.\n`);
79
- return null;
80
- }
81
- }
82
-
83
- function isCodingModel(model: OpenRouterModel): boolean {
84
- if ((model.context_length || 0) < 32000) return false;
85
- if (model.id.includes('embed')) return false;
86
- if (model.id.includes('vision') && !model.id.includes('omni')) return false;
87
- const provider = model.id.includes('/') ? model.id.split('/')[0] : model.id;
88
- return CODING_PROVIDERS.includes(provider);
89
- }
90
-
91
- function dedupeAndFilter(models: OpenRouterModel[]): OpenRouterModel[] {
92
- const coding = models.filter(isCodingModel);
93
-
94
- // Deduplicate by provider family
95
- const seen = new Set<string>();
96
- const deduped: OpenRouterModel[] = [];
97
- for (const m of coding) {
98
- const provider = m.id.includes('/') ? m.id.split('/')[0] : m.id;
99
- if (seen.has(provider)) continue;
100
- seen.add(provider);
101
- deduped.push(m);
102
- }
103
-
104
- return deduped.slice(0, 8);
105
- }
106
-
107
- async function lookupModel(apiKey: string, modelId: string): Promise<ModelEntry | null> {
108
- try {
109
- const res = await fetch('https://openrouter.ai/api/v1/models?order=throughput', {
110
- headers: { Authorization: `Bearer ${apiKey}` },
111
- });
112
- if (!res.ok) return null;
113
- const json = (await res.json()) as { data?: OpenRouterModel[] };
114
- const found = json.data?.find((m) => m.id === modelId);
115
- if (found) return { id: found.id, label: formatLabel(found) };
116
- // If not found but looks like a valid ID, accept it anyway
117
- if (modelId.includes('/')) {
118
- return { id: modelId, label: formatLabel({ id: modelId }) };
119
- }
120
- return null;
121
- } catch {
122
- if (modelId.includes('/')) {
123
- return { id: modelId, label: formatLabel({ id: modelId }) };
124
- }
125
- return null;
126
- }
127
- }
128
-
129
- function promptQ(rl: Interface, question: string): Promise<string> {
130
- return new Promise((resolve) => {
131
- rl.question(question, (answer: string) => resolve(answer));
132
- });
133
- }
134
-
135
- // ── CLI: add <model_id> ─────────────────────────────────────────────
136
- async function cmdAdd(apiKey: string, configPath: string, modelId: string): Promise<void> {
137
- const config = loadConfig(configPath);
138
- if (config.openrouter_models.some((m) => m.id === modelId)) {
139
- console.log(` Already added: ${modelId}`);
140
- return;
141
- }
142
- const entry = await lookupModel(apiKey, modelId);
143
- if (!entry) {
144
- console.error(` Could not find model: ${modelId}`);
145
- process.exit(1);
146
- }
147
- config.openrouter_models.push(entry);
148
- saveConfig(configPath, config);
149
- console.log(` ✓ Added ${entry.label} (${entry.id})`);
150
- }
151
-
152
- // ── CLI: remove <model_id> ──────────────────────────────────────────
153
- function cmdRemove(configPath: string, modelId: string): void {
154
- const config = loadConfig(configPath);
155
- const before = config.openrouter_models.length;
156
- config.openrouter_models = config.openrouter_models.filter((m) => m.id !== modelId);
157
- if (config.openrouter_models.length === before) {
158
- console.log(` Not found: ${modelId}`);
159
- return;
160
- }
161
- saveConfig(configPath, config);
162
- console.log(` ✓ Removed ${modelId}`);
163
- }
164
-
165
- // ── CLI: list ───────────────────────────────────────────────────────
166
- function cmdList(configPath: string): void {
167
- const config = loadConfig(configPath);
168
- if (config.openrouter_models.length === 0) {
169
- console.log(' No models configured. Run: node dist/select-models.js');
170
- return;
171
- }
172
- console.log(' Current models:');
173
- for (const m of config.openrouter_models) {
174
- console.log(` ${m.label} (${m.id})`);
175
- }
176
- }
177
-
178
- // ── Interactive picker (default / install mode) ─────────────────────
179
- async function cmdPick(apiKey: string, configPath: string): Promise<void> {
180
- let topModels: ModelEntry[];
181
- const rawModels = await fetchModels(apiKey);
182
-
183
- if (rawModels === null) {
184
- topModels = FALLBACK_MODELS.map((m) => ({ ...m }));
185
- } else {
186
- const deduped = dedupeAndFilter(rawModels);
187
- topModels =
188
- deduped.length > 0
189
- ? deduped.map((m) => ({ id: m.id, label: formatLabel(m) }))
190
- : FALLBACK_MODELS.map((m) => ({ ...m }));
191
- }
192
-
193
- // Exclude the judge's own model family
194
- const platform = process.env.SMOOTHIE_PLATFORM || 'claude';
195
- const excludePrefixes: Record<string, string[]> = {
196
- claude: ['anthropic'],
197
- codex: ['openai'],
198
- gemini: ['google'],
199
- };
200
- const excluded = excludePrefixes[platform] || [];
201
- topModels = topModels.filter(m => !excluded.some(prefix => m.id.startsWith(prefix + '/')));
202
-
203
- // Default selection: first 3
204
- const selected = new Set([0, 1, 2]);
205
-
206
- // Print list with selection markers
207
- console.log('');
208
- for (let i = 0; i < topModels.length; i++) {
209
- const check = selected.has(i) ? '\x1b[32m✓\x1b[0m' : ' ';
210
- const num = String(i + 1).padStart(2, ' ');
211
- console.log(` ${check} ${num}. ${topModels[i].label}`);
212
- }
213
- console.log('');
214
-
215
- const rl = createInterface({ input: process.stdin, output: process.stdout });
216
-
217
- const answer = await promptQ(
218
- rl,
219
- ' Toggle numbers, paste model ID, or Enter to confirm: ',
220
- );
221
- rl.close();
222
-
223
- const input = answer.trim();
224
-
225
- // Collect any pasted model IDs (contain '/')
226
- const pastedIds: ModelEntry[] = [];
227
- const toggleNums: number[] = [];
228
-
229
- if (input) {
230
- for (const token of input.split(/\s+/)) {
231
- if (token.includes('/')) {
232
- // Looks like a model ID
233
- const entry = await lookupModel(apiKey, token);
234
- if (entry) {
235
- pastedIds.push(entry);
236
- console.log(` ✓ Added ${entry.label}`);
237
- } else {
238
- console.log(` ✗ Unknown: ${token}`);
239
- }
240
- } else {
241
- const n = parseInt(token, 10);
242
- if (n >= 1 && n <= topModels.length) toggleNums.push(n);
243
- }
244
- }
245
- }
246
-
247
- // If user typed numbers, use exactly those (not toggle, just select)
248
- let finalSelection: ModelEntry[];
249
- if (toggleNums.length > 0) {
250
- finalSelection = toggleNums.map((n) => topModels[n - 1]);
251
- } else if (pastedIds.length > 0) {
252
- // Keep defaults + add pasted
253
- finalSelection = [...selected].map((i) => topModels[i]);
254
- } else {
255
- // Enter with no input → use defaults
256
- finalSelection = [...selected].map((i) => topModels[i]);
257
- }
258
-
259
- // Merge pasted IDs
260
- for (const p of pastedIds) {
261
- if (!finalSelection.some((m) => m.id === p.id)) {
262
- finalSelection.push(p);
263
- }
264
- }
265
-
266
- // Preserve existing config fields (like auto_blend)
267
- const existing = loadConfig(configPath);
268
- existing.openrouter_models = finalSelection;
269
- saveConfig(configPath, existing);
270
-
271
- console.log(` ✓ ${finalSelection.map((m) => m.label).join(', ')}`);
272
- }
273
-
274
- // ── Main ────────────────────────────────────────────────────────────
275
- async function main(): Promise<void> {
276
- const args = process.argv.slice(2);
277
- loadEnv();
278
-
279
- const apiKey = process.env.OPENROUTER_API_KEY || '';
280
- const configPath =
281
- args.find((a) => a.endsWith('.json')) || join(PROJECT_ROOT, 'config.json');
282
-
283
- // Subcommands
284
- if (args[0] === 'add' && args[1]) {
285
- if (!apiKey) {
286
- console.error(' Set OPENROUTER_API_KEY in .env first');
287
- process.exit(1);
288
- }
289
- await cmdAdd(apiKey, configPath, args[1]);
290
- return;
291
- }
292
-
293
- if (args[0] === 'remove' && args[1]) {
294
- cmdRemove(configPath, args[1]);
295
- return;
296
- }
297
-
298
- if (args[0] === 'list') {
299
- cmdList(configPath);
300
- return;
301
- }
302
-
303
- // Interactive picker (default behavior / install mode)
304
- const key = args[0] && !args[0].endsWith('.json') ? args[0] : apiKey;
305
- if (!key) {
306
- console.error(' No API key. Usage: node dist/select-models.js <key>');
307
- console.error(' Or set OPENROUTER_API_KEY in .env');
308
- process.exit(1);
309
- }
310
- // If called with positional key, set it for fetching
311
- if (args[0] && !args[0].endsWith('.json') && args[0] !== 'add' && args[0] !== 'remove' && args[0] !== 'list') {
312
- process.env.OPENROUTER_API_KEY = args[0];
313
- }
314
-
315
- await cmdPick(key, configPath);
316
- }
317
-
318
- main();
package/tsconfig.json DELETED
@@ -1,14 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "target": "ES2022",
4
- "module": "Node16",
5
- "moduleResolution": "Node16",
6
- "outDir": "dist",
7
- "rootDir": "src",
8
- "strict": true,
9
- "esModuleInterop": true,
10
- "skipLibCheck": true,
11
- "declaration": true,
12
- "sourceMap": false
13
- }
14
- }