codemaxxing 1.1.3 → 1.1.4
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 +3 -2
- package/dist/index.js +122 -127
- package/dist/ui/connection-types.d.ts +1 -0
- package/dist/ui/connection.js +3 -3
- package/dist/ui/input-router.js +26 -18
- package/dist/ui/pickers.js +8 -8
- package/dist/ui/wizard-types.d.ts +1 -0
- package/dist/ui/wizard.js +7 -3
- package/dist/utils/models.js +52 -32
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -71,7 +71,7 @@ If you already have a local server running, Codemaxxing auto-detects common defa
|
|
|
71
71
|
|
|
72
72
|
For LM Studio:
|
|
73
73
|
1. Download [LM Studio](https://lmstudio.ai)
|
|
74
|
-
2. Load a coding model (
|
|
74
|
+
2. Load a coding model (ideally **Qwen 2.5 Coder 14B** or **DeepSeek Coder V2 16B** if your machine can handle it; use 7B only as a fallback)
|
|
75
75
|
3. Start the local server
|
|
76
76
|
4. Run:
|
|
77
77
|
|
|
@@ -89,10 +89,11 @@ codemaxxing
|
|
|
89
89
|
|
|
90
90
|
If no LLM is available, Codemaxxing can guide you through:
|
|
91
91
|
- detecting your hardware
|
|
92
|
-
- recommending a model
|
|
92
|
+
- recommending a model (with stronger coding defaults first)
|
|
93
93
|
- installing Ollama
|
|
94
94
|
- downloading the model
|
|
95
95
|
- connecting automatically
|
|
96
|
+
- opening the model picker automatically after cloud auth
|
|
96
97
|
|
|
97
98
|
### Option C — ChatGPT Plus (GPT-5.4, easiest cloud option)
|
|
98
99
|
|
package/dist/index.js
CHANGED
|
@@ -229,6 +229,7 @@ function App() {
|
|
|
229
229
|
setApproval,
|
|
230
230
|
setWizardScreen,
|
|
231
231
|
setWizardIndex,
|
|
232
|
+
openModelPicker,
|
|
232
233
|
});
|
|
233
234
|
}, []);
|
|
234
235
|
// Initialize agent on mount
|
|
@@ -252,6 +253,125 @@ function App() {
|
|
|
252
253
|
showSuggestionsRef.current = showSuggestions;
|
|
253
254
|
const pastedChunksRef = React.useRef(pastedChunks);
|
|
254
255
|
pastedChunksRef.current = pastedChunks;
|
|
256
|
+
const openModelPicker = useCallback(async () => {
|
|
257
|
+
addMsg("info", "Fetching available models...");
|
|
258
|
+
const groups = {};
|
|
259
|
+
const providerEntries = [];
|
|
260
|
+
let localFound = false;
|
|
261
|
+
const localEndpoints = [
|
|
262
|
+
{ name: "LM Studio", port: 1234 },
|
|
263
|
+
{ name: "Ollama", port: 11434 },
|
|
264
|
+
{ name: "vLLM", port: 8000 },
|
|
265
|
+
{ name: "LocalAI", port: 8080 },
|
|
266
|
+
];
|
|
267
|
+
for (const endpoint of localEndpoints) {
|
|
268
|
+
if (localFound)
|
|
269
|
+
break;
|
|
270
|
+
try {
|
|
271
|
+
const url = `http://localhost:${endpoint.port}/v1`;
|
|
272
|
+
const models = await listModels(url, "local");
|
|
273
|
+
if (models.length > 0) {
|
|
274
|
+
groups["Local LLM"] = models.map(m => ({
|
|
275
|
+
name: m,
|
|
276
|
+
baseUrl: url,
|
|
277
|
+
apiKey: "local",
|
|
278
|
+
providerType: "openai",
|
|
279
|
+
}));
|
|
280
|
+
localFound = true;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
catch { /* not running */ }
|
|
284
|
+
}
|
|
285
|
+
if (!localFound) {
|
|
286
|
+
try {
|
|
287
|
+
const ollamaModels = await listInstalledModelsDetailed();
|
|
288
|
+
if (ollamaModels.length > 0) {
|
|
289
|
+
groups["Local LLM"] = ollamaModels.map(m => ({
|
|
290
|
+
name: m.name,
|
|
291
|
+
baseUrl: "http://localhost:11434/v1",
|
|
292
|
+
apiKey: "ollama",
|
|
293
|
+
providerType: "openai",
|
|
294
|
+
}));
|
|
295
|
+
localFound = true;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
catch { /* Ollama not running */ }
|
|
299
|
+
}
|
|
300
|
+
if (localFound) {
|
|
301
|
+
providerEntries.push({ name: "Local LLM", description: "No auth needed — auto-detected", authed: true });
|
|
302
|
+
}
|
|
303
|
+
const anthropicCred = getCredential("anthropic");
|
|
304
|
+
const claudeModels = ["claude-sonnet-4-6", "claude-opus-4-6", "claude-haiku-4-5-20251001"];
|
|
305
|
+
if (anthropicCred) {
|
|
306
|
+
groups["Anthropic (Claude)"] = claudeModels.map(m => ({
|
|
307
|
+
name: m,
|
|
308
|
+
baseUrl: "https://api.anthropic.com",
|
|
309
|
+
apiKey: anthropicCred.apiKey,
|
|
310
|
+
providerType: "anthropic",
|
|
311
|
+
}));
|
|
312
|
+
}
|
|
313
|
+
providerEntries.push({ name: "Anthropic (Claude)", description: "Claude Opus, Sonnet, Haiku — use your subscription or API key", authed: !!anthropicCred });
|
|
314
|
+
const openaiCred = getCredential("openai");
|
|
315
|
+
const openaiModels = ["gpt-5.4", "gpt-5.4-pro", "gpt-5", "gpt-5-mini", "gpt-4.1", "gpt-4.1-mini", "o3", "o4-mini", "gpt-4o"];
|
|
316
|
+
if (openaiCred) {
|
|
317
|
+
const isOAuthToken = openaiCred.method === "oauth" || openaiCred.method === "cached-token" ||
|
|
318
|
+
(!openaiCred.apiKey.startsWith("sk-") && !openaiCred.apiKey.startsWith("sess-"));
|
|
319
|
+
const baseUrl = isOAuthToken
|
|
320
|
+
? "https://chatgpt.com/backend-api"
|
|
321
|
+
: (openaiCred.baseUrl || "https://api.openai.com/v1");
|
|
322
|
+
groups["OpenAI (ChatGPT)"] = openaiModels.map(m => ({
|
|
323
|
+
name: m,
|
|
324
|
+
baseUrl,
|
|
325
|
+
apiKey: openaiCred.apiKey,
|
|
326
|
+
providerType: "openai",
|
|
327
|
+
}));
|
|
328
|
+
}
|
|
329
|
+
providerEntries.push({ name: "OpenAI (ChatGPT)", description: "GPT-5, GPT-4.1, o3 — use your ChatGPT subscription or API key", authed: !!openaiCred });
|
|
330
|
+
const openrouterCred = getCredential("openrouter");
|
|
331
|
+
if (openrouterCred) {
|
|
332
|
+
try {
|
|
333
|
+
const orModels = await listModels(openrouterCred.baseUrl || "https://openrouter.ai/api/v1", openrouterCred.apiKey);
|
|
334
|
+
if (orModels.length > 0) {
|
|
335
|
+
groups["OpenRouter"] = orModels.slice(0, 20).map(m => ({
|
|
336
|
+
name: m,
|
|
337
|
+
baseUrl: openrouterCred.baseUrl || "https://openrouter.ai/api/v1",
|
|
338
|
+
apiKey: openrouterCred.apiKey,
|
|
339
|
+
providerType: "openai",
|
|
340
|
+
}));
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
catch { /* skip */ }
|
|
344
|
+
}
|
|
345
|
+
providerEntries.push({ name: "OpenRouter", description: "200+ models (Claude, GPT, Gemini, Llama, etc.) — one login", authed: !!openrouterCred });
|
|
346
|
+
const qwenCred = getCredential("qwen");
|
|
347
|
+
if (qwenCred) {
|
|
348
|
+
groups["Qwen"] = ["qwen-max", "qwen-plus", "qwen-turbo"].map(m => ({
|
|
349
|
+
name: m,
|
|
350
|
+
baseUrl: qwenCred.baseUrl || "https://dashscope.aliyuncs.com/compatible-mode/v1",
|
|
351
|
+
apiKey: qwenCred.apiKey,
|
|
352
|
+
providerType: "openai",
|
|
353
|
+
}));
|
|
354
|
+
}
|
|
355
|
+
providerEntries.push({ name: "Qwen", description: "Qwen 3.5, Qwen Coder — use your Qwen CLI login or API key", authed: !!qwenCred });
|
|
356
|
+
const copilotCred = getCredential("copilot");
|
|
357
|
+
if (copilotCred) {
|
|
358
|
+
groups["GitHub Copilot"] = ["gpt-4o", "claude-3.5-sonnet"].map(m => ({
|
|
359
|
+
name: m,
|
|
360
|
+
baseUrl: copilotCred.baseUrl || "https://api.githubcopilot.com",
|
|
361
|
+
apiKey: copilotCred.apiKey,
|
|
362
|
+
providerType: "openai",
|
|
363
|
+
}));
|
|
364
|
+
}
|
|
365
|
+
providerEntries.push({ name: "GitHub Copilot", description: "Use your GitHub Copilot subscription", authed: !!copilotCred });
|
|
366
|
+
if (providerEntries.length > 0) {
|
|
367
|
+
setModelPickerGroups(groups);
|
|
368
|
+
setProviderPicker(providerEntries);
|
|
369
|
+
setProviderPickerIndex(0);
|
|
370
|
+
setSelectedProvider(null);
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
addMsg("error", "No models available. Download one with /ollama pull or configure a provider.");
|
|
374
|
+
}, [addMsg]);
|
|
255
375
|
const handleSubmit = useCallback(async (value) => {
|
|
256
376
|
value = sanitizeInputArtifacts(value);
|
|
257
377
|
// Skip autocomplete if input exactly matches a command (e.g. /models vs /model)
|
|
@@ -478,133 +598,7 @@ function App() {
|
|
|
478
598
|
return;
|
|
479
599
|
}
|
|
480
600
|
if (trimmed === "/models" || trimmed === "/model") {
|
|
481
|
-
|
|
482
|
-
const groups = {};
|
|
483
|
-
const providerEntries = [];
|
|
484
|
-
// Local LLM (Ollama/LM Studio) — always show, auto-detect
|
|
485
|
-
let localFound = false;
|
|
486
|
-
// Check common local LLM endpoints
|
|
487
|
-
const localEndpoints = [
|
|
488
|
-
{ name: "LM Studio", port: 1234 },
|
|
489
|
-
{ name: "Ollama", port: 11434 },
|
|
490
|
-
{ name: "vLLM", port: 8000 },
|
|
491
|
-
{ name: "LocalAI", port: 8080 },
|
|
492
|
-
];
|
|
493
|
-
for (const endpoint of localEndpoints) {
|
|
494
|
-
if (localFound)
|
|
495
|
-
break;
|
|
496
|
-
try {
|
|
497
|
-
const url = `http://localhost:${endpoint.port}/v1`;
|
|
498
|
-
const models = await listModels(url, "local");
|
|
499
|
-
if (models.length > 0) {
|
|
500
|
-
groups["Local LLM"] = models.map(m => ({
|
|
501
|
-
name: m,
|
|
502
|
-
baseUrl: url,
|
|
503
|
-
apiKey: "local",
|
|
504
|
-
providerType: "openai",
|
|
505
|
-
}));
|
|
506
|
-
localFound = true;
|
|
507
|
-
}
|
|
508
|
-
}
|
|
509
|
-
catch { /* not running */ }
|
|
510
|
-
}
|
|
511
|
-
// Also check Ollama native API
|
|
512
|
-
if (!localFound) {
|
|
513
|
-
try {
|
|
514
|
-
const ollamaModels = await listInstalledModelsDetailed();
|
|
515
|
-
if (ollamaModels.length > 0) {
|
|
516
|
-
groups["Local LLM"] = ollamaModels.map(m => ({
|
|
517
|
-
name: m.name,
|
|
518
|
-
baseUrl: "http://localhost:11434/v1",
|
|
519
|
-
apiKey: "ollama",
|
|
520
|
-
providerType: "openai",
|
|
521
|
-
}));
|
|
522
|
-
localFound = true;
|
|
523
|
-
}
|
|
524
|
-
}
|
|
525
|
-
catch { /* Ollama not running */ }
|
|
526
|
-
}
|
|
527
|
-
if (localFound) {
|
|
528
|
-
providerEntries.push({ name: "Local LLM", description: "No auth needed — auto-detected", authed: true });
|
|
529
|
-
}
|
|
530
|
-
// Anthropic
|
|
531
|
-
const anthropicCred = getCredential("anthropic");
|
|
532
|
-
const claudeModels = ["claude-sonnet-4-6", "claude-opus-4-6", "claude-haiku-4-5-20251001"];
|
|
533
|
-
if (anthropicCred) {
|
|
534
|
-
groups["Anthropic (Claude)"] = claudeModels.map(m => ({
|
|
535
|
-
name: m,
|
|
536
|
-
baseUrl: "https://api.anthropic.com",
|
|
537
|
-
apiKey: anthropicCred.apiKey,
|
|
538
|
-
providerType: "anthropic",
|
|
539
|
-
}));
|
|
540
|
-
}
|
|
541
|
-
providerEntries.push({ name: "Anthropic (Claude)", description: "Claude Opus, Sonnet, Haiku — use your subscription or API key", authed: !!anthropicCred });
|
|
542
|
-
// OpenAI
|
|
543
|
-
const openaiCred = getCredential("openai");
|
|
544
|
-
const openaiModels = ["gpt-5.4", "gpt-5.4-pro", "gpt-5", "gpt-5-mini", "gpt-4.1", "gpt-4.1-mini", "o3", "o4-mini", "gpt-4o"];
|
|
545
|
-
if (openaiCred) {
|
|
546
|
-
// OAuth tokens (non sk- keys) must use ChatGPT backend, not api.openai.com
|
|
547
|
-
const isOAuthToken = openaiCred.method === "oauth" || openaiCred.method === "cached-token" ||
|
|
548
|
-
(!openaiCred.apiKey.startsWith("sk-") && !openaiCred.apiKey.startsWith("sess-"));
|
|
549
|
-
const baseUrl = isOAuthToken
|
|
550
|
-
? "https://chatgpt.com/backend-api"
|
|
551
|
-
: (openaiCred.baseUrl || "https://api.openai.com/v1");
|
|
552
|
-
groups["OpenAI (ChatGPT)"] = openaiModels.map(m => ({
|
|
553
|
-
name: m,
|
|
554
|
-
baseUrl,
|
|
555
|
-
apiKey: openaiCred.apiKey,
|
|
556
|
-
providerType: "openai",
|
|
557
|
-
}));
|
|
558
|
-
}
|
|
559
|
-
providerEntries.push({ name: "OpenAI (ChatGPT)", description: "GPT-5, GPT-4.1, o3 — use your ChatGPT subscription or API key", authed: !!openaiCred });
|
|
560
|
-
// OpenRouter
|
|
561
|
-
const openrouterCred = getCredential("openrouter");
|
|
562
|
-
if (openrouterCred) {
|
|
563
|
-
try {
|
|
564
|
-
const orModels = await listModels(openrouterCred.baseUrl || "https://openrouter.ai/api/v1", openrouterCred.apiKey);
|
|
565
|
-
if (orModels.length > 0) {
|
|
566
|
-
groups["OpenRouter"] = orModels.slice(0, 20).map(m => ({
|
|
567
|
-
name: m,
|
|
568
|
-
baseUrl: openrouterCred.baseUrl || "https://openrouter.ai/api/v1",
|
|
569
|
-
apiKey: openrouterCred.apiKey,
|
|
570
|
-
providerType: "openai",
|
|
571
|
-
}));
|
|
572
|
-
}
|
|
573
|
-
}
|
|
574
|
-
catch { /* skip */ }
|
|
575
|
-
}
|
|
576
|
-
providerEntries.push({ name: "OpenRouter", description: "200+ models (Claude, GPT, Gemini, Llama, etc.) — one login", authed: !!openrouterCred });
|
|
577
|
-
// Qwen
|
|
578
|
-
const qwenCred = getCredential("qwen");
|
|
579
|
-
if (qwenCred) {
|
|
580
|
-
groups["Qwen"] = ["qwen-max", "qwen-plus", "qwen-turbo"].map(m => ({
|
|
581
|
-
name: m,
|
|
582
|
-
baseUrl: qwenCred.baseUrl || "https://dashscope.aliyuncs.com/compatible-mode/v1",
|
|
583
|
-
apiKey: qwenCred.apiKey,
|
|
584
|
-
providerType: "openai",
|
|
585
|
-
}));
|
|
586
|
-
}
|
|
587
|
-
providerEntries.push({ name: "Qwen", description: "Qwen 3.5, Qwen Coder — use your Qwen CLI login or API key", authed: !!qwenCred });
|
|
588
|
-
// GitHub Copilot
|
|
589
|
-
const copilotCred = getCredential("copilot");
|
|
590
|
-
if (copilotCred) {
|
|
591
|
-
groups["GitHub Copilot"] = ["gpt-4o", "claude-3.5-sonnet"].map(m => ({
|
|
592
|
-
name: m,
|
|
593
|
-
baseUrl: copilotCred.baseUrl || "https://api.githubcopilot.com",
|
|
594
|
-
apiKey: copilotCred.apiKey,
|
|
595
|
-
providerType: "openai",
|
|
596
|
-
}));
|
|
597
|
-
}
|
|
598
|
-
providerEntries.push({ name: "GitHub Copilot", description: "Use your GitHub Copilot subscription", authed: !!copilotCred });
|
|
599
|
-
// Show provider picker (step 1)
|
|
600
|
-
if (providerEntries.length > 0) {
|
|
601
|
-
setModelPickerGroups(groups);
|
|
602
|
-
setProviderPicker(providerEntries);
|
|
603
|
-
setProviderPickerIndex(0);
|
|
604
|
-
setSelectedProvider(null);
|
|
605
|
-
return;
|
|
606
|
-
}
|
|
607
|
-
addMsg("error", "No models available. Download one with /ollama pull or configure a provider.");
|
|
601
|
+
await openModelPicker();
|
|
608
602
|
return;
|
|
609
603
|
}
|
|
610
604
|
if (trimmed.startsWith("/model ")) {
|
|
@@ -832,6 +826,7 @@ function App() {
|
|
|
832
826
|
exit,
|
|
833
827
|
refreshConnectionBanner,
|
|
834
828
|
connectToProvider,
|
|
829
|
+
openModelPicker,
|
|
835
830
|
handleSubmit,
|
|
836
831
|
_require,
|
|
837
832
|
});
|
package/dist/ui/connection.js
CHANGED
|
@@ -75,11 +75,11 @@ export async function connectToProvider(isRetry, ctx) {
|
|
|
75
75
|
!!getCredential("openrouter") || !!getCredential("qwen") ||
|
|
76
76
|
!!getCredential("copilot");
|
|
77
77
|
if (hasAnyCreds) {
|
|
78
|
-
// User has auth'd before — skip wizard
|
|
79
|
-
info.push("✔ Found saved credentials.
|
|
78
|
+
// User has auth'd before — skip wizard and go straight to the model picker
|
|
79
|
+
info.push("✔ Found saved credentials. Opening model picker...");
|
|
80
80
|
ctx.setConnectionInfo([...info]);
|
|
81
81
|
ctx.setReady(true);
|
|
82
|
-
|
|
82
|
+
await ctx.openModelPicker();
|
|
83
83
|
return;
|
|
84
84
|
}
|
|
85
85
|
// No creds found — show the setup wizard
|
package/dist/ui/input-router.js
CHANGED
|
@@ -104,9 +104,10 @@ function handleLoginMethodPicker(inputChar, key, ctx) {
|
|
|
104
104
|
ctx.setLoading(true);
|
|
105
105
|
ctx.setSpinnerMsg("Waiting for authorization...");
|
|
106
106
|
openRouterOAuth((msg) => ctx.addMsg("info", msg))
|
|
107
|
-
.then(() => {
|
|
108
|
-
ctx.addMsg("info", `✅ OpenRouter authenticated! Access to 200+ models
|
|
107
|
+
.then(async () => {
|
|
108
|
+
ctx.addMsg("info", `✅ OpenRouter authenticated! Access to 200+ models.\n Opening model picker...`);
|
|
109
109
|
ctx.setLoading(false);
|
|
110
|
+
await ctx.openModelPicker();
|
|
110
111
|
})
|
|
111
112
|
.catch((err) => { ctx.addMsg("error", `OAuth failed: ${err.message}`); ctx.setLoading(false); });
|
|
112
113
|
}
|
|
@@ -115,9 +116,10 @@ function handleLoginMethodPicker(inputChar, key, ctx) {
|
|
|
115
116
|
ctx.setLoading(true);
|
|
116
117
|
ctx.setSpinnerMsg("Waiting for Anthropic authorization...");
|
|
117
118
|
loginAnthropicOAuth((msg) => ctx.addMsg("info", msg))
|
|
118
|
-
.then((cred) => {
|
|
119
|
-
ctx.addMsg("info", `✅ Anthropic authenticated! (${cred.label})\n
|
|
119
|
+
.then(async (cred) => {
|
|
120
|
+
ctx.addMsg("info", `✅ Anthropic authenticated! (${cred.label})\n Opening model picker...`);
|
|
120
121
|
ctx.setLoading(false);
|
|
122
|
+
await ctx.openModelPicker();
|
|
121
123
|
})
|
|
122
124
|
.catch((err) => {
|
|
123
125
|
ctx.addMsg("error", `OAuth failed: ${err.message}\n Fallback: set your key via CLI: codemaxxing auth api-key anthropic <your-key>\n Or set ANTHROPIC_API_KEY env var and restart.\n Get key at: console.anthropic.com/settings/keys`);
|
|
@@ -128,7 +130,8 @@ function handleLoginMethodPicker(inputChar, key, ctx) {
|
|
|
128
130
|
// Try cached Codex token first as a quick path
|
|
129
131
|
const imported = importCodexToken((msg) => ctx.addMsg("info", msg));
|
|
130
132
|
if (imported) {
|
|
131
|
-
ctx.addMsg("info", `✅ Imported Codex credentials! (${imported.label})
|
|
133
|
+
ctx.addMsg("info", `✅ Imported Codex credentials! (${imported.label})\n Opening model picker...`);
|
|
134
|
+
void ctx.openModelPicker();
|
|
132
135
|
}
|
|
133
136
|
else {
|
|
134
137
|
// Primary flow: browser OAuth
|
|
@@ -136,9 +139,10 @@ function handleLoginMethodPicker(inputChar, key, ctx) {
|
|
|
136
139
|
ctx.setLoading(true);
|
|
137
140
|
ctx.setSpinnerMsg("Waiting for OpenAI authorization...");
|
|
138
141
|
loginOpenAICodexOAuth((msg) => ctx.addMsg("info", msg))
|
|
139
|
-
.then((cred) => {
|
|
140
|
-
ctx.addMsg("info", `✅ OpenAI authenticated! (${cred.label})\n
|
|
142
|
+
.then(async (cred) => {
|
|
143
|
+
ctx.addMsg("info", `✅ OpenAI authenticated! (${cred.label})\n Opening model picker...`);
|
|
141
144
|
ctx.setLoading(false);
|
|
145
|
+
await ctx.openModelPicker();
|
|
142
146
|
})
|
|
143
147
|
.catch((err) => {
|
|
144
148
|
ctx.addMsg("error", `OAuth failed: ${err.message}\n Fallback: set your key via CLI: codemaxxing auth api-key openai <your-key>\n Or set OPENAI_API_KEY env var and restart.\n Get key at: platform.openai.com/api-keys`);
|
|
@@ -149,7 +153,8 @@ function handleLoginMethodPicker(inputChar, key, ctx) {
|
|
|
149
153
|
else if (method === "cached-token" && providerId === "qwen") {
|
|
150
154
|
const imported = importQwenToken((msg) => ctx.addMsg("info", msg));
|
|
151
155
|
if (imported) {
|
|
152
|
-
ctx.addMsg("info", `✅ Imported Qwen credentials! (${imported.label})
|
|
156
|
+
ctx.addMsg("info", `✅ Imported Qwen credentials! (${imported.label})\n Opening model picker...`);
|
|
157
|
+
void ctx.openModelPicker();
|
|
153
158
|
}
|
|
154
159
|
else {
|
|
155
160
|
ctx.addMsg("info", "No Qwen CLI found. Install Qwen CLI and sign in first.");
|
|
@@ -160,7 +165,7 @@ function handleLoginMethodPicker(inputChar, key, ctx) {
|
|
|
160
165
|
ctx.setLoading(true);
|
|
161
166
|
ctx.setSpinnerMsg("Waiting for GitHub authorization...");
|
|
162
167
|
copilotDeviceFlow((msg) => ctx.addMsg("info", msg))
|
|
163
|
-
.then(() => { ctx.addMsg("info", `✅ GitHub Copilot authenticated!\n
|
|
168
|
+
.then(async () => { ctx.addMsg("info", `✅ GitHub Copilot authenticated!\n Opening model picker...`); ctx.setLoading(false); await ctx.openModelPicker(); })
|
|
164
169
|
.catch((err) => { ctx.addMsg("error", `Copilot auth failed: ${err.message}`); ctx.setLoading(false); });
|
|
165
170
|
}
|
|
166
171
|
else if (method === "api-key") {
|
|
@@ -196,7 +201,7 @@ function handleLoginPicker(_inputChar, key, ctx) {
|
|
|
196
201
|
ctx.setLoading(true);
|
|
197
202
|
ctx.setSpinnerMsg("Waiting for authorization...");
|
|
198
203
|
openRouterOAuth((msg) => ctx.addMsg("info", msg))
|
|
199
|
-
.then(() => { ctx.addMsg("info", `✅ OpenRouter authenticated! Access to 200+ models.\n
|
|
204
|
+
.then(async () => { ctx.addMsg("info", `✅ OpenRouter authenticated! Access to 200+ models.\n Opening model picker...`); ctx.setLoading(false); await ctx.openModelPicker(); })
|
|
200
205
|
.catch((err) => { ctx.addMsg("error", `OAuth failed: ${err.message}`); ctx.setLoading(false); });
|
|
201
206
|
}
|
|
202
207
|
else if (methods[0] === "device-flow") {
|
|
@@ -205,7 +210,7 @@ function handleLoginPicker(_inputChar, key, ctx) {
|
|
|
205
210
|
ctx.setLoading(true);
|
|
206
211
|
ctx.setSpinnerMsg("Waiting for GitHub authorization...");
|
|
207
212
|
copilotDeviceFlow((msg) => ctx.addMsg("info", msg))
|
|
208
|
-
.then(() => { ctx.addMsg("info", `✅ GitHub Copilot authenticated!\n
|
|
213
|
+
.then(async () => { ctx.addMsg("info", `✅ GitHub Copilot authenticated!\n Opening model picker...`); ctx.setLoading(false); await ctx.openModelPicker(); })
|
|
209
214
|
.catch((err) => { ctx.addMsg("error", `Copilot auth failed: ${err.message}`); ctx.setLoading(false); });
|
|
210
215
|
}
|
|
211
216
|
else if (methods[0] === "api-key") {
|
|
@@ -387,6 +392,9 @@ function handleProviderPicker(_inputChar, key, ctx) {
|
|
|
387
392
|
}
|
|
388
393
|
if (key.escape) {
|
|
389
394
|
ctx.setProviderPicker(null);
|
|
395
|
+
if (!ctx.agent) {
|
|
396
|
+
ctx.addMsg("info", "Model selection cancelled. Use /login for cloud providers or choose local setup from the startup menu.");
|
|
397
|
+
}
|
|
390
398
|
return true;
|
|
391
399
|
}
|
|
392
400
|
if (key.return) {
|
|
@@ -482,13 +490,13 @@ function handleOllamaPullPicker(_inputChar, key, ctx) {
|
|
|
482
490
|
if (!ctx.ollamaPullPicker)
|
|
483
491
|
return false;
|
|
484
492
|
const pullModels = [
|
|
485
|
-
{ id: "qwen2.5-coder:
|
|
486
|
-
{ id: "
|
|
487
|
-
{ id: "qwen2.5-coder:
|
|
488
|
-
{ id: "qwen2.5-coder:32b", name: "Qwen 2.5 Coder 32B", size: "20 GB", desc: "Premium quality, needs
|
|
489
|
-
{ id: "
|
|
490
|
-
{ id: "
|
|
491
|
-
{ id: "
|
|
493
|
+
{ id: "qwen2.5-coder:14b", name: "Qwen 2.5 Coder 14B", size: "9 GB", desc: "Recommended default for coding if your machine can handle it" },
|
|
494
|
+
{ id: "deepseek-coder-v2:16b", name: "DeepSeek Coder V2 16B", size: "9 GB", desc: "Strong higher-quality alternative" },
|
|
495
|
+
{ id: "qwen2.5-coder:7b", name: "Qwen 2.5 Coder 7B", size: "5 GB", desc: "Fallback for mid-range machines" },
|
|
496
|
+
{ id: "qwen2.5-coder:32b", name: "Qwen 2.5 Coder 32B", size: "20 GB", desc: "Premium quality, needs lots of RAM" },
|
|
497
|
+
{ id: "codellama:7b", name: "CodeLlama 7B", size: "4 GB", desc: "Older fallback coding model" },
|
|
498
|
+
{ id: "starcoder2:7b", name: "StarCoder2 7B", size: "4 GB", desc: "Completion-focused fallback" },
|
|
499
|
+
{ id: "qwen2.5-coder:3b", name: "Qwen 2.5 Coder 3B", size: "2 GB", desc: "⚠️ Last resort — may struggle with tool calls" },
|
|
492
500
|
];
|
|
493
501
|
if (key.upArrow) {
|
|
494
502
|
ctx.setOllamaPullPickerIndex((prev) => (prev - 1 + pullModels.length) % pullModels.length);
|
package/dist/ui/pickers.js
CHANGED
|
@@ -74,13 +74,13 @@ export function OllamaDeletePicker({ models, selectedIndex, colors }) {
|
|
|
74
74
|
}
|
|
75
75
|
// ── Ollama Pull Picker ──
|
|
76
76
|
const PULL_MODELS = [
|
|
77
|
-
{ id: "qwen2.5-coder:
|
|
78
|
-
{ id: "
|
|
79
|
-
{ id: "qwen2.5-coder:
|
|
80
|
-
{ id: "qwen2.5-coder:32b", name: "Qwen 2.5 Coder 32B", size: "20 GB", desc: "Premium, needs
|
|
81
|
-
{ id: "
|
|
82
|
-
{ id: "
|
|
83
|
-
{ id: "
|
|
77
|
+
{ id: "qwen2.5-coder:14b", name: "Qwen 2.5 Coder 14B", size: "9 GB", desc: "Recommended default for coding if your machine can handle it" },
|
|
78
|
+
{ id: "deepseek-coder-v2:16b", name: "DeepSeek Coder V2 16B", size: "9 GB", desc: "Strong higher-quality alternative" },
|
|
79
|
+
{ id: "qwen2.5-coder:7b", name: "Qwen 2.5 Coder 7B", size: "5 GB", desc: "Fallback for mid-range machines" },
|
|
80
|
+
{ id: "qwen2.5-coder:32b", name: "Qwen 2.5 Coder 32B", size: "20 GB", desc: "Premium quality, needs lots of RAM" },
|
|
81
|
+
{ id: "codellama:7b", name: "CodeLlama 7B", size: "4 GB", desc: "Older fallback coding model" },
|
|
82
|
+
{ id: "starcoder2:7b", name: "StarCoder2 7B", size: "4 GB", desc: "Completion-focused fallback" },
|
|
83
|
+
{ id: "qwen2.5-coder:3b", name: "Qwen 2.5 Coder 3B", size: "2 GB", desc: "⚠️ Last resort — may struggle with tool calls" },
|
|
84
84
|
];
|
|
85
85
|
export function OllamaPullPicker({ selectedIndex, colors }) {
|
|
86
86
|
return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: colors.border, paddingX: 1, marginBottom: 0, children: [_jsx(Text, { bold: true, color: colors.secondary, children: "Download which model?" }), _jsx(Text, { children: "" }), PULL_MODELS.map((m, i) => (_jsxs(Text, { children: [" ", i === selectedIndex ? _jsx(Text, { color: colors.primary, bold: true, children: "▸ " }) : " ", _jsx(Text, { color: i === selectedIndex ? colors.primary : undefined, bold: true, children: m.name }), _jsxs(Text, { color: colors.muted, children: [" · ", m.size, " · ", m.desc] })] }, m.id))), _jsx(Text, { children: "" }), _jsx(Text, { dimColor: true, children: " ↑↓ navigate · Enter to download · Esc cancel" })] }));
|
|
@@ -113,7 +113,7 @@ export function WizardModels({ wizardIndex, wizardHardware, wizardModels, colors
|
|
|
113
113
|
return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: colors.border, paddingX: 1, marginBottom: 0, children: [_jsx(Text, { bold: true, color: colors.secondary, children: "Your hardware:" }), _jsxs(Text, { color: colors.muted, children: [" CPU: ", wizardHardware.cpu.name, " (", wizardHardware.cpu.cores, " cores)"] }), _jsxs(Text, { color: colors.muted, children: [" RAM: ", formatBytes(wizardHardware.ram)] }), wizardHardware.gpu ? (_jsxs(Text, { color: colors.muted, children: [" GPU: ", wizardHardware.gpu.name, wizardHardware.gpu.vram > 0 ? ` (${formatBytes(wizardHardware.gpu.vram)})` : ""] })) : (_jsx(Text, { color: colors.muted, children: " GPU: none detected" })), !isLlmfitAvailable() && (_jsx(Text, { dimColor: true, children: " Tip: Install llmfit for smarter recommendations: brew install llmfit" })), _jsx(Text, { children: "" }), _jsx(Text, { bold: true, color: colors.secondary, children: "Recommended models:" }), _jsx(Text, { children: "" }), wizardModels.map((m, i) => (_jsxs(Text, { children: [i === wizardIndex ? _jsx(Text, { color: colors.suggestion, bold: true, children: " \u25B8 " }) : _jsx(Text, { children: " " }), _jsxs(Text, { children: [getFitIcon(m.fit), " "] }), _jsx(Text, { color: i === wizardIndex ? colors.suggestion : colors.primary, bold: true, children: m.name }), _jsxs(Text, { color: colors.muted, children: [" ~", m.size, " GB \u00B7 ", m.quality === "best" ? "Best" : m.quality === "great" ? "Great" : "Good", " quality \u00B7 ", m.speed] })] }, m.ollamaId))), wizardModels.length === 0 && (_jsx(Text, { color: colors.error, children: " No suitable models found for your hardware." })), _jsx(Text, { children: "" }), _jsx(Text, { dimColor: true, children: " \u2191\u2193 navigate \u00B7 Enter to install \u00B7 Esc back" })] }));
|
|
114
114
|
}
|
|
115
115
|
export function WizardInstallOllama({ wizardHardware, colors }) {
|
|
116
|
-
return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: colors.warning, paddingX: 1, marginBottom: 0, children: [_jsx(Text, { bold: true, color: colors.warning, children: "Ollama is required for local models." }), _jsx(Text, { children: "" }), _jsx(Text, { color: colors.primary, children: " Press Enter to install Ollama automatically" }), _jsxs(Text, { dimColor: true, children: [" Or install manually: ", _jsx(Text, { children: getOllamaInstallCommand(wizardHardware?.os ?? "linux") })] }), _jsx(Text, { children: "" }), _jsx(Text, { dimColor: true, children: " Enter to install · Esc to
|
|
116
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: colors.warning, paddingX: 1, marginBottom: 0, children: [_jsx(Text, { bold: true, color: colors.warning, children: "Ollama is required for local models." }), _jsx(Text, { children: "" }), _jsx(Text, { color: colors.primary, children: " Press Enter to install Ollama automatically" }), _jsxs(Text, { dimColor: true, children: [" Or install manually: ", _jsx(Text, { children: getOllamaInstallCommand(wizardHardware?.os ?? "linux") })] }), _jsx(Text, { children: "" }), _jsx(Text, { dimColor: true, children: " Enter to install · Esc back to model list" })] }));
|
|
117
117
|
}
|
|
118
118
|
export function WizardPulling({ wizardSelectedModel, wizardPullProgress, wizardPullError, colors }) {
|
|
119
119
|
return (_jsx(Box, { flexDirection: "column", borderStyle: "single", borderColor: colors.border, paddingX: 1, marginBottom: 0, children: wizardPullError ? (_jsxs(_Fragment, { children: [_jsxs(Text, { color: colors.error, bold: true, children: [" \u274C Error: ", wizardPullError] }), _jsx(Text, { children: "" }), _jsx(Text, { dimColor: true, children: " Press Enter to retry \u00B7 Esc to go back" })] })) : wizardPullProgress ? (_jsxs(_Fragment, { children: [_jsxs(Text, { bold: true, color: colors.secondary, children: [" ", wizardSelectedModel ? `Downloading ${wizardSelectedModel.name}...` : wizardPullProgress?.status || "Working..."] }), wizardPullProgress.status === "downloading" || wizardPullProgress.percent > 0 ? (_jsx(_Fragment, { children: _jsxs(Text, { children: [" ", _jsxs(Text, { color: colors.primary, children: ["\u2588".repeat(Math.floor(wizardPullProgress.percent / 5)), "\u2591".repeat(20 - Math.floor(wizardPullProgress.percent / 5))] }), " ", _jsxs(Text, { bold: true, children: [wizardPullProgress.percent, "%"] }), wizardPullProgress.completed != null && wizardPullProgress.total != null ? (_jsxs(Text, { color: colors.muted, children: [" \u00B7 ", formatBytes(wizardPullProgress.completed), " / ", formatBytes(wizardPullProgress.total)] })) : null] }) })) : (_jsxs(Text, { color: colors.muted, children: [" ", wizardPullProgress.status, "..."] }))] })) : null }));
|
|
@@ -23,5 +23,6 @@ export interface WizardContext {
|
|
|
23
23
|
setSpinnerMsg: (val: string) => void;
|
|
24
24
|
addMsg: (type: "user" | "response" | "tool" | "tool-result" | "error" | "info", text: string) => void;
|
|
25
25
|
connectToProvider: (isRetry: boolean) => Promise<void>;
|
|
26
|
+
openModelPicker: () => Promise<void>;
|
|
26
27
|
_require: NodeRequire;
|
|
27
28
|
}
|
package/dist/ui/wizard.js
CHANGED
|
@@ -35,9 +35,10 @@ export function handleWizardScreen(_inputChar, key, ctx) {
|
|
|
35
35
|
ctx.setLoading(true);
|
|
36
36
|
ctx.setSpinnerMsg("Waiting for authorization...");
|
|
37
37
|
openRouterOAuth((msg) => ctx.addMsg("info", msg))
|
|
38
|
-
.then(() => {
|
|
39
|
-
ctx.addMsg("info", "✅ OpenRouter authenticated!
|
|
38
|
+
.then(async () => {
|
|
39
|
+
ctx.addMsg("info", "✅ OpenRouter authenticated! Opening model picker...");
|
|
40
40
|
ctx.setLoading(false);
|
|
41
|
+
await ctx.openModelPicker();
|
|
41
42
|
})
|
|
42
43
|
.catch((err) => { ctx.addMsg("error", `OAuth failed: ${err.message}`); ctx.setLoading(false); });
|
|
43
44
|
}
|
|
@@ -45,10 +46,12 @@ export function handleWizardScreen(_inputChar, key, ctx) {
|
|
|
45
46
|
ctx.setWizardScreen(null);
|
|
46
47
|
ctx.setLoginPicker(true);
|
|
47
48
|
ctx.setLoginPickerIndex(() => 0);
|
|
49
|
+
ctx.addMsg("info", "Choose a cloud provider to continue.");
|
|
48
50
|
}
|
|
49
51
|
else if (selected === "existing") {
|
|
50
52
|
ctx.setWizardScreen(null);
|
|
51
|
-
ctx.addMsg("info", "
|
|
53
|
+
ctx.addMsg("info", "Checking for your running server...");
|
|
54
|
+
void ctx.connectToProvider(true);
|
|
52
55
|
}
|
|
53
56
|
return true;
|
|
54
57
|
}
|
|
@@ -196,6 +199,7 @@ function startPullFlow(ctx, selected) {
|
|
|
196
199
|
return;
|
|
197
200
|
}
|
|
198
201
|
}
|
|
202
|
+
ctx.addMsg("info", `Downloading ${selected.name}. This can take a while on first run depending on your internet and disk speed.`);
|
|
199
203
|
await pullModel(selected.ollamaId, (p) => {
|
|
200
204
|
ctx.setWizardPullProgress(p);
|
|
201
205
|
});
|
package/dist/utils/models.js
CHANGED
|
@@ -1,14 +1,24 @@
|
|
|
1
1
|
import { execSync } from "child_process";
|
|
2
2
|
const MODELS = [
|
|
3
3
|
{
|
|
4
|
-
name: "Qwen 2.5 Coder
|
|
5
|
-
ollamaId: "qwen2.5-coder:
|
|
6
|
-
size:
|
|
7
|
-
ramRequired:
|
|
8
|
-
vramOptimal:
|
|
9
|
-
description: "
|
|
10
|
-
speed: "~
|
|
11
|
-
quality: "
|
|
4
|
+
name: "Qwen 2.5 Coder 14B",
|
|
5
|
+
ollamaId: "qwen2.5-coder:14b",
|
|
6
|
+
size: 9,
|
|
7
|
+
ramRequired: 24,
|
|
8
|
+
vramOptimal: 12,
|
|
9
|
+
description: "Recommended default for coding when your machine can handle it",
|
|
10
|
+
speed: "~25 tok/s on M1 Pro",
|
|
11
|
+
quality: "best",
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
name: "DeepSeek Coder V2 16B",
|
|
15
|
+
ollamaId: "deepseek-coder-v2:16b",
|
|
16
|
+
size: 9,
|
|
17
|
+
ramRequired: 24,
|
|
18
|
+
vramOptimal: 12,
|
|
19
|
+
description: "Strong higher-quality alternative for coding",
|
|
20
|
+
speed: "~30 tok/s on M1 Pro",
|
|
21
|
+
quality: "great",
|
|
12
22
|
},
|
|
13
23
|
{
|
|
14
24
|
name: "Qwen 2.5 Coder 7B",
|
|
@@ -16,20 +26,10 @@ const MODELS = [
|
|
|
16
26
|
size: 5,
|
|
17
27
|
ramRequired: 16,
|
|
18
28
|
vramOptimal: 8,
|
|
19
|
-
description: "
|
|
29
|
+
description: "Fallback for mid-range machines when 14B is too heavy",
|
|
20
30
|
speed: "~45 tok/s on M1",
|
|
21
31
|
quality: "great",
|
|
22
32
|
},
|
|
23
|
-
{
|
|
24
|
-
name: "Qwen 2.5 Coder 14B",
|
|
25
|
-
ollamaId: "qwen2.5-coder:14b",
|
|
26
|
-
size: 9,
|
|
27
|
-
ramRequired: 32,
|
|
28
|
-
vramOptimal: 16,
|
|
29
|
-
description: "High quality coding",
|
|
30
|
-
speed: "~25 tok/s on M1 Pro",
|
|
31
|
-
quality: "best",
|
|
32
|
-
},
|
|
33
33
|
{
|
|
34
34
|
name: "Qwen 2.5 Coder 32B",
|
|
35
35
|
ollamaId: "qwen2.5-coder:32b",
|
|
@@ -40,23 +40,13 @@ const MODELS = [
|
|
|
40
40
|
speed: "~12 tok/s on M1 Max",
|
|
41
41
|
quality: "best",
|
|
42
42
|
},
|
|
43
|
-
{
|
|
44
|
-
name: "DeepSeek Coder V2 16B",
|
|
45
|
-
ollamaId: "deepseek-coder-v2:16b",
|
|
46
|
-
size: 9,
|
|
47
|
-
ramRequired: 32,
|
|
48
|
-
vramOptimal: 16,
|
|
49
|
-
description: "Strong alternative for coding",
|
|
50
|
-
speed: "~30 tok/s on M1 Pro",
|
|
51
|
-
quality: "great",
|
|
52
|
-
},
|
|
53
43
|
{
|
|
54
44
|
name: "CodeLlama 7B",
|
|
55
45
|
ollamaId: "codellama:7b",
|
|
56
46
|
size: 4,
|
|
57
47
|
ramRequired: 16,
|
|
58
48
|
vramOptimal: 8,
|
|
59
|
-
description: "
|
|
49
|
+
description: "Older fallback coding model",
|
|
60
50
|
speed: "~40 tok/s on M1",
|
|
61
51
|
quality: "good",
|
|
62
52
|
},
|
|
@@ -66,10 +56,20 @@ const MODELS = [
|
|
|
66
56
|
size: 4,
|
|
67
57
|
ramRequired: 16,
|
|
68
58
|
vramOptimal: 8,
|
|
69
|
-
description: "
|
|
59
|
+
description: "Completion-focused fallback",
|
|
70
60
|
speed: "~40 tok/s on M1",
|
|
71
61
|
quality: "good",
|
|
72
62
|
},
|
|
63
|
+
{
|
|
64
|
+
name: "Qwen 2.5 Coder 3B",
|
|
65
|
+
ollamaId: "qwen2.5-coder:3b",
|
|
66
|
+
size: 2,
|
|
67
|
+
ramRequired: 8,
|
|
68
|
+
vramOptimal: 4,
|
|
69
|
+
description: "⚠️ Last-resort fallback — may struggle with tool calling",
|
|
70
|
+
speed: "~60 tok/s on M1",
|
|
71
|
+
quality: "limited",
|
|
72
|
+
},
|
|
73
73
|
];
|
|
74
74
|
function scoreModel(model, ramGB, vramGB) {
|
|
75
75
|
if (ramGB < model.ramRequired)
|
|
@@ -86,6 +86,23 @@ function scoreModel(model, ramGB, vramGB) {
|
|
|
86
86
|
}
|
|
87
87
|
const qualityOrder = { best: 3, great: 2, good: 1, limited: 0 };
|
|
88
88
|
const fitOrder = { perfect: 4, good: 3, tight: 2, skip: 1 };
|
|
89
|
+
function recommendationPriority(model) {
|
|
90
|
+
if (model.ollamaId === "qwen2.5-coder:14b")
|
|
91
|
+
return 100;
|
|
92
|
+
if (model.ollamaId === "deepseek-coder-v2:16b")
|
|
93
|
+
return 95;
|
|
94
|
+
if (model.ollamaId === "qwen2.5-coder:7b")
|
|
95
|
+
return 80;
|
|
96
|
+
if (model.ollamaId === "qwen2.5-coder:32b")
|
|
97
|
+
return 75;
|
|
98
|
+
if (model.ollamaId === "codellama:7b")
|
|
99
|
+
return 60;
|
|
100
|
+
if (model.ollamaId === "starcoder2:7b")
|
|
101
|
+
return 55;
|
|
102
|
+
if (model.ollamaId === "qwen2.5-coder:3b")
|
|
103
|
+
return 10;
|
|
104
|
+
return 0;
|
|
105
|
+
}
|
|
89
106
|
export function getRecommendations(hardware) {
|
|
90
107
|
const ramGB = hardware.ram / (1024 * 1024 * 1024);
|
|
91
108
|
const vramGB = hardware.gpu?.vram ? hardware.gpu.vram / (1024 * 1024 * 1024) : 0;
|
|
@@ -95,11 +112,14 @@ export function getRecommendations(hardware) {
|
|
|
95
112
|
...m,
|
|
96
113
|
fit: scoreModel(m, ramGB, effectiveVRAM),
|
|
97
114
|
}));
|
|
98
|
-
// Sort:
|
|
115
|
+
// Sort: fit first, then strongly prefer better coding defaults over tiny fallback models
|
|
99
116
|
scored.sort((a, b) => {
|
|
100
117
|
const fitDiff = (fitOrder[b.fit] ?? 0) - (fitOrder[a.fit] ?? 0);
|
|
101
118
|
if (fitDiff !== 0)
|
|
102
119
|
return fitDiff;
|
|
120
|
+
const priorityDiff = recommendationPriority(b) - recommendationPriority(a);
|
|
121
|
+
if (priorityDiff !== 0)
|
|
122
|
+
return priorityDiff;
|
|
103
123
|
return (qualityOrder[b.quality] ?? 0) - (qualityOrder[a.quality] ?? 0);
|
|
104
124
|
});
|
|
105
125
|
return scored;
|