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,73 @@
1
+ $ErrorActionPreference = "Stop"
2
+
3
+ $repoRoot = Split-Path -Parent $PSScriptRoot
4
+ $userProfile = [Environment]::GetFolderPath("UserProfile")
5
+ $appData = [Environment]::GetFolderPath("ApplicationData")
6
+ $workspaceSettingsPath = Join-Path $appData "Code\\User\\settings.json"
7
+ $claudeSettingsPath = Join-Path $userProfile ".claude\\settings.json"
8
+ $bridgePidPath = Join-Path $repoRoot ".runtime\\bridge.pid"
9
+ $launcherExePath = Join-Path $repoRoot "scripts\\ClaudeWrapperLauncher.exe"
10
+
11
+ function Read-JsonFile {
12
+ param([string]$Path)
13
+
14
+ if (-not (Test-Path $Path)) {
15
+ return @{}
16
+ }
17
+
18
+ $raw = Get-Content -LiteralPath $Path -Raw
19
+ if ([string]::IsNullOrWhiteSpace($raw)) {
20
+ return @{}
21
+ }
22
+
23
+ $parsed = $raw | ConvertFrom-Json
24
+ if ($null -eq $parsed) {
25
+ return @{}
26
+ }
27
+
28
+ $result = @{}
29
+ foreach ($property in $parsed.PSObject.Properties) {
30
+ $result[$property.Name] = $property.Value
31
+ }
32
+
33
+ return $result
34
+ }
35
+
36
+ function Write-JsonFile {
37
+ param(
38
+ [string]$Path,
39
+ [hashtable]$Data
40
+ )
41
+
42
+ $json = $Data | ConvertTo-Json -Depth 20
43
+ Set-Content -LiteralPath $Path -Value ($json + "`n") -Encoding UTF8
44
+ }
45
+
46
+ if (Test-Path $workspaceSettingsPath) {
47
+ $settings = Read-JsonFile -Path $workspaceSettingsPath
48
+ [void]$settings.Remove("claudeCode.claudeProcessWrapper")
49
+ [void]$settings.Remove("claudeCode.disableLoginPrompt")
50
+ Write-JsonFile -Path $workspaceSettingsPath -Data $settings
51
+ }
52
+
53
+ if (Test-Path $claudeSettingsPath) {
54
+ $settings = Read-JsonFile -Path $claudeSettingsPath
55
+ [void]$settings.Remove("model")
56
+ [void]$settings.Remove("availableModels")
57
+ Write-JsonFile -Path $claudeSettingsPath -Data $settings
58
+ }
59
+
60
+ if (Test-Path $bridgePidPath) {
61
+ try {
62
+ $pid = [int](Get-Content -LiteralPath $bridgePidPath -Raw).Trim()
63
+ Stop-Process -Id $pid -Force -ErrorAction SilentlyContinue
64
+ } catch {
65
+ }
66
+ Remove-Item -LiteralPath $bridgePidPath -Force -ErrorAction SilentlyContinue
67
+ }
68
+
69
+ if (Test-Path $launcherExePath) {
70
+ Remove-Item -LiteralPath $launcherExePath -Force -ErrorAction SilentlyContinue
71
+ }
72
+
73
+ Write-Host "Configurazione globale rimossa."
@@ -0,0 +1,30 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+
4
+ export function ensureRuntimeDir(projectRoot) {
5
+ fs.mkdirSync(path.join(projectRoot, ".runtime"), { recursive: true });
6
+ }
7
+
8
+ export function ensureEnvLocal(projectRoot, options = {}) {
9
+ const envLocalPath = path.join(projectRoot, ".env.local");
10
+ const envExamplePath = path.join(projectRoot, ".env.example");
11
+ const logger = options.logger ?? console;
12
+
13
+ if (fs.existsSync(envLocalPath)) {
14
+ return { created: false, path: envLocalPath };
15
+ }
16
+
17
+ if (!fs.existsSync(envExamplePath)) {
18
+ fs.writeFileSync(envLocalPath, "OLLAMA_API_KEYS=\nBRIDGE_HOST=127.0.0.1\nBRIDGE_PORT=8788\n", "utf8");
19
+ logger.warn?.(`Created ${envLocalPath}. Add your Ollama API key(s) before starting the bridge.`);
20
+ return { created: true, path: envLocalPath };
21
+ }
22
+
23
+ fs.copyFileSync(envExamplePath, envLocalPath);
24
+ logger.warn?.(`Created ${envLocalPath} from .env.example. Add your real Ollama API key(s) before starting the bridge.`);
25
+ return { created: true, path: envLocalPath };
26
+ }
27
+
28
+ export function getBridgeBaseUrl(config) {
29
+ return `http://${config.host}:${config.port}`;
30
+ }
package/src/cache.js ADDED
@@ -0,0 +1,64 @@
1
+ import NodeCache from 'node-cache';
2
+ import logger from './logger.js';
3
+
4
+ // Cache per i modelli (5 minuti di TTL)
5
+ export const modelCache = new NodeCache({
6
+ stdTTL: 300, // 5 minuti
7
+ checkperiod: 60, // Controlla ogni minuto se ci sono entry scadute
8
+ useClones: false // Performance migliore se non usiamo clones
9
+ });
10
+
11
+ // Cache per le risposte delle API (30 secondi)
12
+ export const apiCache = new NodeCache({
13
+ stdTTL: 30,
14
+ checkperiod: 10,
15
+ useClones: false
16
+ });
17
+
18
+ // Funzione helper per il caching con error handling
19
+ export function getFromCache(cache, key) {
20
+ try {
21
+ const value = cache.get(key);
22
+ if (value !== undefined) {
23
+ logger.debug(`Cache hit for key: ${key}`);
24
+ }
25
+ return value;
26
+ } catch (error) {
27
+ logger.error(`Error reading from cache: ${error.message}`);
28
+ return undefined;
29
+ }
30
+ }
31
+
32
+ export function setInCache(cache, key, value, ttl) {
33
+ try {
34
+ if (ttl) {
35
+ cache.set(key, value, ttl);
36
+ } else {
37
+ cache.set(key, value);
38
+ }
39
+ logger.debug(`Cached value for key: ${key}`);
40
+ } catch (error) {
41
+ logger.error(`Error writing to cache: ${error.message}`);
42
+ }
43
+ }
44
+
45
+ export function deleteFromCache(cache, key) {
46
+ try {
47
+ cache.del(key);
48
+ logger.debug(`Deleted cache for key: ${key}`);
49
+ } catch (error) {
50
+ logger.error(`Error deleting from cache: ${error.message}`);
51
+ }
52
+ }
53
+
54
+ // Stats per il monitoraggio
55
+ export function getCacheStats(cache, name) {
56
+ const stats = cache.getStats();
57
+ return {
58
+ name,
59
+ hits: stats.hits,
60
+ misses: stats.misses,
61
+ keys: cache.keys().length,
62
+ hitRate: stats.hits > 0 ? (stats.hits / (stats.hits + stats.misses) * 100).toFixed(2) : 0
63
+ };
64
+ }
@@ -0,0 +1,73 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import logger from './logger.js';
4
+
5
+ export class ConfigWatcher {
6
+ constructor(configFile, onChange) {
7
+ this.configFile = configFile;
8
+ this.onChange = onChange;
9
+ this.watcher = null;
10
+ this.debounceTimeout = 1000; // 1 secondo
11
+ this.debounceTimer = null;
12
+ }
13
+
14
+ start() {
15
+ if (this.watcher) {
16
+ logger.warn('Config watcher already started');
17
+ return;
18
+ }
19
+
20
+ try {
21
+ this.watcher = fs.watch(this.configFile, (eventType, filename) => {
22
+ if (eventType === 'change') {
23
+ logger.debug(`Config file changed: ${filename}`);
24
+ this.handleChange();
25
+ }
26
+ });
27
+
28
+ logger.info(`Started watching config file: ${this.configFile}`);
29
+ } catch (error) {
30
+ logger.error(`Failed to start config watcher: ${error.message}`);
31
+ }
32
+ }
33
+
34
+ stop() {
35
+ if (this.watcher) {
36
+ this.watcher.close();
37
+ this.watcher = null;
38
+ logger.info('Config watcher stopped');
39
+ }
40
+
41
+ if (this.debounceTimer) {
42
+ clearTimeout(this.debounceTimer);
43
+ this.debounceTimer = null;
44
+ }
45
+ }
46
+
47
+ handleChange() {
48
+ // Debounce per evitare multipli reload rapidi
49
+ if (this.debounceTimer) {
50
+ clearTimeout(this.debounceTimer);
51
+ }
52
+
53
+ this.debounceTimer = setTimeout(async () => {
54
+ try {
55
+ logger.info('Reloading configuration...');
56
+ await this.onChange();
57
+ logger.info('Configuration reloaded successfully');
58
+ } catch (error) {
59
+ logger.error(`Failed to reload configuration: ${error.message}`);
60
+ }
61
+ }, this.debounceTimeout);
62
+ }
63
+ }
64
+
65
+ // Funzione helper per creare un config watcher con reload automatico
66
+ export function createConfigWatcher(configFile, loadConfigFunction) {
67
+ const watcher = new ConfigWatcher(configFile, async () => {
68
+ const newConfig = loadConfigFunction();
69
+ return newConfig;
70
+ });
71
+
72
+ return watcher;
73
+ }
package/src/config.js ADDED
@@ -0,0 +1,98 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+
4
+ const DEFAULT_PORT = 8788;
5
+ const DEFAULT_HOST = "127.0.0.1";
6
+ const DEFAULT_UPSTREAM = "https://ollama.com";
7
+
8
+ export function loadEnvFile(filePath = path.join(process.cwd(), ".env.local")) {
9
+ if (!fs.existsSync(filePath)) {
10
+ return {};
11
+ }
12
+
13
+ const result = {};
14
+ const raw = fs.readFileSync(filePath, "utf8");
15
+ for (const line of raw.split(/\r?\n/)) {
16
+ const trimmed = line.trim();
17
+ if (!trimmed || trimmed.startsWith("#")) {
18
+ continue;
19
+ }
20
+
21
+ const separator = trimmed.indexOf("=");
22
+ if (separator === -1) {
23
+ continue;
24
+ }
25
+
26
+ const key = trimmed.slice(0, separator).trim();
27
+ let value = trimmed.slice(separator + 1).trim();
28
+ if (
29
+ (value.startsWith("\"") && value.endsWith("\"")) ||
30
+ (value.startsWith("'") && value.endsWith("'"))
31
+ ) {
32
+ value = value.slice(1, -1);
33
+ }
34
+
35
+ result[key] = value;
36
+ }
37
+
38
+ return result;
39
+ }
40
+
41
+ function parseKeyList(value) {
42
+ if (!value) {
43
+ return [];
44
+ }
45
+
46
+ return value
47
+ .split(/[,\r?\n]+/)
48
+ .map((entry) => entry.trim())
49
+ .filter(Boolean);
50
+ }
51
+
52
+ function parseAliasMap(value) {
53
+ if (!value) {
54
+ return {};
55
+ }
56
+
57
+ try {
58
+ return JSON.parse(value);
59
+ } catch (error) {
60
+ throw new Error("OLLAMA_MODEL_ALIASES must be valid JSON.");
61
+ }
62
+ }
63
+
64
+ export function saveEnvFile(filePath, envVars) {
65
+ const lines = Object.entries(envVars)
66
+ .map(([key, value]) => `${key}=${value}`)
67
+ .join("\n");
68
+ fs.writeFileSync(filePath, `${lines}\n`, "utf8");
69
+ }
70
+
71
+ export function loadConfig() {
72
+ const fileEnv = loadEnvFile();
73
+ const getValue = (name, fallback = undefined) => {
74
+ if (Object.prototype.hasOwnProperty.call(fileEnv, name)) {
75
+ return fileEnv[name];
76
+ }
77
+
78
+ if (Object.prototype.hasOwnProperty.call(process.env, name)) {
79
+ return process.env[name];
80
+ }
81
+
82
+ return fallback;
83
+ };
84
+
85
+ const apiKeys = parseKeyList(getValue("OLLAMA_API_KEYS") || getValue("OLLAMA_API_KEY"));
86
+ const aliases = parseAliasMap(getValue("OLLAMA_MODEL_ALIASES"));
87
+
88
+ return {
89
+ host: getValue("BRIDGE_HOST", DEFAULT_HOST),
90
+ port: Number.parseInt(getValue("BRIDGE_PORT", `${DEFAULT_PORT}`), 10),
91
+ upstreamBaseUrl: (getValue("OLLAMA_UPSTREAM_BASE_URL", DEFAULT_UPSTREAM)).replace(/\/+$/, ""),
92
+ apiKeys,
93
+ aliases,
94
+ defaultModel:
95
+ getValue("DEFAULT_MODEL_ALIAS") ||
96
+ "ollama-free-auto",
97
+ };
98
+ }
@@ -0,0 +1,80 @@
1
+ import http from 'node:http';
2
+ import https from 'node:https';
3
+ import logger from './logger.js';
4
+
5
+ // Agent per connessioni HTTP (keep-alive abilitato)
6
+ export const httpAgent = new http.Agent({
7
+ keepAlive: true,
8
+ maxSockets: 50, // Massimo 50 connessioni contemporanee
9
+ maxFreeSockets: 10, // Mantieni fino a 10 connessioni aperte in idle
10
+ timeout: 60000, // Timeout di 60 secondi
11
+ freeSocketTimeout: 30000, // Chiudi socket idle dopo 30 secondi
12
+ scheduling: 'lifo' // Last In, First Out per miglior performance
13
+ });
14
+
15
+ // Agent per connessioni HTTPS
16
+ export const httpsAgent = new https.Agent({
17
+ keepAlive: true,
18
+ maxSockets: 50,
19
+ maxFreeSockets: 10,
20
+ timeout: 60000,
21
+ freeSocketTimeout: 30000,
22
+ scheduling: 'lifo'
23
+ });
24
+
25
+ // Funzione helper per ottenere l'agent corretto
26
+ export function getAgent(url) {
27
+ try {
28
+ const parsed = new URL(url);
29
+ return parsed.protocol === 'https:' ? httpsAgent : httpAgent;
30
+ } catch (error) {
31
+ logger.error(`Invalid URL for agent selection: ${error.message}`);
32
+ return httpAgent;
33
+ }
34
+ }
35
+
36
+ // Stats per monitoraggio
37
+ export function getAgentStats() {
38
+ return {
39
+ http: {
40
+ totalSocketCount: httpAgent.totalSocketCount,
41
+ createSocketCount: httpAgent.createSocketCount,
42
+ timeoutSocketCount: httpAgent.timeoutSocketCount,
43
+ requestCount: httpAgent.requestCount,
44
+ freeSockets: Object.keys(httpAgent.freeSockets).length,
45
+ sockets: Object.keys(httpAgent.sockets).length
46
+ },
47
+ https: {
48
+ totalSocketCount: httpsAgent.totalSocketCount,
49
+ createSocketCount: httpsAgent.createSocketCount,
50
+ timeoutSocketCount: httpsAgent.timeoutSocketCount,
51
+ requestCount: httpsAgent.requestCount,
52
+ freeSockets: Object.keys(httpsAgent.freeSockets).length,
53
+ sockets: Object.keys(httpsAgent.sockets).length
54
+ }
55
+ };
56
+ }
57
+
58
+ // Cleanup function per chiudere tutti gli agent
59
+ export function closeAgents() {
60
+ return new Promise((resolve) => {
61
+ let pending = 2;
62
+
63
+ const done = () => {
64
+ if (--pending === 0) {
65
+ logger.info('All HTTP agents closed');
66
+ resolve();
67
+ }
68
+ };
69
+
70
+ httpAgent.destroy(() => {
71
+ logger.debug('HTTP agent destroyed');
72
+ done();
73
+ });
74
+
75
+ httpsAgent.destroy(() => {
76
+ logger.debug('HTTPS agent destroyed');
77
+ done();
78
+ });
79
+ });
80
+ }
@@ -0,0 +1,69 @@
1
+ import logger from './logger.js';
2
+
3
+ export class KeyManager {
4
+ constructor(keys) {
5
+ this.keys = keys.map(key => ({
6
+ key,
7
+ healthy: true,
8
+ lastUsed: null,
9
+ failureCount: 0,
10
+ lastFailure: null
11
+ }));
12
+ this.index = 0;
13
+ this.maxFailures = 3;
14
+ this.failureWindowMs = 5 * 60 * 1000; // 5 minuti
15
+ }
16
+
17
+ async getHealthyKey() {
18
+ const startIndex = this.index;
19
+ const now = Date.now();
20
+
21
+ do {
22
+ const keyInfo = this.keys[this.index];
23
+
24
+ // Resetta lo stato se è passato abbastanza tempo dall'ultimo fallimento
25
+ if (keyInfo.lastFailure && (now - keyInfo.lastFailure) > this.failureWindowMs) {
26
+ keyInfo.failureCount = 0;
27
+ keyInfo.healthy = true;
28
+ }
29
+
30
+ if (keyInfo.healthy && keyInfo.failureCount < this.maxFailures) {
31
+ this.index = (this.index + 1) % this.keys.length;
32
+ keyInfo.lastUsed = now;
33
+ logger.debug(`Using API key at index ${this.index}, failures: ${keyInfo.failureCount}`);
34
+ return keyInfo.key;
35
+ }
36
+
37
+ this.index = (this.index + 1) % this.keys.length;
38
+ } while (this.index !== startIndex);
39
+
40
+ logger.error('No healthy API keys available');
41
+ throw new Error('No healthy API keys available');
42
+ }
43
+
44
+ markKeyFailed(key, error) {
45
+ const keyInfo = this.keys.find(k => k.key === key);
46
+ if (keyInfo) {
47
+ keyInfo.failureCount++;
48
+ keyInfo.lastFailure = Date.now();
49
+ if (keyInfo.failureCount >= this.maxFailures) {
50
+ keyInfo.healthy = false;
51
+ logger.warn(`API key marked as unhealthy after ${this.maxFailures} failures`);
52
+ }
53
+ logger.error(`API key failed (count: ${keyInfo.failureCount}): ${error.message}`);
54
+ }
55
+ }
56
+
57
+ getStats() {
58
+ return {
59
+ total: this.keys.length,
60
+ healthy: this.keys.filter(k => k.healthy).length,
61
+ unhealthy: this.keys.filter(k => !k.healthy).length,
62
+ keys: this.keys.map(k => ({
63
+ healthy: k.healthy,
64
+ failureCount: k.failureCount,
65
+ lastUsed: k.lastUsed
66
+ }))
67
+ };
68
+ }
69
+ }
package/src/logger.js ADDED
@@ -0,0 +1,46 @@
1
+ import winston from 'winston';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+
5
+ const __filename = fileURLToPath(import.meta.url);
6
+ const __dirname = path.dirname(__filename);
7
+
8
+ const logFormat = winston.format.combine(
9
+ winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
10
+ winston.format.errors({ stack: true }),
11
+ winston.format.printf(({ level, message, timestamp, stack }) => {
12
+ return `${timestamp} [${level.toUpperCase()}]: ${stack || message}`;
13
+ })
14
+ );
15
+
16
+ const logger = winston.createLogger({
17
+ level: process.env.LOG_LEVEL || 'info',
18
+ format: logFormat,
19
+ transports: [
20
+ new winston.transports.Console({
21
+ format: winston.format.combine(
22
+ winston.format.colorize(),
23
+ logFormat
24
+ )
25
+ }),
26
+ new winston.transports.File({
27
+ filename: path.join(__dirname, '..', '.runtime', 'error.log'),
28
+ level: 'error',
29
+ maxsize: 5242880, // 5MB
30
+ maxFiles: 5
31
+ }),
32
+ new winston.transports.File({
33
+ filename: path.join(__dirname, '..', '.runtime', 'combined.log'),
34
+ maxsize: 5242880, // 5MB
35
+ maxFiles: 5
36
+ })
37
+ ],
38
+ exceptionHandlers: [
39
+ new winston.transports.File({ filename: path.join(__dirname, '..', '.runtime', 'exceptions.log') })
40
+ ],
41
+ rejectionHandlers: [
42
+ new winston.transports.File({ filename: path.join(__dirname, '..', '.runtime', 'rejections.log') })
43
+ ]
44
+ });
45
+
46
+ export default logger;