loren-code 0.1.4 → 0.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.
package/README.md CHANGED
@@ -20,7 +20,13 @@ loren help
20
20
 
21
21
  ## First Run
22
22
 
23
- Loren creates `.env.local` automatically if it does not exist.
23
+ Loren stores user config in `%USERPROFILE%\.lorencode`.
24
+
25
+ On first run it creates:
26
+
27
+ ```text
28
+ C:\Users\<you>\.lorencode\.env.local
29
+ ```
24
30
 
25
31
  You must add valid `OLLAMA_API_KEYS` before the bridge can make upstream requests.
26
32
  If you configure multiple keys, Loren rotates them automatically.
@@ -107,11 +113,11 @@ npm.cmd install -g loren-code
107
113
 
108
114
  ### Missing API keys
109
115
 
110
- Populate `OLLAMA_API_KEYS` in `.env.local`.
116
+ Populate `OLLAMA_API_KEYS` in `%USERPROFILE%\.lorencode\.env.local`.
111
117
 
112
118
  ### Port already in use
113
119
 
114
- Change `BRIDGE_PORT` in `.env.local`.
120
+ Change `BRIDGE_PORT` in `%USERPROFILE%\.lorencode\.env.local`.
115
121
 
116
122
  ## Repository
117
123
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "loren-code",
3
- "version": "0.1.4",
3
+ "version": "0.2.0",
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",
@@ -33,6 +33,7 @@
33
33
  "scripts/claude-wrapper.js",
34
34
  "scripts/install-claude-ollama.ps1",
35
35
  "scripts/loren.js",
36
+ "scripts/postinstall.js",
36
37
  "scripts/uninstall-claude-ollama.ps1",
37
38
  "src/*.js",
38
39
  ".env.example",
@@ -57,6 +58,7 @@
57
58
  "check:publish": "node scripts/publish-check.js",
58
59
  "prepack": "node scripts/sync-readme.js npm",
59
60
  "postpack": "node scripts/sync-readme.js github",
61
+ "postinstall": "node scripts/postinstall.js",
60
62
  "prepublishOnly": "npm test && npm run lint",
61
63
  "test": "node scripts/publish-check.js",
62
64
  "lint": "node -e \"console.log('No linter configured')\""
@@ -5,18 +5,19 @@ import process from "node:process";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import { ensureEnvLocal, ensureRuntimeDir, getBridgeBaseUrl } from "../src/bootstrap.js";
7
7
  import { loadConfig } from "../src/config.js";
8
+ import { getEnvFilePath, getRuntimeDir } from "../src/paths.js";
8
9
 
9
10
  const __filename = fileURLToPath(import.meta.url);
10
11
  const __dirname = path.dirname(__filename);
11
12
  const repoRoot = path.resolve(__dirname, "..");
12
- const stateDir = path.join(repoRoot, ".runtime");
13
+ const stateDir = getRuntimeDir();
13
14
  const bridgePidPath = path.join(stateDir, "bridge.pid");
14
15
  const bridgeLogPath = path.join(stateDir, "bridge.log");
15
- const envFilePath = path.join(repoRoot, ".env.local");
16
+ const envFilePath = getEnvFilePath();
16
17
 
17
18
  async function main() {
18
19
  process.chdir(repoRoot);
19
- ensureRuntimeDir(repoRoot);
20
+ ensureRuntimeDir();
20
21
  ensureEnvLocal(repoRoot);
21
22
  const bridgeConfig = loadConfig();
22
23
  const bridgeBaseUrl = getBridgeBaseUrl(bridgeConfig);
@@ -3,13 +3,16 @@ $ErrorActionPreference = "Stop"
3
3
  $repoRoot = Split-Path -Parent $PSScriptRoot
4
4
  $userProfile = [Environment]::GetFolderPath("UserProfile")
5
5
  $appData = [Environment]::GetFolderPath("ApplicationData")
6
+ $lorenHome = if ($env:LOREN_HOME) { $env:LOREN_HOME } else { Join-Path $userProfile ".lorencode" }
6
7
  $workspaceSettingsDir = Join-Path $appData "Code\\User"
7
8
  $workspaceSettingsPath = Join-Path $workspaceSettingsDir "settings.json"
8
9
  $claudeDir = Join-Path $userProfile ".claude"
9
10
  $claudeSettingsPath = Join-Path $claudeDir "settings.json"
10
11
  $launcherSourcePath = Join-Path $repoRoot "scripts\\ClaudeWrapperLauncher.cs"
11
12
  $launcherExePath = Join-Path $repoRoot "scripts\\ClaudeWrapperLauncher.exe"
12
- $envPath = Join-Path $repoRoot ".env.local"
13
+ $envTemplatePath = Join-Path $repoRoot ".env.example"
14
+ $legacyEnvPath = Join-Path $repoRoot ".env.local"
15
+ $envPath = Join-Path $lorenHome ".env.local"
13
16
  $npmBinDir = Join-Path $appData "npm"
14
17
  $claudeCmdPath = Join-Path $npmBinDir "claude.cmd"
15
18
  $claudeShellPath = Join-Path $npmBinDir "claude"
@@ -18,14 +21,21 @@ $claudeCmdBackupPath = Join-Path $npmBinDir "claude.loren-backup.cmd"
18
21
  $claudeShellBackupPath = Join-Path $npmBinDir "claude.loren-backup"
19
22
  $claudePs1BackupPath = Join-Path $npmBinDir "claude.loren-backup.ps1"
20
23
 
21
- if (-not (Test-Path $envPath)) {
22
- throw ".env.local not found. Create it first with OLLAMA_API_KEYS."
23
- }
24
-
25
24
  New-Item -ItemType Directory -Force -Path $workspaceSettingsDir | Out-Null
26
25
  New-Item -ItemType Directory -Force -Path $claudeDir | Out-Null
26
+ New-Item -ItemType Directory -Force -Path $lorenHome | Out-Null
27
27
  New-Item -ItemType Directory -Force -Path $npmBinDir | Out-Null
28
28
 
29
+ if (-not (Test-Path $envPath)) {
30
+ if (Test-Path $legacyEnvPath) {
31
+ Copy-Item -LiteralPath $legacyEnvPath -Destination $envPath -Force
32
+ } elseif (Test-Path $envTemplatePath) {
33
+ Copy-Item -LiteralPath $envTemplatePath -Destination $envPath -Force
34
+ } else {
35
+ Set-Content -LiteralPath $envPath -Value "OLLAMA_API_KEYS=`nBRIDGE_HOST=127.0.0.1`nBRIDGE_PORT=8788`n" -Encoding UTF8
36
+ }
37
+ }
38
+
29
39
  function Read-JsonFile {
30
40
  param([string]$Path)
31
41
 
@@ -197,6 +207,10 @@ $bridgeBaseUrl = "http://${bridgeHost}:${bridgePort}"
197
207
  $workspaceSettings["claudeCode.claudeProcessWrapper"] = $launcherExePath
198
208
  $workspaceSettings["claudeCode.disableLoginPrompt"] = $true
199
209
  $workspaceSettings["claudeCode.environmentVariables"] = @(
210
+ @{
211
+ name = "LOREN_HOME"
212
+ value = $lorenHome
213
+ },
200
214
  @{
201
215
  name = "ANTHROPIC_BASE_URL"
202
216
  value = $bridgeBaseUrl
@@ -233,7 +247,17 @@ if ($availableModels.Count -eq 0) {
233
247
  throw "OLLAMA_MODEL_ALIASES does not contain any models"
234
248
  }
235
249
 
236
- $defaultModel = if ($aliases.ContainsKey("ollama-free-auto")) { "ollama-free-auto" } else { $availableModels[0] }
250
+ $configuredDefaultModel = Get-EnvValue -Path $envPath -Name "DEFAULT_MODEL_ALIAS"
251
+ if (
252
+ -not [string]::IsNullOrWhiteSpace($configuredDefaultModel) -and
253
+ $availableModels.Contains($configuredDefaultModel)
254
+ ) {
255
+ $defaultModel = $configuredDefaultModel
256
+ } elseif ($aliases.ContainsKey("ollama-free-auto")) {
257
+ $defaultModel = "ollama-free-auto"
258
+ } else {
259
+ $defaultModel = $availableModels[0]
260
+ }
237
261
  $claudeSettings["model"] = $defaultModel
238
262
  $claudeSettings["availableModels"] = $availableModels
239
263
  Write-JsonFile -Path $claudeSettingsPath -Data $claudeSettings
@@ -261,9 +285,11 @@ $ps1Content = @"
261
285
  Set-Content -LiteralPath $claudePs1Path -Value $ps1Content -Encoding UTF8
262
286
 
263
287
  Write-Host "Installation completed."
288
+ Write-Host "Loren home:" $lorenHome
264
289
  Write-Host "Claude launcher:" $launcherExePath
265
290
  Write-Host "VS Code user settings:" $workspaceSettingsPath
266
291
  Write-Host "Claude user settings:" $claudeSettingsPath
292
+ Write-Host "Loren config:" $envPath
267
293
  Write-Host "Global Claude command:" $claudeCmdPath
268
294
  Write-Host ""
269
295
  Write-Host "Restart VS Code. Claude Code will use the bridge in any project."
package/scripts/loren.js CHANGED
@@ -5,11 +5,13 @@ import { execFileSync, spawn } from "node:child_process";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import { loadConfig, loadEnvFile, saveEnvFile } from "../src/config.js";
7
7
  import { ensureEnvLocal, ensureRuntimeDir, getBridgeBaseUrl } from "../src/bootstrap.js";
8
+ import { getEnvFilePath, getLorenHome, getRuntimeDir } from "../src/paths.js";
8
9
 
9
10
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
10
11
  const projectRoot = path.resolve(__dirname, "..");
11
- const envFilePath = path.join(projectRoot, ".env.local");
12
- const runtimeDir = path.join(projectRoot, ".runtime");
12
+ const lorenHome = getLorenHome();
13
+ const envFilePath = getEnvFilePath();
14
+ const runtimeDir = getRuntimeDir();
13
15
  const pidFilePath = path.join(runtimeDir, "loren.pid");
14
16
  const logFilePath = path.join(runtimeDir, "bridge.log");
15
17
  const errorLogFilePath = path.join(runtimeDir, "bridge.err.log");
@@ -18,8 +20,8 @@ const claudeSettingsPath = path.join(userHome, ".claude", "settings.json");
18
20
 
19
21
  // Force working directory to project root for config loading
20
22
  process.chdir(projectRoot);
21
- ensureRuntimeDir(projectRoot);
22
- ensureEnvLocal(projectRoot);
23
+ const runtimePath = ensureRuntimeDir();
24
+ const envStatus = ensureEnvLocal(projectRoot);
23
25
 
24
26
  const ASCII_LOGO = `
25
27
  ██╗ ██████╗ ██████╗ ███████╗███╗ ██╗ ██████╗ ██████╗ ██████╗ ███████╗
@@ -56,9 +58,11 @@ const COMMANDS = {
56
58
  function main() {
57
59
  const args = process.argv.slice(2);
58
60
  const [command] = args;
61
+ const config = loadConfig();
59
62
 
60
63
  if (!command || command === "help" || command === "--help" || command === "-h") {
61
64
  printHelp();
65
+ maybePrintSetupHint(config);
62
66
  process.exit(0);
63
67
  }
64
68
 
@@ -336,6 +340,9 @@ function showConfig() {
336
340
 
337
341
  console.log("\nCurrent Configuration:");
338
342
  console.log("─".repeat(40));
343
+ console.log(` Home: ${lorenHome}`);
344
+ console.log(` Env File: ${envFilePath}`);
345
+ console.log(` Runtime: ${runtimePath}`);
339
346
  console.log(` Host: ${config.host}`);
340
347
  console.log(` Port: ${config.port}`);
341
348
  console.log(` Upstream: ${config.upstreamBaseUrl}`);
@@ -497,6 +504,29 @@ function syncClaudeSelectedModel(model) {
497
504
  fs.writeFileSync(claudeSettingsPath, `${JSON.stringify(settings, null, 2)}\n`, "utf8");
498
505
  }
499
506
 
507
+ function maybePrintSetupHint(config) {
508
+ if (!envStatus.created && config.apiKeys.length > 0) {
509
+ return;
510
+ }
511
+
512
+ console.log("Setup:");
513
+ console.log(` Loren home: ${lorenHome}`);
514
+ console.log(` Config file: ${envFilePath}`);
515
+
516
+ if (envStatus.migrated) {
517
+ console.log(" Existing configuration was migrated automatically.");
518
+ } else if (envStatus.created) {
519
+ console.log(" A new config file was created automatically.");
520
+ }
521
+
522
+ if (config.apiKeys.length === 0) {
523
+ console.log(" Add OLLAMA_API_KEYS to finish setup.");
524
+ }
525
+
526
+ console.log(" Then run: loren start");
527
+ console.log("");
528
+ }
529
+
500
530
  // ============== HELP ==============
501
531
 
502
532
  function printHelp() {
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env node
2
+ import { getEnvFilePath, getLorenHome } from "../src/paths.js";
3
+
4
+ console.log("");
5
+ console.log("Loren Code installed.");
6
+ console.log(`Loren home: ${getLorenHome()}`);
7
+ console.log(`Config file: ${getEnvFilePath()}`);
8
+ console.log("");
9
+ console.log("Next steps:");
10
+ console.log(" 1. Run: loren");
11
+ console.log(" 2. Add your OLLAMA_API_KEYS");
12
+ console.log(" 3. Start the bridge with: loren start");
13
+ console.log("");
14
+ console.log("Optional Windows Claude integration:");
15
+ console.log(" powershell -ExecutionPolicy Bypass -File \"$(npm prefix -g)\\node_modules\\loren-code\\scripts\\install-claude-ollama.ps1\"");
16
+ console.log("");
@@ -3,9 +3,10 @@ $ErrorActionPreference = "Stop"
3
3
  $repoRoot = Split-Path -Parent $PSScriptRoot
4
4
  $userProfile = [Environment]::GetFolderPath("UserProfile")
5
5
  $appData = [Environment]::GetFolderPath("ApplicationData")
6
+ $lorenHome = if ($env:LOREN_HOME) { $env:LOREN_HOME } else { Join-Path $userProfile ".lorencode" }
6
7
  $workspaceSettingsPath = Join-Path $appData "Code\\User\\settings.json"
7
8
  $claudeSettingsPath = Join-Path $userProfile ".claude\\settings.json"
8
- $bridgePidPath = Join-Path $repoRoot ".runtime\\bridge.pid"
9
+ $bridgePidPath = Join-Path $lorenHome "runtime\\bridge.pid"
9
10
  $launcherExePath = Join-Path $repoRoot "scripts\\ClaudeWrapperLauncher.exe"
10
11
  $npmBinDir = Join-Path $appData "npm"
11
12
  $claudeCmdPath = Join-Path $npmBinDir "claude.cmd"
package/src/bootstrap.js CHANGED
@@ -1,19 +1,30 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
+ import { getEnvFilePath, getLegacyEnvFilePath, getLorenHome, getRuntimeDir } from "./paths.js";
3
4
 
4
- export function ensureRuntimeDir(projectRoot) {
5
- fs.mkdirSync(path.join(projectRoot, ".runtime"), { recursive: true });
5
+ export function ensureRuntimeDir() {
6
+ fs.mkdirSync(getLorenHome(), { recursive: true });
7
+ fs.mkdirSync(getRuntimeDir(), { recursive: true });
8
+ return getRuntimeDir();
6
9
  }
7
10
 
8
11
  export function ensureEnvLocal(projectRoot, options = {}) {
9
- const envLocalPath = path.join(projectRoot, ".env.local");
12
+ const envLocalPath = getEnvFilePath();
13
+ const legacyEnvPath = getLegacyEnvFilePath(projectRoot);
10
14
  const envExamplePath = path.join(projectRoot, ".env.example");
11
15
  const logger = options.logger ?? console;
16
+ fs.mkdirSync(getLorenHome(), { recursive: true });
12
17
 
13
18
  if (fs.existsSync(envLocalPath)) {
14
19
  return { created: false, path: envLocalPath };
15
20
  }
16
21
 
22
+ if (fs.existsSync(legacyEnvPath) && legacyEnvPath !== envLocalPath) {
23
+ fs.copyFileSync(legacyEnvPath, envLocalPath);
24
+ logger.warn?.(`Migrated existing config from ${legacyEnvPath} to ${envLocalPath}.`);
25
+ return { created: true, migrated: true, path: envLocalPath };
26
+ }
27
+
17
28
  if (!fs.existsSync(envExamplePath)) {
18
29
  fs.writeFileSync(envLocalPath, "OLLAMA_API_KEYS=\nBRIDGE_HOST=127.0.0.1\nBRIDGE_PORT=8788\n", "utf8");
19
30
  logger.warn?.(`Created ${envLocalPath}. Add your Ollama API key(s) before starting the bridge.`);
package/src/config.js CHANGED
@@ -1,11 +1,11 @@
1
1
  import fs from "node:fs";
2
- import path from "node:path";
2
+ import { getEnvFilePath } from "./paths.js";
3
3
 
4
4
  const DEFAULT_PORT = 8788;
5
5
  const DEFAULT_HOST = "127.0.0.1";
6
6
  const DEFAULT_UPSTREAM = "https://ollama.com";
7
7
 
8
- export function loadEnvFile(filePath = path.join(process.cwd(), ".env.local")) {
8
+ export function loadEnvFile(filePath = getEnvFilePath()) {
9
9
  if (!fs.existsSync(filePath)) {
10
10
  return {};
11
11
  }
@@ -68,8 +68,8 @@ export function saveEnvFile(filePath, envVars) {
68
68
  fs.writeFileSync(filePath, `${lines}\n`, "utf8");
69
69
  }
70
70
 
71
- export function loadConfig() {
72
- const fileEnv = loadEnvFile();
71
+ export function loadConfig(filePath = getEnvFilePath()) {
72
+ const fileEnv = loadEnvFile(filePath);
73
73
  const getValue = (name, fallback = undefined) => {
74
74
  if (Object.prototype.hasOwnProperty.call(fileEnv, name)) {
75
75
  return fileEnv[name];
package/src/logger.js CHANGED
@@ -1,9 +1,10 @@
1
1
  import winston from 'winston';
2
+ import fs from 'node:fs';
2
3
  import path from 'node:path';
3
- import { fileURLToPath } from 'node:url';
4
+ import { getRuntimeDir } from './paths.js';
4
5
 
5
- const __filename = fileURLToPath(import.meta.url);
6
- const __dirname = path.dirname(__filename);
6
+ const runtimeDir = getRuntimeDir();
7
+ fs.mkdirSync(runtimeDir, { recursive: true });
7
8
 
8
9
  const logFormat = winston.format.combine(
9
10
  winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
@@ -24,23 +25,23 @@ const logger = winston.createLogger({
24
25
  )
25
26
  }),
26
27
  new winston.transports.File({
27
- filename: path.join(__dirname, '..', '.runtime', 'error.log'),
28
+ filename: path.join(runtimeDir, 'error.log'),
28
29
  level: 'error',
29
30
  maxsize: 5242880, // 5MB
30
31
  maxFiles: 5
31
32
  }),
32
33
  new winston.transports.File({
33
- filename: path.join(__dirname, '..', '.runtime', 'combined.log'),
34
+ filename: path.join(runtimeDir, 'combined.log'),
34
35
  maxsize: 5242880, // 5MB
35
36
  maxFiles: 5
36
37
  })
37
38
  ],
38
39
  exceptionHandlers: [
39
- new winston.transports.File({ filename: path.join(__dirname, '..', '.runtime', 'exceptions.log') })
40
+ new winston.transports.File({ filename: path.join(runtimeDir, 'exceptions.log') })
40
41
  ],
41
42
  rejectionHandlers: [
42
- new winston.transports.File({ filename: path.join(__dirname, '..', '.runtime', 'rejections.log') })
43
+ new winston.transports.File({ filename: path.join(runtimeDir, 'rejections.log') })
43
44
  ]
44
45
  });
45
46
 
46
- export default logger;
47
+ export default logger;
package/src/paths.js ADDED
@@ -0,0 +1,22 @@
1
+ import path from "node:path";
2
+ import process from "node:process";
3
+
4
+ export function getUserHomeDir() {
5
+ return process.env.USERPROFILE || process.env.HOME || process.cwd();
6
+ }
7
+
8
+ export function getLorenHome() {
9
+ return process.env.LOREN_HOME || path.join(getUserHomeDir(), ".lorencode");
10
+ }
11
+
12
+ export function getEnvFilePath() {
13
+ return path.join(getLorenHome(), ".env.local");
14
+ }
15
+
16
+ export function getRuntimeDir() {
17
+ return path.join(getLorenHome(), "runtime");
18
+ }
19
+
20
+ export function getLegacyEnvFilePath(projectRoot) {
21
+ return path.join(projectRoot, ".env.local");
22
+ }
package/src/server.js CHANGED
@@ -6,6 +6,7 @@ import path from "node:path";
6
6
  import { fileURLToPath } from "node:url";
7
7
  import { loadConfig } from "./config.js";
8
8
  import { ensureEnvLocal, ensureRuntimeDir } from "./bootstrap.js";
9
+ import { getEnvFilePath } from "./paths.js";
9
10
  import logger from "./logger.js";
10
11
  import { KeyManager } from "./key-manager.js";
11
12
  import { validateInput, MessageSchema, CountTokensSchema } from "./schemas.js";
@@ -20,12 +21,12 @@ const __filename = fileURLToPath(import.meta.url);
20
21
  const __dirname = path.dirname(__filename);
21
22
  const projectRoot = path.resolve(__dirname, "..");
22
23
 
23
- ensureRuntimeDir(projectRoot);
24
+ ensureRuntimeDir();
24
25
  ensureEnvLocal(projectRoot, { logger });
25
26
 
26
27
  let config = loadConfig();
27
28
  let keyManager = new KeyManager(config.apiKeys);
28
- const envFilePath = path.join(projectRoot, ".env.local");
29
+ const envFilePath = getEnvFilePath();
29
30
 
30
31
  function reloadRuntimeConfig() {
31
32
  config = loadConfig();
@@ -57,7 +58,7 @@ process.on('SIGTERM', () => {
57
58
  });
58
59
 
59
60
  if (!config.apiKeys.length) {
60
- logger.error('No Ollama API keys found. Set OLLAMA_API_KEYS or OLLAMA_API_KEY in the environment or .env.local.');
61
+ logger.error(`No Ollama API keys found. Set OLLAMA_API_KEYS or OLLAMA_API_KEY in the environment or ${envFilePath}.`);
61
62
  process.exit(1);
62
63
  }
63
64