loren-code 0.1.5 → 0.2.1

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.5",
3
+ "version": "0.2.1",
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
@@ -271,10 +285,8 @@ $ps1Content = @"
271
285
  Set-Content -LiteralPath $claudePs1Path -Value $ps1Content -Encoding UTF8
272
286
 
273
287
  Write-Host "Installation completed."
274
- Write-Host "Claude launcher:" $launcherExePath
275
- Write-Host "VS Code user settings:" $workspaceSettingsPath
276
- Write-Host "Claude user settings:" $claudeSettingsPath
277
- Write-Host "Global Claude command:" $claudeCmdPath
278
288
  Write-Host ""
279
- Write-Host "Restart VS Code. Claude Code will use the bridge in any project."
280
- Write-Host "The global 'claude' command now routes through Loren."
289
+ Write-Host "Claude Code is now wired to Loren."
290
+ Write-Host "Restart VS Code and open a fresh chat."
291
+ Write-Host "The global 'claude' command now goes through Loren too."
292
+ Write-Host "Tiny goblins have been escorted away from the terminal."
package/scripts/loren.js CHANGED
@@ -1,33 +1,32 @@
1
1
  #!/usr/bin/env node
2
2
  import fs from "node:fs";
3
3
  import path from "node:path";
4
+ import process from "node:process";
4
5
  import { execFileSync, spawn } from "node:child_process";
6
+ import { createInterface } from "node:readline/promises";
5
7
  import { fileURLToPath } from "node:url";
6
8
  import { loadConfig, loadEnvFile, saveEnvFile } from "../src/config.js";
7
9
  import { ensureEnvLocal, ensureRuntimeDir, getBridgeBaseUrl } from "../src/bootstrap.js";
10
+ import { getEnvFilePath, getLorenHome, getRuntimeDir } from "../src/paths.js";
8
11
 
9
12
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
10
13
  const projectRoot = path.resolve(__dirname, "..");
11
- const envFilePath = path.join(projectRoot, ".env.local");
12
- const runtimeDir = path.join(projectRoot, ".runtime");
14
+ const lorenHome = getLorenHome();
15
+ const envFilePath = getEnvFilePath();
16
+ const runtimeDir = getRuntimeDir();
13
17
  const pidFilePath = path.join(runtimeDir, "loren.pid");
14
18
  const logFilePath = path.join(runtimeDir, "bridge.log");
15
19
  const errorLogFilePath = path.join(runtimeDir, "bridge.err.log");
16
20
  const userHome = process.env.USERPROFILE || process.env.HOME || projectRoot;
17
21
  const claudeSettingsPath = path.join(userHome, ".claude", "settings.json");
18
22
 
19
- // Force working directory to project root for config loading
20
23
  process.chdir(projectRoot);
21
- ensureRuntimeDir(projectRoot);
22
- ensureEnvLocal(projectRoot);
23
-
24
- const ASCII_LOGO = `
25
- ██╗ ██████╗ ██████╗ ███████╗███╗ ██╗ ██████╗ ██████╗ ██████╗ ███████╗
26
- ██║ ██╔═══██╗██╔══██╗██╔════╝████╗ ██║ ██╔════╝██╔═══██╗██╔══██╗██╔════╝
27
- ██║ ██║ ██║██████╔╝█████╗ ██╔██╗ ██║ ██║ ██║ ██║██║ ██║█████╗
28
- ██║ ██║ ██║██╔══██╗██╔══╝ ██║╚██╗██║ ██║ ██║ ██║██║ ██║██╔══╝
29
- ███████╗╚██████╔╝██║ ██║███████╗██║ ╚████║ ╚██████╗╚██████╔╝██████╔╝███████╗
30
- ╚══════╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝╚═╝ ╚═══╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚══════
24
+ ensureRuntimeDir();
25
+ const envStatus = ensureEnvLocal(projectRoot, { logger: { warn() {} } });
26
+
27
+ const BANNER = `
28
+ LOREN CODE
29
+ Smarter bridge, fewer rituals.
31
30
  `;
32
31
 
33
32
  const COMMANDS = {
@@ -45,6 +44,7 @@ const COMMANDS = {
45
44
  },
46
45
  config: {
47
46
  show: showConfig,
47
+ paths: showPaths,
48
48
  },
49
49
  server: {
50
50
  start: startServer,
@@ -53,16 +53,25 @@ const COMMANDS = {
53
53
  },
54
54
  };
55
55
 
56
- function main() {
56
+ async function main() {
57
57
  const args = process.argv.slice(2);
58
58
  const [command] = args;
59
+ const config = loadConfig();
60
+
61
+ if (!command) {
62
+ await runSetupWizard(config);
63
+ return;
64
+ }
59
65
 
60
- if (!command || command === "help" || command === "--help" || command === "-h") {
66
+ if (command === "help" || command === "--help" || command === "-h") {
61
67
  printHelp();
62
- process.exit(0);
68
+ return;
63
69
  }
64
70
 
65
- const [category, action] = command.split(":");
71
+ if (command === "setup") {
72
+ await runSetupWizard(config);
73
+ return;
74
+ }
66
75
 
67
76
  if (command === "start") {
68
77
  startServer();
@@ -79,24 +88,24 @@ function main() {
79
88
  return;
80
89
  }
81
90
 
91
+ const [category, action] = command.split(":");
82
92
  if (category && action && COMMANDS[category] && COMMANDS[category][action]) {
83
- COMMANDS[category][action](args.slice(1));
93
+ await COMMANDS[category][action](args.slice(1));
84
94
  return;
85
95
  }
86
96
 
87
97
  console.error(`Unknown command: ${command}`);
88
- printHelp();
98
+ console.log("");
99
+ console.log("Run `loren help` if the command goblin struck again.");
89
100
  process.exit(1);
90
101
  }
91
102
 
92
- // ============== MODEL COMMANDS ==============
93
-
94
103
  async function listModels() {
95
104
  const config = loadConfig();
96
105
 
97
106
  try {
98
107
  const response = await fetch(`${config.upstreamBaseUrl}/api/tags`, {
99
- headers: { "accept": "application/json" },
108
+ headers: { accept: "application/json" },
100
109
  });
101
110
 
102
111
  if (!response.ok) {
@@ -106,7 +115,6 @@ async function listModels() {
106
115
  const data = await response.json();
107
116
  let models = Array.isArray(data.models) ? data.models : [];
108
117
 
109
- // Sort by modified date (most recent first)
110
118
  models = models.sort((a, b) => {
111
119
  const dateA = a.modified_at ? new Date(a.modified_at).getTime() : 0;
112
120
  const dateB = b.modified_at ? new Date(b.modified_at).getTime() : 0;
@@ -123,9 +131,7 @@ async function listModels() {
123
131
  const size = formatSize(model.size);
124
132
  const modified = model.modified_at ? new Date(model.modified_at).toLocaleDateString() : "unknown";
125
133
  const marker = modelId === config.defaultModel ? "●" : "○";
126
- console.log(
127
- `${marker} ${modelId.padEnd(28)}${size.padStart(12)}${modified.padStart(12)}`
128
- );
134
+ console.log(`${marker} ${modelId.padEnd(28)}${size.padStart(12)}${modified.padStart(12)}`);
129
135
  }
130
136
 
131
137
  console.log("");
@@ -139,21 +145,24 @@ async function listModels() {
139
145
  }
140
146
 
141
147
  function formatSize(bytes) {
142
- if (!bytes) return "unknown";
148
+ if (!bytes) {
149
+ return "unknown";
150
+ }
151
+
143
152
  const gb = bytes / (1024 ** 3);
144
153
  return `${gb.toFixed(1)} GB`;
145
154
  }
146
155
 
147
156
  async function refreshModels() {
148
157
  const config = loadConfig();
149
- const url = `http://${config.host}:${config.port}/v1/refresh`;
158
+ const url = `${getBridgeBaseUrl(config)}/v1/refresh`;
150
159
 
151
- console.log(`Sending refresh request to ${url}...`);
160
+ console.log("Refreshing the model list...");
152
161
 
153
162
  try {
154
163
  const response = await fetch(url, {
155
164
  method: "POST",
156
- headers: { "accept": "application/json" },
165
+ headers: { accept: "application/json" },
157
166
  });
158
167
 
159
168
  if (!response.ok) {
@@ -163,12 +172,11 @@ async function refreshModels() {
163
172
  const data = await response.json();
164
173
  const models = Array.isArray(data.data) ? data.data : [];
165
174
 
166
- console.log("\n✓ Models refreshed successfully!");
167
- console.log(` Fetched ${models.length} model(s) from Ollama Cloud.`);
175
+ console.log(`\nDone. Fetched ${models.length} model(s).`);
168
176
  console.log("");
169
177
  } catch (error) {
170
178
  console.error(`Error refreshing models: ${error.message}`);
171
- console.error("Make sure the server is running: loren start");
179
+ console.error("Tip: start the bridge first with `loren start`.");
172
180
  process.exit(1);
173
181
  }
174
182
  }
@@ -177,31 +185,27 @@ function setModel(args) {
177
185
  const requestedModel = args.join(" ").trim();
178
186
 
179
187
  if (!requestedModel) {
180
- console.error("Error: Specify a model name.");
188
+ console.error("Please specify a model name.");
181
189
  console.error("Example: loren model:set qwen3.5:397b");
182
190
  process.exit(1);
183
191
  }
184
192
 
185
193
  const config = loadConfig();
186
-
187
- // Check if it's a valid alias or add it as a new direct model
188
194
  const isValidAlias = Object.keys(config.aliases).includes(requestedModel);
189
195
 
190
196
  if (!isValidAlias) {
191
- console.warn(`Warning: '${requestedModel}' is not a configured alias.`);
192
- console.warn("It will be used as a direct model name.");
197
+ console.warn(`Using '${requestedModel}' as a direct model name.`);
193
198
  }
194
199
 
195
- // Update .env.local with new DEFAULT_MODEL_ALIAS
196
200
  const envVars = loadEnvFile(envFilePath);
197
201
  envVars.DEFAULT_MODEL_ALIAS = requestedModel;
198
202
  saveEnvFile(envFilePath, envVars);
199
203
  syncClaudeSelectedModel(requestedModel);
200
204
 
201
- console.log(`\n✓ Default model set to: ${requestedModel}`);
202
- console.log(" New requests will use this model immediately.");
205
+ console.log(`\nDefault model set to ${requestedModel}.`);
206
+ console.log("Fresh requests will use it right away.");
203
207
  if (fs.existsSync(claudeSettingsPath)) {
204
- console.log(" Claude Code settings were updated as well.");
208
+ console.log("Claude Code settings were updated too.");
205
209
  }
206
210
  console.log("");
207
211
  }
@@ -212,18 +216,16 @@ function showCurrentModel() {
212
216
  console.log("");
213
217
  }
214
218
 
215
- // ============== API KEY COMMANDS ==============
216
-
217
219
  function listKeys() {
218
220
  const config = loadConfig();
219
221
 
220
- console.log("\nConfigured API Keys:");
222
+ console.log("\nConfigured API keys:");
221
223
  console.log("─".repeat(40));
222
224
 
223
225
  if (config.apiKeys.length === 0) {
224
- console.log(" (none configured)");
226
+ console.log(" none yet");
225
227
  } else {
226
- for (let i = 0; i < config.apiKeys.length; i++) {
228
+ for (let i = 0; i < config.apiKeys.length; i += 1) {
227
229
  const key = config.apiKeys[i];
228
230
  const masked = `${key.slice(0, 4)}...${key.slice(-4)}`;
229
231
  const marker = i === 0 ? "●" : "○";
@@ -232,27 +234,22 @@ function listKeys() {
232
234
  }
233
235
 
234
236
  console.log("");
235
- console.log(`Total: ${config.apiKeys.length} key(s)`);
236
- console.log("");
237
237
  }
238
238
 
239
239
  function addKey(args) {
240
240
  const newKey = args.join(" ").trim();
241
241
 
242
242
  if (!newKey) {
243
- console.error("Error: Specify an API key.");
243
+ console.error("Please specify an API key.");
244
244
  console.error("Example: loren keys:add sk-your-key-here");
245
245
  process.exit(1);
246
246
  }
247
247
 
248
248
  const envVars = loadEnvFile(envFilePath);
249
- const existingKeys = (envVars.OLLAMA_API_KEYS || "")
250
- .split(/[,\r?\n]+/)
251
- .map((k) => k.trim())
252
- .filter(Boolean);
249
+ const existingKeys = splitKeyList(envVars.OLLAMA_API_KEYS);
253
250
 
254
251
  if (existingKeys.includes(newKey)) {
255
- console.log(" Key already exists, skipping.");
252
+ console.log("That key is already there. Loren noticed before I did.");
256
253
  return;
257
254
  }
258
255
 
@@ -260,9 +257,7 @@ function addKey(args) {
260
257
  envVars.OLLAMA_API_KEYS = existingKeys.join(",");
261
258
  saveEnvFile(envFilePath, envVars);
262
259
 
263
- console.log(`\n✓ API key added.`);
264
- console.log(` Total keys: ${existingKeys.length}`);
265
- console.log(" New key will be used for subsequent requests.");
260
+ console.log(`\nKey added. Total keys: ${existingKeys.length}`);
266
261
  console.log("");
267
262
  }
268
263
 
@@ -270,71 +265,64 @@ function removeKey(args) {
270
265
  const indexOrKey = args.join(" ").trim();
271
266
 
272
267
  if (!indexOrKey) {
273
- console.error("Error: Specify key index or the key itself.");
268
+ console.error("Please specify a key index or the full key.");
274
269
  console.error("Example: loren keys:remove 0");
275
- console.error(" loren keys:remove sk-xxx...");
276
270
  process.exit(1);
277
271
  }
278
272
 
279
273
  const envVars = loadEnvFile(envFilePath);
280
- let existingKeys = (envVars.OLLAMA_API_KEYS || "")
281
- .split(/[,\r?\n]+/)
282
- .map((k) => k.trim())
283
- .filter(Boolean);
274
+ let existingKeys = splitKeyList(envVars.OLLAMA_API_KEYS);
284
275
 
285
276
  let keyToRemove;
286
- const index = parseInt(indexOrKey, 10);
287
-
288
- if (!isNaN(index) && index >= 0 && index < existingKeys.length) {
277
+ const index = Number.parseInt(indexOrKey, 10);
278
+ if (!Number.isNaN(index) && index >= 0 && index < existingKeys.length) {
289
279
  keyToRemove = existingKeys[index];
290
280
  } else {
291
- keyToRemove = existingKeys.find((k) => k === indexOrKey);
281
+ keyToRemove = existingKeys.find((key) => key === indexOrKey);
292
282
  }
293
283
 
294
284
  if (!keyToRemove) {
295
- console.error("Error: Key not found.");
285
+ console.error("Key not found.");
296
286
  process.exit(1);
297
287
  }
298
288
 
299
- existingKeys = existingKeys.filter((k) => k !== keyToRemove);
289
+ existingKeys = existingKeys.filter((key) => key !== keyToRemove);
300
290
  envVars.OLLAMA_API_KEYS = existingKeys.join(",");
301
291
  saveEnvFile(envFilePath, envVars);
302
292
 
303
- console.log(`\n✓ API key removed.`);
304
- console.log(` Remaining keys: ${existingKeys.length}`);
293
+ console.log(`\nKey removed. Remaining keys: ${existingKeys.length}`);
305
294
  console.log("");
306
295
  }
307
296
 
308
- function rotateKeys(args) {
297
+ function rotateKeys() {
309
298
  const envVars = loadEnvFile(envFilePath);
310
- let existingKeys = (envVars.OLLAMA_API_KEYS || "")
311
- .split(/[,\r?\n]+/)
312
- .map((k) => k.trim())
313
- .filter(Boolean);
299
+ let existingKeys = splitKeyList(envVars.OLLAMA_API_KEYS);
314
300
 
315
301
  if (existingKeys.length < 2) {
316
- console.log("Need at least 2 keys to rotate.");
302
+ console.log("You need at least two keys to rotate.");
317
303
  return;
318
304
  }
319
305
 
320
- // Move first key to the end
321
306
  const [first, ...rest] = existingKeys;
322
307
  existingKeys = [...rest, first];
323
-
324
308
  envVars.OLLAMA_API_KEYS = existingKeys.join(",");
325
309
  saveEnvFile(envFilePath, envVars);
326
310
 
327
- console.log("\n✓ API keys rotated.");
328
- console.log(" First key moved to end of list.");
311
+ console.log("\nKeys rotated. The first one took a well-earned break.");
329
312
  console.log("");
330
313
  }
331
314
 
332
- // ============== CONFIG COMMANDS ==============
315
+ function splitKeyList(raw = "") {
316
+ return raw
317
+ .split(/[,\r?\n]+/)
318
+ .map((entry) => entry.trim())
319
+ .filter(Boolean);
320
+ }
333
321
 
334
322
  function showConfig() {
335
323
  const config = loadConfig();
336
324
 
337
- console.log("\nCurrent Configuration:");
325
+ console.log("\nCurrent configuration:");
338
326
  console.log("─".repeat(40));
339
327
  console.log(` Host: ${config.host}`);
340
328
  console.log(` Port: ${config.port}`);
@@ -345,73 +333,70 @@ function showConfig() {
345
333
  console.log("");
346
334
  }
347
335
 
348
- // ============== SERVER COMMANDS ==============
336
+ function showPaths() {
337
+ console.log("\nLoren paths:");
338
+ console.log("─".repeat(40));
339
+ console.log(` Home: ${lorenHome}`);
340
+ console.log(` Config: ${envFilePath}`);
341
+ console.log(` Runtime: ${runtimeDir}`);
342
+ console.log("");
343
+ }
349
344
 
350
345
  function startServer() {
351
346
  const existingPid = readPidFile();
352
347
  if (existingPid && isProcessRunning(existingPid)) {
353
348
  const config = loadConfig();
354
- console.log(`\nLoren server is already running (PID ${existingPid}).`);
355
- console.log(` URL: ${getBridgeBaseUrl(config)}`);
349
+ console.log("\nLoren is already running.");
350
+ console.log(`URL: ${getBridgeBaseUrl(config)}`);
356
351
  console.log("");
357
352
  return;
358
353
  }
359
354
 
360
- if (!fs.existsSync(runtimeDir)) {
361
- fs.mkdirSync(runtimeDir, { recursive: true });
362
- }
355
+ fs.mkdirSync(runtimeDir, { recursive: true });
363
356
 
364
357
  const child = spawn(process.execPath, [path.join(projectRoot, "src", "server.js")], {
365
358
  cwd: projectRoot,
366
359
  detached: true,
367
- stdio: [
368
- "ignore",
369
- fs.openSync(logFilePath, "a"),
370
- fs.openSync(errorLogFilePath, "a"),
371
- ],
360
+ stdio: ["ignore", fs.openSync(logFilePath, "a"), fs.openSync(errorLogFilePath, "a")],
372
361
  windowsHide: true,
373
362
  });
374
363
 
375
364
  child.unref();
376
- const pid = child.pid;
377
-
378
- fs.writeFileSync(pidFilePath, `${pid}\n`, "utf8");
365
+ fs.writeFileSync(pidFilePath, `${child.pid}\n`, "utf8");
379
366
 
380
367
  const config = loadConfig();
381
- console.log(`\n✓ Loren server started (PID ${pid}).`);
382
- console.log(` URL: ${getBridgeBaseUrl(config)}`);
368
+ console.log("\nLoren is up and listening.");
369
+ console.log(`URL: ${getBridgeBaseUrl(config)}`);
383
370
  console.log("");
384
371
  }
385
372
 
386
373
  function stopServer() {
387
374
  const pid = readPidFile();
388
375
  if (!pid) {
389
- console.log("\nLoren server is not running.");
376
+ console.log("\nLoren is not running.");
390
377
  console.log("");
391
378
  return;
392
379
  }
393
380
 
394
381
  if (!isProcessRunning(pid)) {
395
382
  safeUnlink(pidFilePath);
396
- console.log(`\nRemoved stale PID file for process ${pid}.`);
383
+ console.log("\nCleaned up a stale PID file.");
397
384
  console.log("");
398
385
  return;
399
386
  }
400
387
 
401
388
  try {
402
389
  if (process.platform === "win32") {
403
- execFileSync("taskkill.exe", ["/PID", `${pid}`, "/T", "/F"], {
404
- stdio: "ignore",
405
- });
390
+ execFileSync("taskkill.exe", ["/PID", `${pid}`, "/T", "/F"], { stdio: "ignore" });
406
391
  } else {
407
392
  process.kill(pid, "SIGINT");
408
393
  }
409
394
 
410
395
  safeUnlink(pidFilePath);
411
- console.log(`\n✓ Loren server stopped (PID ${pid}).`);
396
+ console.log("\nLoren stopped cleanly.");
412
397
  console.log("");
413
398
  } catch (error) {
414
- console.error(`Error stopping server: ${error.message}`);
399
+ console.error(`Error stopping Loren: ${error.message}`);
415
400
  process.exit(1);
416
401
  }
417
402
  }
@@ -421,15 +406,12 @@ function showServerStatus() {
421
406
  const pid = readPidFile();
422
407
  const running = pid ? isProcessRunning(pid) : false;
423
408
 
424
- console.log("\nServer Status:");
409
+ console.log("\nServer status:");
425
410
  console.log("─".repeat(40));
426
411
  console.log(` Running: ${running ? "yes" : "no"}`);
427
412
  console.log(` Host: ${config.host}`);
428
413
  console.log(` Port: ${config.port}`);
429
414
  console.log(` URL: ${getBridgeBaseUrl(config)}`);
430
- if (pid) {
431
- console.log(` PID: ${pid}${running ? "" : " (stale)"}`);
432
- }
433
415
  console.log("");
434
416
  }
435
417
 
@@ -446,15 +428,11 @@ function readPidFile() {
446
428
  function isProcessRunning(pid) {
447
429
  if (process.platform === "win32") {
448
430
  try {
449
- const output = execFileSync("powershell.exe", [
450
- "-NoProfile",
451
- "-Command",
452
- `Get-Process -Id ${pid} -ErrorAction Stop | Select-Object -ExpandProperty Id`,
453
- ], {
454
- encoding: "utf8",
455
- stdio: ["ignore", "pipe", "ignore"],
456
- }).trim();
457
-
431
+ const output = execFileSync(
432
+ "powershell.exe",
433
+ ["-NoProfile", "-Command", `Get-Process -Id ${pid} -ErrorAction Stop | Select-Object -ExpandProperty Id`],
434
+ { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] },
435
+ ).trim();
458
436
  return output === `${pid}`;
459
437
  } catch {
460
438
  return false;
@@ -497,47 +475,134 @@ function syncClaudeSelectedModel(model) {
497
475
  fs.writeFileSync(claudeSettingsPath, `${JSON.stringify(settings, null, 2)}\n`, "utf8");
498
476
  }
499
477
 
500
- // ============== HELP ==============
478
+ async function runSetupWizard(config) {
479
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
480
+ printHelp();
481
+ printQuickSetup(config);
482
+ return;
483
+ }
484
+
485
+ if (config.apiKeys.length > 0) {
486
+ printWelcomeBack(config);
487
+ return;
488
+ }
489
+
490
+ printWizardIntro();
491
+
492
+ const rl = createInterface({
493
+ input: process.stdin,
494
+ output: process.stdout,
495
+ });
496
+
497
+ 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
+ }
514
+
515
+ 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") {
518
+ installClaudeIntegration();
519
+ } else {
520
+ console.log("\nNo problem. You can wire Claude in later.");
521
+ }
522
+ }
523
+
524
+ console.log("Setup complete. Fewer steps, fewer goblins.");
525
+ console.log("");
526
+ } finally {
527
+ rl.close();
528
+ }
529
+ }
530
+
531
+ function printWizardIntro() {
532
+ console.log(BANNER);
533
+ if (envStatus.migrated) {
534
+ console.log("Your previous settings were imported automatically.");
535
+ } else if (envStatus.created) {
536
+ console.log("A fresh config is ready.");
537
+ }
538
+ console.log("Let's get Loren ready in one quick pass.");
539
+ console.log("");
540
+ }
541
+
542
+ function printWelcomeBack(config) {
543
+ console.log(BANNER);
544
+ console.log(`Welcome back. ${config.apiKeys.length} key(s) loaded.`);
545
+ console.log(`Current default model: ${config.defaultModel}`);
546
+ 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("");
552
+ }
553
+
554
+ function printQuickSetup(config) {
555
+ if (config.apiKeys.length > 0) {
556
+ console.log("Run `loren start` to launch the bridge.");
557
+ console.log("");
558
+ return;
559
+ }
560
+
561
+ console.log("Quick start:");
562
+ console.log(" 1. Run `loren` in an interactive terminal");
563
+ console.log(" 2. Add your Ollama API key(s)");
564
+ console.log(" 3. Start the bridge");
565
+ console.log("");
566
+ }
567
+
568
+ function installClaudeIntegration() {
569
+ const scriptPath = path.join(projectRoot, "scripts", "install-claude-ollama.ps1");
570
+
571
+ try {
572
+ execFileSync("powershell.exe", ["-ExecutionPolicy", "Bypass", "-File", scriptPath], {
573
+ stdio: "inherit",
574
+ });
575
+ } catch (error) {
576
+ console.error(`Couldn't install Claude integration automatically: ${error.message}`);
577
+ }
578
+ }
501
579
 
502
580
  function printHelp() {
503
- console.log(ASCII_LOGO);
504
- console.log(`
505
- LOREN CODE - Ollama Cloud Model Manager
506
- ────────────────────────────────────────────────────
507
-
508
- MODEL COMMANDS:
509
- loren model:list Fetch & list models from Ollama Cloud
510
- loren model:set <name> Set default model (immediate effect)
511
- loren model:current Show current default model
512
- loren model:refresh Force refresh models cache
513
-
514
- KEY COMMANDS:
515
- loren keys:list List configured API keys
516
- loren keys:add <key> Add a new API key
517
- loren keys:remove <idx|key> Remove a key by index or value
518
- loren keys:rotate Rotate keys (move first to end)
519
-
520
- CONFIG COMMANDS:
521
- loren config:show Show current configuration
522
-
523
- SERVER COMMANDS:
524
- loren start Start bridge server (port 8788)
525
- loren stop Stop bridge server
526
- loren status Show bridge server status
527
-
528
- EXAMPLES:
529
- loren model:list
530
- loren model:set gpt-oss:20b
531
- loren model:refresh
532
- loren keys:add sk-ollama-abc123...
533
- loren keys:remove 0
534
- loren config:show
535
-
536
- TIPS:
537
- - Model changes take effect immediately for new requests
538
- - Use model:refresh after changing model to update Claude Code's list
539
- - Models are sorted by modification date (most recent first)
540
- `);
581
+ console.log(BANNER);
582
+ console.log("Commands:");
583
+ console.log(" loren setup Run the setup wizard");
584
+ console.log(" loren start Start the bridge");
585
+ console.log(" loren stop Stop the bridge");
586
+ console.log(" loren status Show bridge status");
587
+ console.log(" loren model:list List models");
588
+ console.log(" loren model:set <name> Set the default model");
589
+ console.log(" loren model:current Show the current model");
590
+ console.log(" loren model:refresh Refresh cached models");
591
+ console.log(" loren keys:list List API keys");
592
+ console.log(" loren keys:add <key> Add an API key");
593
+ console.log(" loren keys:remove <value> Remove an API key");
594
+ console.log(" loren keys:rotate Rotate configured keys");
595
+ console.log(" loren config:show Show current config");
596
+ console.log(" loren config:paths Show Loren paths");
597
+ console.log("");
598
+ console.log("Examples:");
599
+ console.log(" loren");
600
+ console.log(" loren start");
601
+ console.log(" loren model:set gpt-oss:20b");
602
+ console.log("");
541
603
  }
542
604
 
543
- main();
605
+ main().catch((error) => {
606
+ console.error(error instanceof Error ? error.message : String(error));
607
+ process.exit(1);
608
+ });
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+
3
+ console.log("");
4
+ console.log("Loren Code is installed.");
5
+ console.log("Run `loren` to begin setup.");
6
+ console.log("No treasure map required.");
7
+ 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,27 +1,38 @@
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?.("Existing Loren settings were migrated automatically.");
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
- logger.warn?.(`Created ${envLocalPath}. Add your Ollama API key(s) before starting the bridge.`);
30
+ logger.warn?.("A fresh Loren config was created. Add your Ollama API key(s) before starting the bridge.");
20
31
  return { created: true, path: envLocalPath };
21
32
  }
22
33
 
23
34
  fs.copyFileSync(envExamplePath, envLocalPath);
24
- logger.warn?.(`Created ${envLocalPath} from .env.example. Add your real Ollama API key(s) before starting the bridge.`);
35
+ logger.warn?.("A fresh Loren config was created from the template. Add your real Ollama API key(s) before starting the bridge.");
25
36
  return { created: true, path: envLocalPath };
26
37
  }
27
38
 
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