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.
- package/package.json +1 -1
- package/scripts/loren.js +184 -69
package/package.json
CHANGED
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
|
|
28
|
-
|
|
29
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
350
|
-
|
|
351
|
-
|
|
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
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
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
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
535
|
+
printBanner();
|
|
533
536
|
if (envStatus.migrated) {
|
|
534
|
-
console.log(
|
|
537
|
+
console.log(`${MAGENTA}Your previous settings were imported automatically.${RESET}`);
|
|
535
538
|
} else if (envStatus.created) {
|
|
536
|
-
console.log(
|
|
539
|
+
console.log(`${GREEN}A fresh config is ready.${RESET}`);
|
|
537
540
|
}
|
|
538
|
-
console.log(
|
|
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
|
-
|
|
544
|
-
console.log(`Welcome back
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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);
|