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.
package/.env.example ADDED
@@ -0,0 +1,14 @@
1
+ # Copy to .env.local and fill in your real Ollama Cloud API keys.
2
+ # The bridge rotates one key per upstream request.
3
+ OLLAMA_API_KEYS=
4
+
5
+ # Optional: override where the bridge listens
6
+ # BRIDGE_HOST=127.0.0.1
7
+ # BRIDGE_PORT=8788
8
+
9
+ # Optional: Ollama cloud host
10
+ # OLLAMA_UPSTREAM_BASE_URL=https://ollama.com
11
+
12
+ # Alias map shown to Claude Code users. Left side = selectable model in Claude.
13
+ # Right side = real Ollama Cloud model id.
14
+ OLLAMA_MODEL_ALIASES={"ollama-free-auto":"gpt-oss:20b","ollama-free-fast":"gemma3:12b","ollama-free-tools":"qwen3:32b"}
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 LOREN CODE
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,153 @@
1
+ # LOREN CODE
2
+
3
+ Ollama Cloud model manager and local bridge for Claude Code, with dynamic model switching, API key rotation, and first-run bootstrap.
4
+
5
+ ## Features
6
+
7
+ - Dynamic model switching without restarting the server
8
+ - Live model list fetched from Ollama Cloud
9
+ - API key add/remove/rotate commands
10
+ - First-run setup for `.env.local` and `.runtime`
11
+ - Local bridge on port `8788`
12
+ - Claude Code wrapper support
13
+
14
+ ## Quick Start
15
+
16
+ ### Prerequisites
17
+
18
+ - Node.js 18+
19
+ - Ollama Cloud API key(s)
20
+
21
+ ### Clone And Run Locally
22
+
23
+ ```bash
24
+ git clone https://github.com/lorenzune/loren-code.git
25
+ cd loren-code
26
+ npm install
27
+ node scripts/loren.js help
28
+ ```
29
+
30
+ If `.env.local` is missing, Loren creates it automatically from `.env.example`.
31
+ You still need to add real `OLLAMA_API_KEYS`.
32
+
33
+ ### Install From npm
34
+
35
+ ```bash
36
+ npm install -g loren-code
37
+ loren help
38
+ ```
39
+
40
+ The published package exposes `loren` via the `bin` field automatically.
41
+
42
+ ## Configuration
43
+
44
+ Example `.env.local`:
45
+
46
+ ```bash
47
+ BRIDGE_HOST=127.0.0.1
48
+ BRIDGE_PORT=8788
49
+ OLLAMA_API_KEYS=sk-key1,sk-key2
50
+ OLLAMA_UPSTREAM_BASE_URL=https://ollama.com
51
+ DEFAULT_MODEL_ALIAS=gpt-oss:20b
52
+ OLLAMA_MODEL_ALIASES={"ollama-free-auto":"gpt-oss:20b","ollama-free-fast":"gemma3:12b"}
53
+ ```
54
+
55
+ ## Usage
56
+
57
+ ### CLI
58
+
59
+ ```bash
60
+ loren help
61
+ loren config:show
62
+ loren status
63
+ loren model:list
64
+ loren model:set gpt-oss:20b
65
+ loren model:refresh
66
+ loren keys:list
67
+ loren keys:add sk-your-new-key
68
+ loren keys:remove 0
69
+ loren keys:rotate
70
+ ```
71
+
72
+ ### Server
73
+
74
+ ```bash
75
+ npm start
76
+ ```
77
+
78
+ or:
79
+
80
+ ```bash
81
+ loren start
82
+ loren stop
83
+ loren status
84
+ ```
85
+
86
+ ## Bridge Endpoints
87
+
88
+ - `GET /health`
89
+ - `GET /v1/models`
90
+ - `GET /v1/models?refresh=true`
91
+ - `POST /v1/refresh`
92
+ - `POST /v1/messages`
93
+ - `POST /v1/messages/count_tokens`
94
+ - `GET /metrics`
95
+ - `GET /dashboard`
96
+
97
+ ## Claude Code Integration
98
+
99
+ 1. Start the bridge.
100
+ 2. Point Claude Code to `http://127.0.0.1:8788`.
101
+ 3. Use `loren model:set` to switch model aliases.
102
+ 4. Use `loren model:refresh` to refresh the model list.
103
+
104
+ ## Troubleshooting
105
+
106
+ ### `Command not found: loren`
107
+
108
+ Install the package globally:
109
+
110
+ ```bash
111
+ npm install -g loren-code
112
+ ```
113
+
114
+ If you are working from a local clone, use `node scripts/loren.js ...`.
115
+
116
+ ### `npm` blocked in PowerShell
117
+
118
+ Use `npm.cmd` instead:
119
+
120
+ ```powershell
121
+ npm.cmd run help
122
+ ```
123
+
124
+ ### Missing API keys
125
+
126
+ Populate `OLLAMA_API_KEYS` in `.env.local`.
127
+
128
+ ### Port already in use
129
+
130
+ Change `BRIDGE_PORT` in `.env.local`.
131
+
132
+ ## Project Structure
133
+
134
+ ```text
135
+ loren-code/
136
+ |- scripts/
137
+ | |- loren.js
138
+ | |- claude-wrapper.js
139
+ | `- install-claude-ollama.ps1
140
+ |- src/
141
+ | |- bootstrap.js
142
+ | |- server.js
143
+ | |- config.js
144
+ | |- key-manager.js
145
+ | `- ...
146
+ |- .env.example
147
+ |- package.json
148
+ `- README.md
149
+ ```
150
+
151
+ ## License
152
+
153
+ MIT
package/package.json ADDED
@@ -0,0 +1,70 @@
1
+ {
2
+ "name": "loren-code",
3
+ "version": "0.1.0",
4
+ "description": "Ollama Cloud Model Manager - Dynamic model switching, API key rotation, and real-time configuration updates",
5
+ "author": "lorenzune",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/lorenzune/loren-code.git"
10
+ },
11
+ "homepage": "https://github.com/lorenzune/loren-code#readme",
12
+ "bugs": {
13
+ "url": "https://github.com/lorenzune/loren-code/issues"
14
+ },
15
+ "keywords": [
16
+ "ollama",
17
+ "ollama-cloud",
18
+ "claude",
19
+ "claude-code",
20
+ "ai",
21
+ "llm",
22
+ "model-manager",
23
+ "cli",
24
+ "bridge"
25
+ ],
26
+ "type": "module",
27
+ "main": "src/server.js",
28
+ "bin": {
29
+ "loren": "scripts/loren.js"
30
+ },
31
+ "files": [
32
+ "scripts/ClaudeWrapperLauncher.cs",
33
+ "scripts/claude-wrapper.js",
34
+ "scripts/install-claude-ollama.ps1",
35
+ "scripts/loren.js",
36
+ "scripts/uninstall-claude-ollama.ps1",
37
+ "src/*.js",
38
+ ".env.example",
39
+ "README.md"
40
+ ],
41
+ "scripts": {
42
+ "start": "node src/server.js",
43
+ "loren": "node scripts/loren.js",
44
+ "install:claude": "powershell -ExecutionPolicy Bypass -File scripts/install-claude-ollama.ps1",
45
+ "uninstall:claude": "powershell -ExecutionPolicy Bypass -File scripts/uninstall-claude-ollama.ps1",
46
+ "model:list": "node scripts/loren.js model:list",
47
+ "model:set": "node scripts/loren.js model:set",
48
+ "model:current": "node scripts/loren.js model:current",
49
+ "model:refresh": "node scripts/loren.js model:refresh",
50
+ "keys:list": "node scripts/loren.js keys:list",
51
+ "keys:add": "node scripts/loren.js keys:add",
52
+ "keys:remove": "node scripts/loren.js keys:remove",
53
+ "keys:rotate": "node scripts/loren.js keys:rotate",
54
+ "config:show": "node scripts/loren.js config:show",
55
+ "help": "node scripts/loren.js help",
56
+ "smoke": "node scripts/smoke-test.js",
57
+ "check:publish": "node scripts/publish-check.js",
58
+ "prepublishOnly": "npm test && npm run lint",
59
+ "test": "node scripts/publish-check.js",
60
+ "lint": "node -e \"console.log('No linter configured')\""
61
+ },
62
+ "dependencies": {
63
+ "node-cache": "^5.1.2",
64
+ "winston": "^3.19.0",
65
+ "zod": "^4.3.6"
66
+ },
67
+ "engines": {
68
+ "node": ">=18.0.0"
69
+ }
70
+ }
@@ -0,0 +1,78 @@
1
+ using System;
2
+ using System.Diagnostics;
3
+ using System.IO;
4
+
5
+ internal static class ClaudeWrapperLauncher
6
+ {
7
+ private static int Main(string[] args)
8
+ {
9
+ try
10
+ {
11
+ var launcherDir = AppDomain.CurrentDomain.BaseDirectory;
12
+ var wrapperScript = Path.Combine(launcherDir, "claude-wrapper.js");
13
+
14
+ if (!File.Exists(wrapperScript))
15
+ {
16
+ Console.Error.WriteLine("Missing wrapper script: " + wrapperScript);
17
+ return 1;
18
+ }
19
+
20
+ var parent = Directory.GetParent(launcherDir);
21
+ var workingDirectory = parent != null ? parent.FullName : launcherDir;
22
+
23
+ var psi = new ProcessStartInfo
24
+ {
25
+ FileName = "node.exe",
26
+ Arguments = Quote(wrapperScript) + BuildArgumentString(args),
27
+ UseShellExecute = false,
28
+ RedirectStandardInput = false,
29
+ RedirectStandardOutput = false,
30
+ RedirectStandardError = false,
31
+ WorkingDirectory = workingDirectory,
32
+ };
33
+
34
+ using (var process = Process.Start(psi))
35
+ {
36
+ if (process == null)
37
+ {
38
+ Console.Error.WriteLine("Failed to start node.exe");
39
+ return 1;
40
+ }
41
+
42
+ process.WaitForExit();
43
+ return process.ExitCode;
44
+ }
45
+ }
46
+ catch (Exception ex)
47
+ {
48
+ Console.Error.WriteLine(ex.Message);
49
+ return 1;
50
+ }
51
+ }
52
+
53
+ private static string BuildArgumentString(string[] args)
54
+ {
55
+ if (args == null || args.Length == 0)
56
+ {
57
+ return string.Empty;
58
+ }
59
+
60
+ var pieces = new string[args.Length];
61
+ for (var i = 0; i < args.Length; i++)
62
+ {
63
+ pieces[i] = Quote(args[i]);
64
+ }
65
+
66
+ return " " + string.Join(" ", pieces);
67
+ }
68
+
69
+ private static string Quote(string value)
70
+ {
71
+ if (string.IsNullOrEmpty(value))
72
+ {
73
+ return "\"\"";
74
+ }
75
+
76
+ return "\"" + value.Replace("\\", "\\\\").Replace("\"", "\\\"") + "\"";
77
+ }
78
+ }
@@ -0,0 +1,216 @@
1
+ import { spawn } from "node:child_process";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import process from "node:process";
5
+ import { fileURLToPath } from "node:url";
6
+ import { ensureEnvLocal, ensureRuntimeDir, getBridgeBaseUrl } from "../src/bootstrap.js";
7
+ import { loadConfig } from "../src/config.js";
8
+
9
+ const __filename = fileURLToPath(import.meta.url);
10
+ const __dirname = path.dirname(__filename);
11
+ const repoRoot = path.resolve(__dirname, "..");
12
+ const stateDir = path.join(repoRoot, ".runtime");
13
+ const bridgePidPath = path.join(stateDir, "bridge.pid");
14
+ const bridgeLogPath = path.join(stateDir, "bridge.log");
15
+ const envFilePath = path.join(repoRoot, ".env.local");
16
+
17
+ async function main() {
18
+ process.chdir(repoRoot);
19
+ ensureRuntimeDir(repoRoot);
20
+ ensureEnvLocal(repoRoot);
21
+ const bridgeConfig = loadConfig();
22
+ const bridgeBaseUrl = getBridgeBaseUrl(bridgeConfig);
23
+
24
+ const env = {
25
+ ...process.env,
26
+ ...loadEnvFile(envFilePath),
27
+ ANTHROPIC_BASE_URL: process.env.ANTHROPIC_BASE_URL || bridgeBaseUrl,
28
+ ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY || "bridge-local",
29
+ ANTHROPIC_AUTH_TOKEN: process.env.ANTHROPIC_AUTH_TOKEN || "",
30
+ CLAUDE_CODE_SKIP_AUTH_LOGIN: process.env.CLAUDE_CODE_SKIP_AUTH_LOGIN || "1",
31
+ CLAUDE_CODE_ENTRYPOINT: process.env.CLAUDE_CODE_ENTRYPOINT || "claude-vscode",
32
+ };
33
+
34
+ await ensureBridgeRunning(env, bridgeBaseUrl);
35
+
36
+ const claudeExecutable = resolveClaudeExecutable();
37
+ if (process.env.CLAUDE_WRAPPER_TEST === "1") {
38
+ console.log(
39
+ JSON.stringify(
40
+ {
41
+ bridgeUrl: env.ANTHROPIC_BASE_URL,
42
+ executable: claudeExecutable.command,
43
+ },
44
+ null,
45
+ 2,
46
+ ),
47
+ );
48
+ process.exit(0);
49
+ }
50
+
51
+ const child = spawn(claudeExecutable.command, [...claudeExecutable.args, ...process.argv.slice(2)], {
52
+ stdio: "inherit",
53
+ env,
54
+ cwd: process.cwd(),
55
+ windowsHide: false,
56
+ });
57
+
58
+ child.on("error", (error) => {
59
+ console.error(`Failed to start Claude executable: ${error.message}`);
60
+ process.exit(1);
61
+ });
62
+
63
+ child.on("exit", (code, signal) => {
64
+ if (signal) {
65
+ process.kill(process.pid, signal);
66
+ return;
67
+ }
68
+
69
+ process.exit(code ?? 0);
70
+ });
71
+ }
72
+
73
+ function resolveClaudeExecutable() {
74
+ const override = process.env.CLAUDE_REAL_EXECUTABLE;
75
+ if (override && fs.existsSync(override)) {
76
+ return { command: override, args: [] };
77
+ }
78
+
79
+ const candidates = findClaudeExtensionExecutables();
80
+ if (candidates.length > 0) {
81
+ return { command: candidates[0], args: [] };
82
+ }
83
+
84
+ return { command: "claude", args: [] };
85
+ }
86
+
87
+ function findClaudeExtensionExecutables() {
88
+ const home = process.env.USERPROFILE || process.env.HOME;
89
+ if (!home) {
90
+ return [];
91
+ }
92
+
93
+ const extensionRoot = path.join(home, ".vscode", "extensions");
94
+ if (!fs.existsSync(extensionRoot)) {
95
+ return [];
96
+ }
97
+
98
+ const matches = [];
99
+ for (const entry of fs.readdirSync(extensionRoot, { withFileTypes: true })) {
100
+ if (!entry.isDirectory() || !entry.name.startsWith("anthropic.claude-code-")) {
101
+ continue;
102
+ }
103
+
104
+ const candidates = [
105
+ path.join(extensionRoot, entry.name, "resources", "native-binary", "claude.exe"),
106
+ path.join(extensionRoot, entry.name, "resources", "native-binaries", "win32-x64", "claude.exe"),
107
+ path.join(extensionRoot, entry.name, "resources", "native-binaries", "win32-arm64", "claude.exe"),
108
+ ];
109
+
110
+ for (const executable of candidates) {
111
+ if (fs.existsSync(executable)) {
112
+ matches.push(executable);
113
+ }
114
+ }
115
+ }
116
+
117
+ return matches.sort().reverse();
118
+ }
119
+
120
+ async function ensureBridgeRunning(env, bridgeBaseUrl) {
121
+ if (await isBridgeHealthy(bridgeBaseUrl)) {
122
+ return;
123
+ }
124
+
125
+ if (fs.existsSync(bridgePidPath)) {
126
+ try {
127
+ const pid = Number.parseInt(fs.readFileSync(bridgePidPath, "utf8").trim(), 10);
128
+ if (Number.isInteger(pid)) {
129
+ process.kill(pid, 0);
130
+ }
131
+ } catch {
132
+ safeUnlink(bridgePidPath);
133
+ }
134
+ }
135
+
136
+ const bridge = spawn(process.execPath, [path.join(repoRoot, "src", "server.js")], {
137
+ cwd: repoRoot,
138
+ env,
139
+ detached: true,
140
+ stdio: ["ignore", fs.openSync(bridgeLogPath, "a"), fs.openSync(bridgeLogPath, "a")],
141
+ windowsHide: true,
142
+ });
143
+
144
+ fs.writeFileSync(bridgePidPath, `${bridge.pid}\n`);
145
+ bridge.unref();
146
+
147
+ const deadline = Date.now() + 15000;
148
+ while (Date.now() < deadline) {
149
+ if (await isBridgeHealthy(bridgeBaseUrl)) {
150
+ return;
151
+ }
152
+
153
+ await sleep(400);
154
+ }
155
+
156
+ throw new Error(`Bridge did not become healthy. Check ${bridgeLogPath}`);
157
+ }
158
+
159
+ async function isBridgeHealthy(bridgeBaseUrl) {
160
+ try {
161
+ const response = await fetch(`${bridgeBaseUrl}/health`);
162
+ return response.ok;
163
+ } catch {
164
+ return false;
165
+ }
166
+ }
167
+
168
+ function loadEnvFile(filePath) {
169
+ if (!fs.existsSync(filePath)) {
170
+ return {};
171
+ }
172
+
173
+ const result = {};
174
+ const raw = fs.readFileSync(filePath, "utf8");
175
+ for (const line of raw.split(/\r?\n/)) {
176
+ const trimmed = line.trim();
177
+ if (!trimmed || trimmed.startsWith("#")) {
178
+ continue;
179
+ }
180
+
181
+ const separator = trimmed.indexOf("=");
182
+ if (separator === -1) {
183
+ continue;
184
+ }
185
+
186
+ const key = trimmed.slice(0, separator).trim();
187
+ let value = trimmed.slice(separator + 1).trim();
188
+ if (
189
+ (value.startsWith("\"") && value.endsWith("\"")) ||
190
+ (value.startsWith("'") && value.endsWith("'"))
191
+ ) {
192
+ value = value.slice(1, -1);
193
+ }
194
+
195
+ result[key] = value;
196
+ }
197
+
198
+ return result;
199
+ }
200
+
201
+ function safeUnlink(filePath) {
202
+ try {
203
+ fs.unlinkSync(filePath);
204
+ } catch {
205
+ // ignore
206
+ }
207
+ }
208
+
209
+ function sleep(ms) {
210
+ return new Promise((resolve) => setTimeout(resolve, ms));
211
+ }
212
+
213
+ main().catch((error) => {
214
+ console.error(error instanceof Error ? error.message : String(error));
215
+ process.exit(1);
216
+ });