loren-code 0.2.0 → 0.2.2

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