loren-code 0.2.2 → 0.2.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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/scripts/loren.js +184 -69
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "loren-code",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
4
4
  "description": "Ollama Cloud Model Manager - Dynamic model switching, API key rotation, and real-time configuration updates",
5
5
  "author": "lorenzune",
6
6
  "license": "MIT",
package/scripts/loren.js CHANGED
@@ -19,15 +19,34 @@ const logFilePath = path.join(runtimeDir, "bridge.log");
19
19
  const errorLogFilePath = path.join(runtimeDir, "bridge.err.log");
20
20
  const userHome = process.env.USERPROFILE || process.env.HOME || projectRoot;
21
21
  const claudeSettingsPath = path.join(userHome, ".claude", "settings.json");
22
+ const displayName = getDisplayName();
22
23
 
23
24
  process.chdir(projectRoot);
24
25
  ensureRuntimeDir();
25
26
  const envStatus = ensureEnvLocal(projectRoot, { logger: { warn() {} } });
26
27
 
27
- const BANNER = `
28
- LOREN CODE
29
- Smarter bridge, fewer rituals.
30
- `;
28
+ const ASCII_BANNER_LINES = [
29
+ "██╗ ██████╗ ██████╗ ███████╗███╗ ██╗ ██████╗ ██████╗ ██████╗ ███████╗",
30
+ "██║ ██╔═══██╗██╔══██╗██╔════╝████╗ ██║ ██╔════╝██╔═══██╗██╔══██╗██╔════╝",
31
+ "██║ ██║ ██║██████╔╝█████╗ ██╔██╗ ██║ ██║ ██║ ██║██║ ██║█████╗",
32
+ "██║ ██║ ██║██╔══██╗██╔══╝ ██║╚██╗██║ ██║ ██║ ██║██║ ██║██╔══╝",
33
+ "███████╗╚██████╔╝██║ ██║███████╗██║ ╚████║ ╚██████╗╚██████╔╝██████╔╝███████╗",
34
+ "╚══════╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝╚═╝ ╚═══╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝",
35
+ ];
36
+ const GREEN = "\x1b[32m";
37
+ const YELLOW = "\x1b[33m";
38
+ const RED = "\x1b[31m";
39
+ const CYAN = "\x1b[36m";
40
+ const MAGENTA = "\x1b[35m";
41
+ const RESET = "\x1b[0m";
42
+ const BANNER_COLORS = [
43
+ "\x1b[38;2;198;218;255m",
44
+ "\x1b[38;2;190;212;255m",
45
+ "\x1b[38;2;181;206;255m",
46
+ "\x1b[38;2;188;201;255m",
47
+ "\x1b[38;2;196;197;255m",
48
+ "\x1b[38;2;205;193;255m",
49
+ ];
31
50
 
32
51
  const COMMANDS = {
33
52
  model: {
@@ -101,25 +120,8 @@ async function main() {
101
120
  }
102
121
 
103
122
  async function listModels() {
104
- const config = loadConfig();
105
-
106
123
  try {
107
- const response = await fetch(`${config.upstreamBaseUrl}/api/tags`, {
108
- headers: { accept: "application/json" },
109
- });
110
-
111
- if (!response.ok) {
112
- throw new Error(`HTTP ${response.status}: ${response.statusText}`);
113
- }
114
-
115
- const data = await response.json();
116
- let models = Array.isArray(data.models) ? data.models : [];
117
-
118
- models = models.sort((a, b) => {
119
- const dateA = a.modified_at ? new Date(a.modified_at).getTime() : 0;
120
- const dateB = b.modified_at ? new Date(b.modified_at).getTime() : 0;
121
- return dateB - dateA;
122
- });
124
+ const { config, models } = await fetchAvailableModels();
123
125
 
124
126
  console.log("\nAvailable models from Ollama Cloud:");
125
127
  console.log("─".repeat(70));
@@ -202,12 +204,7 @@ function setModel(args) {
202
204
  saveEnvFile(envFilePath, envVars);
203
205
  syncClaudeSelectedModel(requestedModel);
204
206
 
205
- console.log(`\nDefault model set to ${requestedModel}.`);
206
- console.log("Fresh requests will use it right away.");
207
- if (fs.existsSync(claudeSettingsPath)) {
208
- console.log("Claude Code settings were updated too.");
209
- }
210
- console.log("");
207
+ return requestedModel;
211
208
  }
212
209
 
213
210
  function showCurrentModel() {
@@ -342,13 +339,16 @@ function showPaths() {
342
339
  console.log("");
343
340
  }
344
341
 
345
- function startServer() {
342
+ function startServer(options = {}) {
343
+ const quiet = options.quiet === true;
346
344
  const existingPid = readPidFile();
347
345
  if (existingPid && isProcessRunning(existingPid)) {
348
346
  const config = loadConfig();
349
- console.log("\nLoren is already running.");
350
- console.log(`URL: ${getBridgeBaseUrl(config)}`);
351
- console.log("");
347
+ if (!quiet) {
348
+ console.log("\nLoren is already running.");
349
+ console.log(`URL: ${getBridgeBaseUrl(config)}`);
350
+ console.log("");
351
+ }
352
352
  return;
353
353
  }
354
354
 
@@ -364,10 +364,12 @@ function startServer() {
364
364
  child.unref();
365
365
  fs.writeFileSync(pidFilePath, `${child.pid}\n`, "utf8");
366
366
 
367
- const config = loadConfig();
368
- console.log("\nLoren is up and listening.");
369
- console.log(`URL: ${getBridgeBaseUrl(config)}`);
370
- console.log("");
367
+ if (!quiet) {
368
+ const config = loadConfig();
369
+ console.log("\nLoren is up and listening.");
370
+ console.log(`URL: ${getBridgeBaseUrl(config)}`);
371
+ console.log("");
372
+ }
371
373
  }
372
374
 
373
375
  function stopServer() {
@@ -495,33 +497,34 @@ async function runSetupWizard(config) {
495
497
  });
496
498
 
497
499
  try {
498
- const rawKeys = (await rl.question("Paste your Ollama API key(s), separated by commas: ")).trim();
499
-
500
- if (rawKeys) {
501
- const keys = splitKeyList(rawKeys);
502
- const envVars = loadEnvFile(envFilePath);
503
- envVars.OLLAMA_API_KEYS = keys.join(",");
504
- saveEnvFile(envFilePath, envVars);
505
- console.log(`\nNice. Loren is holding ${keys.length} key(s) and feeling organized.`);
506
- } else {
507
- console.log("\nNo keys yet. Loren will wait here and act casual about it.");
508
- }
509
-
510
- const startNow = (await rl.question("Start the bridge now? [Y/n] ")).trim().toLowerCase();
511
- if (startNow === "" || startNow === "y" || startNow === "yes") {
512
- startServer();
513
- }
500
+ const keys = await promptForApiKeys(rl);
501
+ const envVars = loadEnvFile(envFilePath);
502
+ envVars.OLLAMA_API_KEYS = keys.join(",");
503
+ saveEnvFile(envFilePath, envVars);
504
+ console.log(`${GREEN}✓ Saved ${keys.length} API key(s).${RESET}`);
505
+ console.log("");
514
506
 
515
507
  if (process.platform === "win32") {
516
- const installClaude = (await rl.question("Install Claude Code integration too? [y/N] ")).trim().toLowerCase();
517
- if (installClaude === "y" || installClaude === "yes") {
508
+ const installClaude = (await askQuestion(rl, "Install Claude Code integration too? [Y/n] ")).trim().toLowerCase();
509
+ if (installClaude === "" || installClaude === "y" || installClaude === "yes") {
518
510
  installClaudeIntegration();
511
+ console.log(`${GREEN}✓ Claude Code integration installed.${RESET}`);
512
+ console.log("");
519
513
  } else {
520
514
  console.log("\nNo problem. You can wire Claude in later.");
521
515
  }
522
516
  }
523
517
 
524
- console.log("Setup complete. Fewer steps, fewer goblins.");
518
+ await promptForModelSelection(rl);
519
+
520
+ const startNow = (await askQuestion(rl, "Start the bridge now? [Y/n] ")).trim().toLowerCase();
521
+ if (startNow === "" || startNow === "y" || startNow === "yes") {
522
+ startServer({ quiet: true });
523
+ console.log(`${GREEN}✓ Bridge started.${RESET}`);
524
+ console.log("");
525
+ }
526
+
527
+ console.log(`${GREEN}Setup complete. Fewer steps, fewer goblins.${RESET}`);
525
528
  console.log("");
526
529
  } finally {
527
530
  rl.close();
@@ -529,35 +532,39 @@ async function runSetupWizard(config) {
529
532
  }
530
533
 
531
534
  function printWizardIntro() {
532
- console.log(BANNER);
535
+ printBanner();
533
536
  if (envStatus.migrated) {
534
- console.log("Your previous settings were imported automatically.");
537
+ console.log(`${MAGENTA}Your previous settings were imported automatically.${RESET}`);
535
538
  } else if (envStatus.created) {
536
- console.log("A fresh config is ready.");
539
+ console.log(`${GREEN}A fresh config is ready.${RESET}`);
537
540
  }
538
- console.log("Let's get Loren ready in one quick pass.");
541
+ console.log(`${CYAN}Welcome${displayName ? `, ${displayName}` : ""}.${RESET}`);
542
+ console.log(`${YELLOW}Run \`loren\` in an interactive terminal to finish setup.${RESET}`);
543
+ console.log(`${GREEN}Let's get Loren ready in one quick pass.${RESET}`);
539
544
  console.log("");
540
545
  }
541
546
 
542
547
  function printWelcomeBack(config) {
543
- console.log(BANNER);
544
- console.log(`Welcome back. ${config.apiKeys.length} key(s) loaded.`);
548
+ printBanner();
549
+ console.log(`Welcome back${displayName ? `, ${displayName}` : ""}.`);
550
+ console.log(`${config.apiKeys.length} key(s) loaded.`);
545
551
  console.log(`Current default model: ${config.defaultModel}`);
552
+ console.log(`${GREEN}Run \`loren start\` to launch the bridge.${RESET}`);
546
553
  console.log("");
547
- console.log("Useful commands:");
548
- console.log(" loren start");
549
- console.log(" loren model:list");
550
- console.log(" loren config:show");
551
- console.log("");
554
+ printCommandSummary();
552
555
  }
553
556
 
554
557
  function printQuickSetup(config) {
555
558
  if (config.apiKeys.length > 0) {
556
- console.log("Run `loren start` to launch the bridge.");
557
- console.log("");
559
+ printWelcomeBack(config);
558
560
  return;
559
561
  }
560
562
 
563
+ printBanner();
564
+ console.log(`Welcome${displayName ? `, ${displayName}` : ""}.`);
565
+ console.log(`${YELLOW}Run \`loren\` in an interactive terminal to finish setup.${RESET}`);
566
+ console.log("");
567
+ printCommandSummary();
561
568
  console.log("Quick start:");
562
569
  console.log(" 1. Run `loren` in an interactive terminal");
563
570
  console.log(" 2. Add your Ollama API key(s)");
@@ -577,8 +584,94 @@ function installClaudeIntegration() {
577
584
  }
578
585
  }
579
586
 
587
+ async function promptForApiKeys(rl) {
588
+ while (true) {
589
+ const rawKeys = (await askQuestion(rl, "Paste your Ollama API key(s), separated by commas: ")).trim();
590
+ const keys = splitKeyList(rawKeys);
591
+
592
+ if (keys.length > 0) {
593
+ return keys;
594
+ }
595
+
596
+ console.log(`${RED}At least one API key is required to continue.${RESET}`);
597
+ console.log("");
598
+ }
599
+ }
600
+
601
+ async function promptForModelSelection(rl) {
602
+ try {
603
+ const { models } = await fetchAvailableModels();
604
+
605
+ console.log("Available models:");
606
+ console.log("─".repeat(70));
607
+ for (const model of models) {
608
+ const modelId = model.model || model.name;
609
+ const size = formatSize(model.size);
610
+ const modified = model.modified_at ? new Date(model.modified_at).toLocaleDateString() : "unknown";
611
+ console.log(`${modelId.padEnd(30)}${size.padStart(12)}${modified.padStart(12)}`);
612
+ }
613
+ console.log("");
614
+
615
+ while (true) {
616
+ const requestedModel = (await askQuestion(rl, "Choose the default model: ")).trim();
617
+ const match = models.find((model) => (model.model || model.name) === requestedModel);
618
+
619
+ if (!requestedModel) {
620
+ console.log(`${RED}Please choose a model from the list above.${RESET}`);
621
+ console.log("");
622
+ continue;
623
+ }
624
+
625
+ if (!match) {
626
+ console.log(`${RED}That model is not in the current list.${RESET}`);
627
+ console.log("");
628
+ continue;
629
+ }
630
+
631
+ const model = setModel([requestedModel]);
632
+ console.log(`${GREEN}✓ Default model set to ${model}.${RESET}`);
633
+ console.log("");
634
+ return;
635
+ }
636
+ } catch (error) {
637
+ console.log(`${RED}Couldn't load models right now: ${error.message}${RESET}`);
638
+ console.log(`${RED}Please fix your keys and run \`loren model:list\` after setup.${RESET}`);
639
+ console.log("");
640
+ throw error;
641
+ }
642
+ }
643
+
644
+ async function fetchAvailableModels() {
645
+ const config = loadConfig();
646
+ const response = await fetch(`${config.upstreamBaseUrl}/api/tags`, {
647
+ headers: { accept: "application/json" },
648
+ });
649
+
650
+ if (!response.ok) {
651
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
652
+ }
653
+
654
+ const data = await response.json();
655
+ let models = Array.isArray(data.models) ? data.models : [];
656
+ models = models.sort((a, b) => {
657
+ const dateA = a.modified_at ? new Date(a.modified_at).getTime() : 0;
658
+ const dateB = b.modified_at ? new Date(b.modified_at).getTime() : 0;
659
+ return dateB - dateA;
660
+ });
661
+
662
+ return { config, models };
663
+ }
664
+
665
+ function askQuestion(rl, prompt) {
666
+ return rl.question(`${CYAN}${prompt}${RESET}`);
667
+ }
668
+
580
669
  function printHelp() {
581
- console.log(BANNER);
670
+ printBanner();
671
+ printCommandSummary();
672
+ }
673
+
674
+ function printCommandSummary() {
582
675
  console.log("Commands:");
583
676
  console.log(" loren setup Run the setup wizard");
584
677
  console.log(" loren start Start the bridge");
@@ -602,6 +695,28 @@ function printHelp() {
602
695
  console.log("");
603
696
  }
604
697
 
698
+ function getDisplayName() {
699
+ const explicit = process.env.USERNAME || process.env.USER;
700
+ if (explicit && explicit.trim()) {
701
+ return explicit.trim();
702
+ }
703
+
704
+ const baseName = path.basename(userHome || "").trim();
705
+ return baseName || "";
706
+ }
707
+
708
+ function printBanner() {
709
+ const coloredBanner = ASCII_BANNER_LINES
710
+ .map((line, index) => `${BANNER_COLORS[index] || ""}${line}${RESET}`)
711
+ .join("\n");
712
+
713
+ console.log(coloredBanner);
714
+ console.log("");
715
+ console.log(`${CYAN}LOREN CODE${RESET}`);
716
+ console.log("Smarter bridge, fewer rituals.");
717
+ console.log("");
718
+ }
719
+
605
720
  main().catch((error) => {
606
721
  console.error(error instanceof Error ? error.message : String(error));
607
722
  process.exit(1);