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 +1 -1
- package/scripts/install-claude-ollama.ps1 +4 -8
- package/scripts/loren.js +207 -172
- package/scripts/postinstall.js +3 -12
- package/src/bootstrap.js +3 -3
package/package.json
CHANGED
|
@@ -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 "
|
|
296
|
-
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,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
|
-
|
|
24
|
-
const envStatus = ensureEnvLocal(projectRoot);
|
|
25
|
-
|
|
26
|
-
const
|
|
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
|
|
61
|
+
if (!command) {
|
|
62
|
+
await runSetupWizard(config);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (command === "help" || command === "--help" || command === "-h") {
|
|
64
67
|
printHelp();
|
|
65
|
-
|
|
66
|
-
process.exit(0);
|
|
68
|
+
return;
|
|
67
69
|
}
|
|
68
70
|
|
|
69
|
-
|
|
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
|
-
|
|
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: {
|
|
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)
|
|
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 =
|
|
158
|
+
const url = `${getBridgeBaseUrl(config)}/v1/refresh`;
|
|
154
159
|
|
|
155
|
-
console.log(
|
|
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: {
|
|
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(
|
|
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("
|
|
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("
|
|
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(`
|
|
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(`\
|
|
206
|
-
console.log("
|
|
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("
|
|
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
|
|
222
|
+
console.log("\nConfigured API keys:");
|
|
225
223
|
console.log("─".repeat(40));
|
|
226
224
|
|
|
227
225
|
if (config.apiKeys.length === 0) {
|
|
228
|
-
console.log("
|
|
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("
|
|
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("
|
|
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(`\
|
|
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("
|
|
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((
|
|
281
|
+
keyToRemove = existingKeys.find((key) => key === indexOrKey);
|
|
296
282
|
}
|
|
297
283
|
|
|
298
284
|
if (!keyToRemove) {
|
|
299
|
-
console.error("
|
|
285
|
+
console.error("Key not found.");
|
|
300
286
|
process.exit(1);
|
|
301
287
|
}
|
|
302
288
|
|
|
303
|
-
existingKeys = existingKeys.filter((
|
|
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(`\
|
|
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(
|
|
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("
|
|
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("\
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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(
|
|
362
|
-
console.log(`
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
389
|
-
console.log(`
|
|
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
|
|
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(
|
|
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(
|
|
396
|
+
console.log("\nLoren stopped cleanly.");
|
|
419
397
|
console.log("");
|
|
420
398
|
} catch (error) {
|
|
421
|
-
console.error(`Error stopping
|
|
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
|
|
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(
|
|
457
|
-
"
|
|
458
|
-
"-Command",
|
|
459
|
-
|
|
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
|
|
508
|
-
if (!
|
|
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
|
-
|
|
513
|
-
|
|
514
|
-
|
|
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("
|
|
534
|
+
console.log("Your previous settings were imported automatically.");
|
|
518
535
|
} else if (envStatus.created) {
|
|
519
|
-
console.log("
|
|
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
|
-
|
|
523
|
-
|
|
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("
|
|
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
|
-
|
|
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(
|
|
534
|
-
console.log(
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
loren model:list
|
|
540
|
-
loren model:set <name>
|
|
541
|
-
loren model:current
|
|
542
|
-
loren model:refresh
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
loren keys:
|
|
546
|
-
loren keys:
|
|
547
|
-
loren
|
|
548
|
-
loren
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
loren
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
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
|
+
});
|
package/scripts/postinstall.js
CHANGED
|
@@ -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(`
|
|
7
|
-
console.log(
|
|
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?.(
|
|
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?.(
|
|
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?.(
|
|
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
|
|