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 +9 -3
- package/package.json +3 -1
- package/scripts/claude-wrapper.js +4 -3
- package/scripts/install-claude-ollama.ps1 +23 -11
- package/scripts/loren.js +225 -160
- package/scripts/postinstall.js +7 -0
- package/scripts/uninstall-claude-ollama.ps1 +2 -1
- package/src/bootstrap.js +16 -5
- package/src/config.js +4 -4
- package/src/logger.js +9 -8
- package/src/paths.js +22 -0
- package/src/server.js +4 -3
package/README.md
CHANGED
|
@@ -20,7 +20,13 @@ loren help
|
|
|
20
20
|
|
|
21
21
|
## First Run
|
|
22
22
|
|
|
23
|
-
Loren
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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 =
|
|
16
|
+
const envFilePath = getEnvFilePath();
|
|
16
17
|
|
|
17
18
|
async function main() {
|
|
18
19
|
process.chdir(repoRoot);
|
|
19
|
-
ensureRuntimeDir(
|
|
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
|
-
$
|
|
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 "
|
|
280
|
-
Write-Host "
|
|
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
|
|
12
|
-
const
|
|
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(
|
|
22
|
-
ensureEnvLocal(projectRoot);
|
|
23
|
-
|
|
24
|
-
const
|
|
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 (
|
|
66
|
+
if (command === "help" || command === "--help" || command === "-h") {
|
|
61
67
|
printHelp();
|
|
62
|
-
|
|
68
|
+
return;
|
|
63
69
|
}
|
|
64
70
|
|
|
65
|
-
|
|
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
|
-
|
|
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: {
|
|
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)
|
|
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 =
|
|
158
|
+
const url = `${getBridgeBaseUrl(config)}/v1/refresh`;
|
|
150
159
|
|
|
151
|
-
console.log(
|
|
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: {
|
|
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(
|
|
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("
|
|
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("
|
|
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(`
|
|
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(`\
|
|
202
|
-
console.log("
|
|
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("
|
|
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
|
|
222
|
+
console.log("\nConfigured API keys:");
|
|
221
223
|
console.log("─".repeat(40));
|
|
222
224
|
|
|
223
225
|
if (config.apiKeys.length === 0) {
|
|
224
|
-
console.log("
|
|
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("
|
|
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("
|
|
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(`\
|
|
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("
|
|
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((
|
|
281
|
+
keyToRemove = existingKeys.find((key) => key === indexOrKey);
|
|
292
282
|
}
|
|
293
283
|
|
|
294
284
|
if (!keyToRemove) {
|
|
295
|
-
console.error("
|
|
285
|
+
console.error("Key not found.");
|
|
296
286
|
process.exit(1);
|
|
297
287
|
}
|
|
298
288
|
|
|
299
|
-
existingKeys = existingKeys.filter((
|
|
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(`\
|
|
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(
|
|
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("
|
|
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("\
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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(
|
|
355
|
-
console.log(`
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
382
|
-
console.log(`
|
|
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
|
|
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(
|
|
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(
|
|
396
|
+
console.log("\nLoren stopped cleanly.");
|
|
412
397
|
console.log("");
|
|
413
398
|
} catch (error) {
|
|
414
|
-
console.error(`Error stopping
|
|
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
|
|
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(
|
|
450
|
-
"
|
|
451
|
-
"-Command",
|
|
452
|
-
|
|
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
|
-
|
|
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(
|
|
504
|
-
console.log(
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
loren model:list
|
|
510
|
-
loren model:set <name>
|
|
511
|
-
loren model:current
|
|
512
|
-
loren model:refresh
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
loren keys:
|
|
516
|
-
loren keys:
|
|
517
|
-
loren
|
|
518
|
-
loren
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
loren
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
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
|
+
});
|
|
@@ -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 $
|
|
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(
|
|
5
|
-
fs.mkdirSync(
|
|
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 =
|
|
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?.(
|
|
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?.(
|
|
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
|
|
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 =
|
|
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 {
|
|
4
|
+
import { getRuntimeDir } from './paths.js';
|
|
4
5
|
|
|
5
|
-
const
|
|
6
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
40
|
+
new winston.transports.File({ filename: path.join(runtimeDir, 'exceptions.log') })
|
|
40
41
|
],
|
|
41
42
|
rejectionHandlers: [
|
|
42
|
-
new winston.transports.File({ filename: path.join(
|
|
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(
|
|
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 =
|
|
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(
|
|
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
|
|