loren-code 0.1.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.
@@ -0,0 +1,184 @@
1
+ $ErrorActionPreference = "Stop"
2
+
3
+ $repoRoot = Split-Path -Parent $PSScriptRoot
4
+ $userProfile = [Environment]::GetFolderPath("UserProfile")
5
+ $appData = [Environment]::GetFolderPath("ApplicationData")
6
+ $workspaceSettingsDir = Join-Path $appData "Code\\User"
7
+ $workspaceSettingsPath = Join-Path $workspaceSettingsDir "settings.json"
8
+ $claudeDir = Join-Path $userProfile ".claude"
9
+ $claudeSettingsPath = Join-Path $claudeDir "settings.json"
10
+ $launcherSourcePath = Join-Path $repoRoot "scripts\\ClaudeWrapperLauncher.cs"
11
+ $launcherExePath = Join-Path $repoRoot "scripts\\ClaudeWrapperLauncher.exe"
12
+ $envPath = Join-Path $repoRoot ".env.local"
13
+
14
+ if (-not (Test-Path $envPath)) {
15
+ throw ".env.local non trovato. Crea prima il file con OLLAMA_API_KEYS."
16
+ }
17
+
18
+ New-Item -ItemType Directory -Force -Path $workspaceSettingsDir | Out-Null
19
+ New-Item -ItemType Directory -Force -Path $claudeDir | Out-Null
20
+
21
+ function Read-JsonFile {
22
+ param([string]$Path)
23
+
24
+ if (-not (Test-Path $Path)) {
25
+ return @{}
26
+ }
27
+
28
+ $raw = Get-Content -LiteralPath $Path -Raw
29
+ if ([string]::IsNullOrWhiteSpace($raw)) {
30
+ return @{}
31
+ }
32
+
33
+ $parsed = $raw | ConvertFrom-Json
34
+ if ($null -eq $parsed) {
35
+ return @{}
36
+ }
37
+
38
+ $result = @{}
39
+ foreach ($property in $parsed.PSObject.Properties) {
40
+ $result[$property.Name] = $property.Value
41
+ }
42
+
43
+ return $result
44
+ }
45
+
46
+ function Write-JsonFile {
47
+ param(
48
+ [string]$Path,
49
+ [hashtable]$Data
50
+ )
51
+
52
+ $json = $Data | ConvertTo-Json -Depth 20
53
+ Set-Content -LiteralPath $Path -Value ($json + "`n") -Encoding UTF8
54
+ }
55
+
56
+ function Get-EnvValue {
57
+ param(
58
+ [string]$Path,
59
+ [string]$Name
60
+ )
61
+
62
+ $lines = Get-Content -LiteralPath $Path
63
+ foreach ($line in $lines) {
64
+ if ($line -match "^\s*$Name=(.+)$") {
65
+ return $Matches[1].Trim()
66
+ }
67
+ }
68
+
69
+ return $null
70
+ }
71
+
72
+ function Get-CSharpCompiler {
73
+ $candidates = @(
74
+ "C:\Windows\Microsoft.NET\Framework64\v4.0.30319\csc.exe",
75
+ "C:\Windows\Microsoft.NET\Framework\v4.0.30319\csc.exe"
76
+ )
77
+
78
+ foreach ($candidate in $candidates) {
79
+ if (Test-Path $candidate) {
80
+ return $candidate
81
+ }
82
+ }
83
+
84
+ throw "Compilatore C# non trovato. Impossibile generare il launcher .exe."
85
+ }
86
+
87
+ function Get-OllamaAvailableModels {
88
+ param(
89
+ [string]$EnvPath,
90
+ [hashtable]$Aliases
91
+ )
92
+
93
+ $models = [System.Collections.ArrayList]::new()
94
+
95
+ foreach ($alias in $Aliases.Keys) {
96
+ if (-not $models.Contains($alias)) {
97
+ [void]$models.Add($alias)
98
+ }
99
+ }
100
+
101
+ foreach ($target in $Aliases.Values) {
102
+ if (-not [string]::IsNullOrWhiteSpace($target) -and -not $models.Contains($target)) {
103
+ [void]$models.Add($target)
104
+ }
105
+ }
106
+
107
+ $apiKeysRaw = Get-EnvValue -Path $EnvPath -Name "OLLAMA_API_KEYS"
108
+ if (-not $apiKeysRaw) {
109
+ $apiKeysRaw = Get-EnvValue -Path $EnvPath -Name "OLLAMA_API_KEY"
110
+ }
111
+
112
+ if (-not $apiKeysRaw) {
113
+ return $models
114
+ }
115
+
116
+ $apiKey = ($apiKeysRaw -split ",")[0].Trim()
117
+ if ([string]::IsNullOrWhiteSpace($apiKey)) {
118
+ return $models
119
+ }
120
+
121
+ try {
122
+ $headers = @{ Authorization = "Bearer $apiKey" }
123
+ $response = Invoke-WebRequest -UseBasicParsing -Headers $headers "https://ollama.com/api/tags"
124
+ $payload = $response.Content | ConvertFrom-Json
125
+ foreach ($model in $payload.models) {
126
+ $name = [string]$model.model
127
+ if ([string]::IsNullOrWhiteSpace($name)) {
128
+ $name = [string]$model.name
129
+ }
130
+
131
+ if (-not [string]::IsNullOrWhiteSpace($name) -and -not $models.Contains($name)) {
132
+ [void]$models.Add($name)
133
+ }
134
+ }
135
+ } catch {
136
+ Write-Warning "Impossibile caricare la lista modelli da Ollama Cloud. Continuo con alias e target locali."
137
+ }
138
+
139
+ return $models
140
+ }
141
+
142
+ $compilerPath = Get-CSharpCompiler
143
+ & $compilerPath "/nologo" "/target:exe" "/out:$launcherExePath" $launcherSourcePath | Out-Null
144
+ if ($LASTEXITCODE -ne 0 -or -not (Test-Path $launcherExePath)) {
145
+ if (Test-Path $launcherExePath) {
146
+ Write-Warning "Compilazione launcher fallita, ma uso il launcher esistente."
147
+ } else {
148
+ throw "Compilazione del launcher fallita."
149
+ }
150
+ }
151
+
152
+ $workspaceSettings = Read-JsonFile -Path $workspaceSettingsPath
153
+ $workspaceSettings["claudeCode.claudeProcessWrapper"] = $launcherExePath
154
+ $workspaceSettings["claudeCode.disableLoginPrompt"] = $true
155
+ Write-JsonFile -Path $workspaceSettingsPath -Data $workspaceSettings
156
+
157
+ $claudeSettings = Read-JsonFile -Path $claudeSettingsPath
158
+ $aliasJson = Get-EnvValue -Path $envPath -Name "OLLAMA_MODEL_ALIASES"
159
+ if (-not $aliasJson) {
160
+ throw "OLLAMA_MODEL_ALIASES non trovato in .env.local"
161
+ }
162
+
163
+ $parsedAliases = $aliasJson | ConvertFrom-Json
164
+ $aliases = @{}
165
+ foreach ($property in $parsedAliases.PSObject.Properties) {
166
+ $aliases[$property.Name] = [string]$property.Value
167
+ }
168
+ $availableModels = Get-OllamaAvailableModels -EnvPath $envPath -Aliases $aliases
169
+
170
+ if ($availableModels.Count -eq 0) {
171
+ throw "OLLAMA_MODEL_ALIASES non contiene modelli"
172
+ }
173
+
174
+ $defaultModel = if ($aliases.ContainsKey("ollama-free-auto")) { "ollama-free-auto" } else { $availableModels[0] }
175
+ $claudeSettings["model"] = $defaultModel
176
+ $claudeSettings["availableModels"] = $availableModels
177
+ Write-JsonFile -Path $claudeSettingsPath -Data $claudeSettings
178
+
179
+ Write-Host "Installazione completata."
180
+ Write-Host "Launcher Claude:" $launcherExePath
181
+ Write-Host "VS Code user settings:" $workspaceSettingsPath
182
+ Write-Host "Claude user settings:" $claudeSettingsPath
183
+ Write-Host ""
184
+ Write-Host "Riavvia VS Code. Claude Code usera il bridge in qualsiasi progetto."
@@ -0,0 +1,515 @@
1
+ #!/usr/bin/env node
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { execFileSync, spawn } from "node:child_process";
5
+ import { fileURLToPath } from "node:url";
6
+ import { loadConfig, loadEnvFile, saveEnvFile } from "../src/config.js";
7
+ import { ensureEnvLocal, ensureRuntimeDir, getBridgeBaseUrl } from "../src/bootstrap.js";
8
+
9
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
10
+ const projectRoot = path.resolve(__dirname, "..");
11
+ const envFilePath = path.join(projectRoot, ".env.local");
12
+ const runtimeDir = path.join(projectRoot, ".runtime");
13
+ const pidFilePath = path.join(runtimeDir, "loren.pid");
14
+ const logFilePath = path.join(runtimeDir, "bridge.log");
15
+ const errorLogFilePath = path.join(runtimeDir, "bridge.err.log");
16
+
17
+ // Force working directory to project root for config loading
18
+ process.chdir(projectRoot);
19
+ ensureRuntimeDir(projectRoot);
20
+ ensureEnvLocal(projectRoot);
21
+
22
+ const ASCII_LOGO = `
23
+ ██╗ ██████╗ ██████╗ ███████╗███╗ ██╗ ██████╗ ██████╗ ██████╗ ███████╗
24
+ ██║ ██╔═══██╗██╔══██╗██╔════╝████╗ ██║ ██╔════╝██╔═══██╗██╔══██╗██╔════╝
25
+ ██║ ██║ ██║██████╔╝█████╗ ██╔██╗ ██║ ██║ ██║ ██║██║ ██║█████╗
26
+ ██║ ██║ ██║██╔══██╗██╔══╝ ██║╚██╗██║ ██║ ██║ ██║██║ ██║██╔══╝
27
+ ███████╗╚██████╔╝██║ ██║███████╗██║ ╚████║ ╚██████╗╚██████╔╝██████╔╝███████╗
28
+ ╚══════╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝╚═╝ ╚═══╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚══════
29
+ `;
30
+
31
+ const COMMANDS = {
32
+ model: {
33
+ list: listModels,
34
+ set: setModel,
35
+ current: showCurrentModel,
36
+ refresh: refreshModels,
37
+ },
38
+ keys: {
39
+ list: listKeys,
40
+ add: addKey,
41
+ remove: removeKey,
42
+ rotate: rotateKeys,
43
+ },
44
+ config: {
45
+ show: showConfig,
46
+ },
47
+ server: {
48
+ start: startServer,
49
+ stop: stopServer,
50
+ status: showServerStatus,
51
+ },
52
+ };
53
+
54
+ function main() {
55
+ const args = process.argv.slice(2);
56
+ const [command, subcommand, ...rest] = args;
57
+
58
+ if (!command || command === "help" || command === "--help" || command === "-h") {
59
+ printHelp();
60
+ process.exit(0);
61
+ }
62
+
63
+ const [category, action] = command.split(":");
64
+
65
+ if (command === "start") {
66
+ startServer();
67
+ return;
68
+ }
69
+
70
+ if (command === "stop") {
71
+ stopServer();
72
+ return;
73
+ }
74
+
75
+ if (command === "status") {
76
+ showServerStatus();
77
+ return;
78
+ }
79
+
80
+ if (category && action && COMMANDS[category] && COMMANDS[category][action]) {
81
+ COMMANDS[category][action](rest);
82
+ return;
83
+ }
84
+
85
+ console.error(`Unknown command: ${command}`);
86
+ printHelp();
87
+ process.exit(1);
88
+ }
89
+
90
+ // ============== MODEL COMMANDS ==============
91
+
92
+ async function listModels() {
93
+ const config = loadConfig();
94
+
95
+ try {
96
+ const response = await fetch(`${config.upstreamBaseUrl}/api/tags`, {
97
+ headers: { "accept": "application/json" },
98
+ });
99
+
100
+ if (!response.ok) {
101
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
102
+ }
103
+
104
+ const data = await response.json();
105
+ let models = Array.isArray(data.models) ? data.models : [];
106
+
107
+ // Sort by modified date (most recent first)
108
+ models = models.sort((a, b) => {
109
+ const dateA = a.modified_at ? new Date(a.modified_at).getTime() : 0;
110
+ const dateB = b.modified_at ? new Date(b.modified_at).getTime() : 0;
111
+ return dateB - dateA;
112
+ });
113
+
114
+ console.log("\nAvailable models from Ollama Cloud:");
115
+ console.log("─".repeat(70));
116
+ console.log("MODEL".padEnd(30) + "SIZE".padStart(12) + "MODIFIED".padStart(12));
117
+ console.log("─".repeat(70));
118
+
119
+ for (const model of models) {
120
+ const modelId = model.model || model.name;
121
+ const size = formatSize(model.size);
122
+ const modified = model.modified_at ? new Date(model.modified_at).toLocaleDateString() : "unknown";
123
+ const marker = modelId === config.defaultModel ? "●" : "○";
124
+ console.log(
125
+ `${marker} ${modelId.padEnd(28)}${size.padStart(12)}${modified.padStart(12)}`
126
+ );
127
+ }
128
+
129
+ console.log("");
130
+ console.log(`Total: ${models.length} model(s)`);
131
+ console.log(`Current default: ${config.defaultModel}`);
132
+ console.log("");
133
+ } catch (error) {
134
+ console.error(`Error fetching models: ${error.message}`);
135
+ process.exit(1);
136
+ }
137
+ }
138
+
139
+ function formatSize(bytes) {
140
+ if (!bytes) return "unknown";
141
+ const gb = bytes / (1024 ** 3);
142
+ return `${gb.toFixed(1)} GB`;
143
+ }
144
+
145
+ async function refreshModels() {
146
+ const config = loadConfig();
147
+ const url = `http://${config.host}:${config.port}/v1/refresh`;
148
+
149
+ console.log(`Sending refresh request to ${url}...`);
150
+
151
+ try {
152
+ const response = await fetch(url, {
153
+ method: "POST",
154
+ headers: { "accept": "application/json" },
155
+ });
156
+
157
+ if (!response.ok) {
158
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
159
+ }
160
+
161
+ const data = await response.json();
162
+ const models = Array.isArray(data.data) ? data.data : [];
163
+
164
+ console.log("\n✓ Models refreshed successfully!");
165
+ console.log(` Fetched ${models.length} model(s) from Ollama Cloud.`);
166
+ console.log("");
167
+ } catch (error) {
168
+ console.error(`Error refreshing models: ${error.message}`);
169
+ console.error("Make sure the server is running: loren start");
170
+ process.exit(1);
171
+ }
172
+ }
173
+
174
+ function setModel(args) {
175
+ const requestedModel = args.join(" ").trim();
176
+
177
+ if (!requestedModel) {
178
+ console.error("Error: Specify a model name.");
179
+ console.error("Example: loren model:set qwen3.5:397b");
180
+ process.exit(1);
181
+ }
182
+
183
+ const config = loadConfig();
184
+
185
+ // Check if it's a valid alias or add it as a new direct model
186
+ const isValidAlias = Object.keys(config.aliases).includes(requestedModel);
187
+
188
+ if (!isValidAlias) {
189
+ console.warn(`Warning: '${requestedModel}' is not a configured alias.`);
190
+ console.warn("It will be used as a direct model name.");
191
+ }
192
+
193
+ // Update .env.local with new DEFAULT_MODEL_ALIAS
194
+ const envVars = loadEnvFile(envFilePath);
195
+ envVars.DEFAULT_MODEL_ALIAS = requestedModel;
196
+ saveEnvFile(envFilePath, envVars);
197
+
198
+ console.log(`\n✓ Default model set to: ${requestedModel}`);
199
+ console.log(" New requests will use this model immediately.");
200
+ console.log("");
201
+ }
202
+
203
+ function showCurrentModel() {
204
+ const config = loadConfig();
205
+ console.log(`\nCurrent default model: ${config.defaultModel}`);
206
+ console.log("");
207
+ }
208
+
209
+ // ============== API KEY COMMANDS ==============
210
+
211
+ function listKeys() {
212
+ const config = loadConfig();
213
+
214
+ console.log("\nConfigured API Keys:");
215
+ console.log("─".repeat(40));
216
+
217
+ if (config.apiKeys.length === 0) {
218
+ console.log(" (none configured)");
219
+ } else {
220
+ for (let i = 0; i < config.apiKeys.length; i++) {
221
+ const key = config.apiKeys[i];
222
+ const masked = `${key.slice(0, 4)}...${key.slice(-4)}`;
223
+ const marker = i === 0 ? "●" : "○";
224
+ console.log(` ${marker} [${i}] ${masked}`);
225
+ }
226
+ }
227
+
228
+ console.log("");
229
+ console.log(`Total: ${config.apiKeys.length} key(s)`);
230
+ console.log("");
231
+ }
232
+
233
+ function addKey(args) {
234
+ const newKey = args.join(" ").trim();
235
+
236
+ if (!newKey) {
237
+ console.error("Error: Specify an API key.");
238
+ console.error("Example: loren keys:add sk-your-key-here");
239
+ process.exit(1);
240
+ }
241
+
242
+ const envVars = loadEnvFile(envFilePath);
243
+ const existingKeys = (envVars.OLLAMA_API_KEYS || "")
244
+ .split(/[,\r?\n]+/)
245
+ .map((k) => k.trim())
246
+ .filter(Boolean);
247
+
248
+ if (existingKeys.includes(newKey)) {
249
+ console.log(" Key already exists, skipping.");
250
+ return;
251
+ }
252
+
253
+ existingKeys.push(newKey);
254
+ envVars.OLLAMA_API_KEYS = existingKeys.join(",");
255
+ saveEnvFile(envFilePath, envVars);
256
+
257
+ console.log(`\n✓ API key added.`);
258
+ console.log(` Total keys: ${existingKeys.length}`);
259
+ console.log(" New key will be used for subsequent requests.");
260
+ console.log("");
261
+ }
262
+
263
+ function removeKey(args) {
264
+ const indexOrKey = args.join(" ").trim();
265
+
266
+ if (!indexOrKey) {
267
+ console.error("Error: Specify key index or the key itself.");
268
+ console.error("Example: loren keys:remove 0");
269
+ console.error(" loren keys:remove sk-xxx...");
270
+ process.exit(1);
271
+ }
272
+
273
+ const envVars = loadEnvFile(envFilePath);
274
+ let existingKeys = (envVars.OLLAMA_API_KEYS || "")
275
+ .split(/[,\r?\n]+/)
276
+ .map((k) => k.trim())
277
+ .filter(Boolean);
278
+
279
+ let keyToRemove;
280
+ const index = parseInt(indexOrKey, 10);
281
+
282
+ if (!isNaN(index) && index >= 0 && index < existingKeys.length) {
283
+ keyToRemove = existingKeys[index];
284
+ } else {
285
+ keyToRemove = existingKeys.find((k) => k === indexOrKey);
286
+ }
287
+
288
+ if (!keyToRemove) {
289
+ console.error("Error: Key not found.");
290
+ process.exit(1);
291
+ }
292
+
293
+ existingKeys = existingKeys.filter((k) => k !== keyToRemove);
294
+ envVars.OLLAMA_API_KEYS = existingKeys.join(",");
295
+ saveEnvFile(envFilePath, envVars);
296
+
297
+ console.log(`\n✓ API key removed.`);
298
+ console.log(` Remaining keys: ${existingKeys.length}`);
299
+ console.log("");
300
+ }
301
+
302
+ function rotateKeys(args) {
303
+ const envVars = loadEnvFile(envFilePath);
304
+ let existingKeys = (envVars.OLLAMA_API_KEYS || "")
305
+ .split(/[,\r?\n]+/)
306
+ .map((k) => k.trim())
307
+ .filter(Boolean);
308
+
309
+ if (existingKeys.length < 2) {
310
+ console.log("Need at least 2 keys to rotate.");
311
+ return;
312
+ }
313
+
314
+ // Move first key to the end
315
+ const [first, ...rest] = existingKeys;
316
+ existingKeys = [...rest, first];
317
+
318
+ envVars.OLLAMA_API_KEYS = existingKeys.join(",");
319
+ saveEnvFile(envFilePath, envVars);
320
+
321
+ console.log("\n✓ API keys rotated.");
322
+ console.log(" First key moved to end of list.");
323
+ console.log("");
324
+ }
325
+
326
+ // ============== CONFIG COMMANDS ==============
327
+
328
+ function showConfig() {
329
+ const config = loadConfig();
330
+
331
+ console.log("\nCurrent Configuration:");
332
+ console.log("─".repeat(40));
333
+ console.log(` Host: ${config.host}`);
334
+ console.log(` Port: ${config.port}`);
335
+ console.log(` Upstream: ${config.upstreamBaseUrl}`);
336
+ console.log(` API Keys: ${config.apiKeys.length}`);
337
+ console.log(` Aliases: ${Object.keys(config.aliases).length}`);
338
+ console.log(` Default: ${config.defaultModel}`);
339
+ console.log("");
340
+ }
341
+
342
+ // ============== SERVER COMMANDS ==============
343
+
344
+ function startServer() {
345
+ const existingPid = readPidFile();
346
+ if (existingPid && isProcessRunning(existingPid)) {
347
+ const config = loadConfig();
348
+ console.log(`\nLoren server is already running (PID ${existingPid}).`);
349
+ console.log(` URL: ${getBridgeBaseUrl(config)}`);
350
+ console.log("");
351
+ return;
352
+ }
353
+
354
+ if (!fs.existsSync(runtimeDir)) {
355
+ fs.mkdirSync(runtimeDir, { recursive: true });
356
+ }
357
+
358
+ const child = spawn(process.execPath, [path.join(projectRoot, "src", "server.js")], {
359
+ cwd: projectRoot,
360
+ detached: true,
361
+ stdio: [
362
+ "ignore",
363
+ fs.openSync(logFilePath, "a"),
364
+ fs.openSync(errorLogFilePath, "a"),
365
+ ],
366
+ windowsHide: true,
367
+ });
368
+
369
+ child.unref();
370
+ const pid = child.pid;
371
+
372
+ fs.writeFileSync(pidFilePath, `${pid}\n`, "utf8");
373
+
374
+ const config = loadConfig();
375
+ console.log(`\n✓ Loren server started (PID ${pid}).`);
376
+ console.log(` URL: ${getBridgeBaseUrl(config)}`);
377
+ console.log("");
378
+ }
379
+
380
+ function stopServer() {
381
+ const pid = readPidFile();
382
+ if (!pid) {
383
+ console.log("\nLoren server is not running.");
384
+ console.log("");
385
+ return;
386
+ }
387
+
388
+ if (!isProcessRunning(pid)) {
389
+ safeUnlink(pidFilePath);
390
+ console.log(`\nRemoved stale PID file for process ${pid}.`);
391
+ console.log("");
392
+ return;
393
+ }
394
+
395
+ try {
396
+ if (process.platform === "win32") {
397
+ execFileSync("taskkill.exe", ["/PID", `${pid}`, "/T", "/F"], {
398
+ stdio: "ignore",
399
+ });
400
+ } else {
401
+ process.kill(pid, "SIGINT");
402
+ }
403
+
404
+ safeUnlink(pidFilePath);
405
+ console.log(`\n✓ Loren server stopped (PID ${pid}).`);
406
+ console.log("");
407
+ } catch (error) {
408
+ console.error(`Error stopping server: ${error.message}`);
409
+ process.exit(1);
410
+ }
411
+ }
412
+
413
+ function showServerStatus() {
414
+ const config = loadConfig();
415
+ const pid = readPidFile();
416
+ const running = pid ? isProcessRunning(pid) : false;
417
+
418
+ console.log("\nServer Status:");
419
+ console.log("─".repeat(40));
420
+ console.log(` Running: ${running ? "yes" : "no"}`);
421
+ console.log(` Host: ${config.host}`);
422
+ console.log(` Port: ${config.port}`);
423
+ console.log(` URL: ${getBridgeBaseUrl(config)}`);
424
+ if (pid) {
425
+ console.log(` PID: ${pid}${running ? "" : " (stale)"}`);
426
+ }
427
+ console.log("");
428
+ }
429
+
430
+ function readPidFile() {
431
+ if (!fs.existsSync(pidFilePath)) {
432
+ return null;
433
+ }
434
+
435
+ const raw = fs.readFileSync(pidFilePath, "utf8").trim();
436
+ const pid = Number.parseInt(raw, 10);
437
+ return Number.isInteger(pid) && pid > 0 ? pid : null;
438
+ }
439
+
440
+ function isProcessRunning(pid) {
441
+ if (process.platform === "win32") {
442
+ try {
443
+ const output = execFileSync("powershell.exe", [
444
+ "-NoProfile",
445
+ "-Command",
446
+ `Get-Process -Id ${pid} -ErrorAction Stop | Select-Object -ExpandProperty Id`,
447
+ ], {
448
+ encoding: "utf8",
449
+ stdio: ["ignore", "pipe", "ignore"],
450
+ }).trim();
451
+
452
+ return output === `${pid}`;
453
+ } catch {
454
+ return false;
455
+ }
456
+ }
457
+
458
+ try {
459
+ process.kill(pid, 0);
460
+ return true;
461
+ } catch {
462
+ return false;
463
+ }
464
+ }
465
+
466
+ function safeUnlink(filePath) {
467
+ if (fs.existsSync(filePath)) {
468
+ fs.unlinkSync(filePath);
469
+ }
470
+ }
471
+
472
+ // ============== HELP ==============
473
+
474
+ function printHelp() {
475
+ console.log(ASCII_LOGO);
476
+ console.log(`
477
+ LOREN CODE - Ollama Cloud Model Manager
478
+ ────────────────────────────────────────────────────
479
+
480
+ MODEL COMMANDS:
481
+ loren model:list Fetch & list models from Ollama Cloud
482
+ loren model:set <name> Set default model (immediate effect)
483
+ loren model:current Show current default model
484
+ loren model:refresh Force refresh models cache
485
+
486
+ KEY COMMANDS:
487
+ loren keys:list List configured API keys
488
+ loren keys:add <key> Add a new API key
489
+ loren keys:remove <idx|key> Remove a key by index or value
490
+ loren keys:rotate Rotate keys (move first to end)
491
+
492
+ CONFIG COMMANDS:
493
+ loren config:show Show current configuration
494
+
495
+ SERVER COMMANDS:
496
+ loren start Start bridge server (port 8788)
497
+ loren stop Stop bridge server
498
+ loren status Show bridge server status
499
+
500
+ EXAMPLES:
501
+ loren model:list
502
+ loren model:set gpt-oss:20b
503
+ loren model:refresh
504
+ loren keys:add sk-ollama-abc123...
505
+ loren keys:remove 0
506
+ loren config:show
507
+
508
+ TIPS:
509
+ - Model changes take effect immediately for new requests
510
+ - Use model:refresh after changing model to update Claude Code's list
511
+ - Models are sorted by modification date (most recent first)
512
+ `);
513
+ }
514
+
515
+ main();