@splashcodex/api-key-manager 5.0.2 → 5.3.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/bin/cli.js +154 -24
- package/dist/env/index.d.ts +6 -0
- package/dist/env/index.js +12 -0
- package/dist/env/index.js.map +1 -0
- package/dist/env/loader.d.ts +95 -0
- package/dist/env/loader.js +172 -0
- package/dist/env/loader.js.map +1 -0
- package/dist/index.d.ts +43 -13
- package/dist/index.js +167 -33
- package/dist/index.js.map +1 -1
- package/dist/persistence/file.js +12 -1
- package/dist/persistence/file.js.map +1 -1
- package/dist/presets/anthropic.d.ts +54 -0
- package/dist/presets/anthropic.js +71 -0
- package/dist/presets/anthropic.js.map +1 -0
- package/dist/presets/base.d.ts +6 -0
- package/dist/presets/base.js +62 -7
- package/dist/presets/base.js.map +1 -1
- package/dist/presets/index.d.ts +1 -0
- package/dist/presets/index.js +3 -1
- package/dist/presets/index.js.map +1 -1
- package/dist/presets/multi.js +12 -3
- package/dist/presets/multi.js.map +1 -1
- package/package.json +37 -5
- package/src/env/index.ts +14 -0
- package/src/env/loader.ts +225 -0
- package/src/index.ts +195 -45
- package/src/persistence/file.ts +9 -2
- package/src/presets/anthropic.ts +78 -0
- package/src/presets/base.ts +71 -11
- package/src/presets/index.ts +1 -0
- package/src/presets/multi.ts +14 -4
package/bin/cli.js
CHANGED
|
@@ -1,50 +1,180 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
const fs = require('fs');
|
|
3
3
|
const path = require('path');
|
|
4
|
+
const os = require('os');
|
|
4
5
|
|
|
5
6
|
const args = process.argv.slice(2);
|
|
6
7
|
const command = args[0];
|
|
7
8
|
|
|
9
|
+
const ENV_DIR = path.join(os.homedir(), 'codedex', 'env');
|
|
10
|
+
|
|
8
11
|
function printDocs() {
|
|
9
|
-
console.log(`\x1b[92m
|
|
10
|
-
console.log(`\x1b[
|
|
11
|
-
console.log(`\x1b[
|
|
12
|
-
console.log(` npx @splashcodex/api-key-manager init
|
|
13
|
-
console.log(
|
|
12
|
+
console.log(`\x1b[92m@splashcodex/api-key-manager v5.1\x1b[0m`);
|
|
13
|
+
console.log(`\x1b[96mDocs: https://www.npmjs.com/package/@splashcodex/ApiKeyManager\x1b[0m\n`);
|
|
14
|
+
console.log(`\x1b[93mCommands:\x1b[0m`);
|
|
15
|
+
console.log(` npx @splashcodex/api-key-manager init \x1b[90m# Scaffold demo + central env directory\x1b[0m`);
|
|
16
|
+
console.log(` npx @splashcodex/api-key-manager setup \x1b[90m# Create ~/codedex/env/ with template files\x1b[0m`);
|
|
17
|
+
console.log(` npx @splashcodex/api-key-manager status \x1b[90m# Show which env files are loaded\x1b[0m`);
|
|
18
|
+
console.log('');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function setupEnvDir() {
|
|
22
|
+
console.log(`\n\x1b[96mSetting up centralized env directory...\x1b[0m\n`);
|
|
23
|
+
|
|
24
|
+
if (!fs.existsSync(ENV_DIR)) {
|
|
25
|
+
fs.mkdirSync(ENV_DIR, { recursive: true });
|
|
26
|
+
console.log(`\x1b[92m[+] Created ${ENV_DIR}\x1b[0m`);
|
|
27
|
+
} else {
|
|
28
|
+
console.log(`\x1b[90m[-] ${ENV_DIR} already exists\x1b[0m`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const templates = {
|
|
32
|
+
'common.env': [
|
|
33
|
+
'# Common environment variables shared across all projects',
|
|
34
|
+
'# DATABASE_URL="postgresql://user:pass@localhost:5432/mydb"',
|
|
35
|
+
'# REDIS_URL="redis://localhost:6379"',
|
|
36
|
+
'# SUPABASE_URL="https://your-project.supabase.co"',
|
|
37
|
+
'# SUPABASE_ANON_KEY="your-anon-key"',
|
|
38
|
+
'',
|
|
39
|
+
].join('\n'),
|
|
40
|
+
'llm.env': [
|
|
41
|
+
'# LLM API keys — supports comma-separated or JSON arrays',
|
|
42
|
+
'# GOOGLE_GEMINI_API_KEY="key1,key2,key3"',
|
|
43
|
+
'# OPENAI_API_KEY="sk-..."',
|
|
44
|
+
'# ANTHROPIC_API_KEY="sk-ant-..."',
|
|
45
|
+
'',
|
|
46
|
+
].join('\n'),
|
|
47
|
+
'stripe.env': [
|
|
48
|
+
'# Stripe payment keys',
|
|
49
|
+
'# STRIPE_SECRET_KEY="sk_test_..."',
|
|
50
|
+
'# STRIPE_WEBHOOK_SECRET="whsec_..."',
|
|
51
|
+
'',
|
|
52
|
+
].join('\n'),
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
for (const [name, content] of Object.entries(templates)) {
|
|
56
|
+
const filePath = path.join(ENV_DIR, name);
|
|
57
|
+
if (!fs.existsSync(filePath)) {
|
|
58
|
+
fs.writeFileSync(filePath, content);
|
|
59
|
+
console.log(`\x1b[92m[+] Created ${name}\x1b[0m`);
|
|
60
|
+
} else {
|
|
61
|
+
console.log(`\x1b[90m[-] ${name} already exists\x1b[0m`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
console.log(`\n\x1b[93mNext steps:\x1b[0m`);
|
|
66
|
+
console.log(` 1. Add your real keys to the files in ${ENV_DIR}`);
|
|
67
|
+
console.log(` 2. Back up the folder to Google Drive for portability`);
|
|
68
|
+
console.log(` 3. In your projects, add at the entry point:`);
|
|
69
|
+
console.log(`\x1b[36m import { loadCentralEnv } from '@splashcodex/api-key-manager/env';\x1b[0m`);
|
|
70
|
+
console.log(`\x1b[36m loadCentralEnv();\x1b[0m\n`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function showStatus() {
|
|
74
|
+
console.log(`\n\x1b[96mCentralized env status\x1b[0m\n`);
|
|
75
|
+
console.log(` Directory: ${ENV_DIR}`);
|
|
76
|
+
|
|
77
|
+
if (!fs.existsSync(ENV_DIR)) {
|
|
78
|
+
console.log(` \x1b[31mNot found.\x1b[0m Run: npx @splashcodex/api-key-manager setup\n`);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const files = fs.readdirSync(ENV_DIR).filter(f => f.endsWith('.env')).sort();
|
|
83
|
+
if (files.length === 0) {
|
|
84
|
+
console.log(` \x1b[33mNo .env files found.\x1b[0m\n`);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
let totalVars = 0;
|
|
89
|
+
for (const file of files) {
|
|
90
|
+
const content = fs.readFileSync(path.join(ENV_DIR, file), 'utf-8');
|
|
91
|
+
const vars = content.split('\n')
|
|
92
|
+
.filter(l => l.trim() && !l.trim().startsWith('#'))
|
|
93
|
+
.filter(l => l.includes('='));
|
|
94
|
+
totalVars += vars.length;
|
|
95
|
+
|
|
96
|
+
const icon = vars.length > 0 ? '\x1b[92m' : '\x1b[90m';
|
|
97
|
+
console.log(` ${icon}${file}\x1b[0m — ${vars.length} variable(s)`);
|
|
98
|
+
for (const v of vars) {
|
|
99
|
+
const key = v.split('=')[0].trim();
|
|
100
|
+
console.log(` \x1b[90m${key}\x1b[0m`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
console.log(`\n Total: ${files.length} file(s), ${totalVars} variable(s)\n`);
|
|
14
105
|
}
|
|
15
106
|
|
|
16
107
|
function initProject() {
|
|
17
108
|
const cwd = process.cwd();
|
|
18
|
-
const envPath = path.join(cwd, '.env');
|
|
19
109
|
const demoPath = path.join(cwd, 'demo.ts');
|
|
20
110
|
|
|
21
|
-
console.log(`\n\x1b[
|
|
111
|
+
console.log(`\n\x1b[96mInitializing @splashcodex/api-key-manager...\x1b[0m\n`);
|
|
22
112
|
|
|
23
|
-
//
|
|
24
|
-
|
|
25
|
-
fs.writeFileSync(envPath, `GOOGLE_GEMINI_API_KEY="your_api_key_1,your_api_key_2"\nOPENAI_API_KEY="sk-..."\n`);
|
|
26
|
-
console.log(`\x1b[92m[✔] Created .env file\x1b[0m`);
|
|
27
|
-
} else {
|
|
28
|
-
console.log(`\x1b[90m[-] .env already exists. Remember to add GOOGLE_GEMINI_API_KEY!\x1b[0m`);
|
|
29
|
-
}
|
|
113
|
+
// Ensure central env directory exists
|
|
114
|
+
setupEnvDir();
|
|
30
115
|
|
|
31
116
|
// Create demo.ts
|
|
32
117
|
if (!fs.existsSync(demoPath)) {
|
|
33
|
-
const tsCode =
|
|
118
|
+
const tsCode = `/**
|
|
119
|
+
* Demo: @splashcodex/api-key-manager v5.1
|
|
120
|
+
*
|
|
121
|
+
* Shows the two-layer pattern:
|
|
122
|
+
* Layer 1: loadCentralEnv() loads keys from ~/codedex/env/
|
|
123
|
+
* Layer 2: GeminiManager handles rotation, retries, circuit breaking
|
|
124
|
+
*/
|
|
125
|
+
import { loadCentralEnv } from '@splashcodex/api-key-manager/env';
|
|
126
|
+
import { GeminiManager } from '@splashcodex/api-key-manager/presets/gemini';
|
|
127
|
+
|
|
128
|
+
// Layer 1: Load centralized environment
|
|
129
|
+
const envResult = loadCentralEnv();
|
|
130
|
+
console.log(\`Loaded \${envResult.varsSet} env vars from \${envResult.filesLoaded.length} files\`);
|
|
131
|
+
|
|
132
|
+
async function main() {
|
|
133
|
+
// Layer 2: Initialize key manager (reads from process.env)
|
|
134
|
+
const result = GeminiManager.getInstance();
|
|
135
|
+
if (!result.success) {
|
|
136
|
+
console.error("Failed:", result.error.message);
|
|
137
|
+
console.error("Add your Gemini key to ~/codedex/env/llm.env");
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const gemini = result.data;
|
|
142
|
+
console.log(\`GeminiManager ready: \${gemini.getKeyCount()} key(s)\`);
|
|
143
|
+
|
|
144
|
+
try {
|
|
145
|
+
const response = await gemini.execute(async (key) => {
|
|
146
|
+
console.log(\`Using key: \${key.substring(0, 8)}...\`);
|
|
147
|
+
// Replace with actual API call
|
|
148
|
+
return "Simulated API Response";
|
|
149
|
+
}, { maxRetries: 3, timeoutMs: 30000 });
|
|
150
|
+
console.log("Result:", response);
|
|
151
|
+
} catch (e: any) {
|
|
152
|
+
console.error("Failed:", e.message);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
main();
|
|
157
|
+
`;
|
|
34
158
|
fs.writeFileSync(demoPath, tsCode);
|
|
35
|
-
console.log(`\x1b[92m[
|
|
159
|
+
console.log(`\x1b[92m[+] Created demo.ts\x1b[0m`);
|
|
36
160
|
} else {
|
|
37
|
-
console.log(`\x1b[90m[-] demo.ts already exists
|
|
161
|
+
console.log(`\x1b[90m[-] demo.ts already exists\x1b[0m`);
|
|
38
162
|
}
|
|
39
163
|
|
|
40
|
-
console.log(`\n\x1b[
|
|
41
|
-
console.log(`To run the demo:`);
|
|
42
|
-
console.log(`\x1b[36m npm install dotenv\x1b[0m`);
|
|
164
|
+
console.log(`\n\x1b[93mTo run the demo:\x1b[0m`);
|
|
43
165
|
console.log(`\x1b[36m npx ts-node demo.ts\x1b[0m\n`);
|
|
44
166
|
}
|
|
45
167
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
168
|
+
switch (command) {
|
|
169
|
+
case 'init':
|
|
170
|
+
initProject();
|
|
171
|
+
break;
|
|
172
|
+
case 'setup':
|
|
173
|
+
setupEnvDir();
|
|
174
|
+
break;
|
|
175
|
+
case 'status':
|
|
176
|
+
showStatus();
|
|
177
|
+
break;
|
|
178
|
+
default:
|
|
179
|
+
printDocs();
|
|
50
180
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getDefaultEnvDir = exports.getCentralEnvVar = exports.loadCentralEnv = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Centralized Environment Loader
|
|
6
|
+
* @module env
|
|
7
|
+
*/
|
|
8
|
+
var loader_1 = require("./loader");
|
|
9
|
+
Object.defineProperty(exports, "loadCentralEnv", { enumerable: true, get: function () { return loader_1.loadCentralEnv; } });
|
|
10
|
+
Object.defineProperty(exports, "getCentralEnvVar", { enumerable: true, get: function () { return loader_1.getCentralEnvVar; } });
|
|
11
|
+
Object.defineProperty(exports, "getDefaultEnvDir", { enumerable: true, get: function () { return loader_1.getDefaultEnvDir; } });
|
|
12
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/env/index.ts"],"names":[],"mappings":";;;AAAA;;;GAGG;AACH,mCAIkB;AAHd,wGAAA,cAAc,OAAA;AACd,0GAAA,gBAAgB,OAAA;AAChB,0GAAA,gBAAgB,OAAA"}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Centralized Environment Loader
|
|
3
|
+
*
|
|
4
|
+
* Loads environment variables from ~/codedex/env/ so that all 35+ projects
|
|
5
|
+
* share a single source of truth for secrets. Portable via Google Drive.
|
|
6
|
+
*
|
|
7
|
+
* Directory structure:
|
|
8
|
+
* ~/codedex/env/
|
|
9
|
+
* common.env — REDIS_URL, DATABASE_URL, shared across all projects
|
|
10
|
+
* llm.env — GEMINI_API_KEY, OPENAI_API_KEY (comma-separated or JSON arrays)
|
|
11
|
+
* stripe.env — STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET
|
|
12
|
+
* <custom>.env — Any additional groupings
|
|
13
|
+
*
|
|
14
|
+
* @module env/loader
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```ts
|
|
18
|
+
* // Load all env files from ~/codedex/env/
|
|
19
|
+
* import { loadCentralEnv } from '@splashcodex/api-key-manager/env';
|
|
20
|
+
* loadCentralEnv();
|
|
21
|
+
*
|
|
22
|
+
* // Now process.env has all the values
|
|
23
|
+
* console.log(process.env.GEMINI_API_KEY);
|
|
24
|
+
* ```
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```ts
|
|
28
|
+
* // Load specific files only
|
|
29
|
+
* import { loadCentralEnv } from '@splashcodex/api-key-manager/env';
|
|
30
|
+
* loadCentralEnv({ files: ['llm.env', 'common.env'] });
|
|
31
|
+
* ```
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* ```ts
|
|
35
|
+
* // Custom env directory (e.g., team-shared location)
|
|
36
|
+
* import { loadCentralEnv } from '@splashcodex/api-key-manager/env';
|
|
37
|
+
* loadCentralEnv({ envDir: '/shared/team/env' });
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
export interface LoadCentralEnvOptions {
|
|
41
|
+
/**
|
|
42
|
+
* Path to the centralized env directory.
|
|
43
|
+
* Defaults to ~/codedex/env/
|
|
44
|
+
*
|
|
45
|
+
* Can also be set via CODEDEX_ENV_DIR environment variable.
|
|
46
|
+
*/
|
|
47
|
+
envDir?: string;
|
|
48
|
+
/**
|
|
49
|
+
* Specific .env files to load (e.g., ['llm.env', 'common.env']).
|
|
50
|
+
* If omitted, loads ALL .env files in the directory.
|
|
51
|
+
*/
|
|
52
|
+
files?: string[];
|
|
53
|
+
/**
|
|
54
|
+
* If true, central env values will NOT overwrite existing process.env values.
|
|
55
|
+
* Useful when a project needs to override a shared value locally.
|
|
56
|
+
* Default: false (central values take precedence over local .env)
|
|
57
|
+
*/
|
|
58
|
+
preserveExisting?: boolean;
|
|
59
|
+
/**
|
|
60
|
+
* If true, silently continue when the env directory doesn't exist.
|
|
61
|
+
* If false, throw an error.
|
|
62
|
+
* Default: true (silent — for portability across dev environments)
|
|
63
|
+
*/
|
|
64
|
+
silent?: boolean;
|
|
65
|
+
}
|
|
66
|
+
export interface LoadResult {
|
|
67
|
+
/** Whether the env directory was found and loaded */
|
|
68
|
+
loaded: boolean;
|
|
69
|
+
/** Path to the env directory that was used */
|
|
70
|
+
envDir: string;
|
|
71
|
+
/** List of .env files that were loaded */
|
|
72
|
+
filesLoaded: string[];
|
|
73
|
+
/** Number of environment variables set */
|
|
74
|
+
varsSet: number;
|
|
75
|
+
/** Variables that were skipped because they already existed (when preserveExisting=true) */
|
|
76
|
+
varsSkipped: string[];
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Default env directory path: ~/codedex/env/
|
|
80
|
+
*/
|
|
81
|
+
export declare function getDefaultEnvDir(): string;
|
|
82
|
+
/**
|
|
83
|
+
* Load environment variables from the centralized env directory into process.env.
|
|
84
|
+
*
|
|
85
|
+
* Call this once at the top of your app's entry point, before any code
|
|
86
|
+
* that reads process.env.
|
|
87
|
+
*/
|
|
88
|
+
export declare function loadCentralEnv(options?: LoadCentralEnvOptions): LoadResult;
|
|
89
|
+
/**
|
|
90
|
+
* Read a single value from the centralized env without loading everything.
|
|
91
|
+
* Useful for one-off lookups.
|
|
92
|
+
*/
|
|
93
|
+
export declare function getCentralEnvVar(key: string, options?: {
|
|
94
|
+
envDir?: string;
|
|
95
|
+
}): string | undefined;
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Centralized Environment Loader
|
|
4
|
+
*
|
|
5
|
+
* Loads environment variables from ~/codedex/env/ so that all 35+ projects
|
|
6
|
+
* share a single source of truth for secrets. Portable via Google Drive.
|
|
7
|
+
*
|
|
8
|
+
* Directory structure:
|
|
9
|
+
* ~/codedex/env/
|
|
10
|
+
* common.env — REDIS_URL, DATABASE_URL, shared across all projects
|
|
11
|
+
* llm.env — GEMINI_API_KEY, OPENAI_API_KEY (comma-separated or JSON arrays)
|
|
12
|
+
* stripe.env — STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET
|
|
13
|
+
* <custom>.env — Any additional groupings
|
|
14
|
+
*
|
|
15
|
+
* @module env/loader
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```ts
|
|
19
|
+
* // Load all env files from ~/codedex/env/
|
|
20
|
+
* import { loadCentralEnv } from '@splashcodex/api-key-manager/env';
|
|
21
|
+
* loadCentralEnv();
|
|
22
|
+
*
|
|
23
|
+
* // Now process.env has all the values
|
|
24
|
+
* console.log(process.env.GEMINI_API_KEY);
|
|
25
|
+
* ```
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```ts
|
|
29
|
+
* // Load specific files only
|
|
30
|
+
* import { loadCentralEnv } from '@splashcodex/api-key-manager/env';
|
|
31
|
+
* loadCentralEnv({ files: ['llm.env', 'common.env'] });
|
|
32
|
+
* ```
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* ```ts
|
|
36
|
+
* // Custom env directory (e.g., team-shared location)
|
|
37
|
+
* import { loadCentralEnv } from '@splashcodex/api-key-manager/env';
|
|
38
|
+
* loadCentralEnv({ envDir: '/shared/team/env' });
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
42
|
+
exports.getDefaultEnvDir = getDefaultEnvDir;
|
|
43
|
+
exports.loadCentralEnv = loadCentralEnv;
|
|
44
|
+
exports.getCentralEnvVar = getCentralEnvVar;
|
|
45
|
+
const fs_1 = require("fs");
|
|
46
|
+
const path_1 = require("path");
|
|
47
|
+
const os_1 = require("os");
|
|
48
|
+
const dotenv_1 = require("dotenv");
|
|
49
|
+
// ─── Parser ──────────────────────────────────────────────────────────────────
|
|
50
|
+
/**
|
|
51
|
+
* Parse a .env file content string into key-value pairs.
|
|
52
|
+
*
|
|
53
|
+
* Delegates to `dotenv.parse()` for battle-tested parsing that handles:
|
|
54
|
+
* KEY=value
|
|
55
|
+
* KEY="quoted value with spaces"
|
|
56
|
+
* KEY='single quoted'
|
|
57
|
+
* KEY=`backtick quoted`
|
|
58
|
+
* MULTILINE="line1\nline2" (real newlines inside double quotes)
|
|
59
|
+
* KEY=value # inline comment
|
|
60
|
+
* export KEY=value (bash-style)
|
|
61
|
+
* # full-line comments
|
|
62
|
+
* \n, \r, \t escape sequences
|
|
63
|
+
*
|
|
64
|
+
* Returns a Map<string, string> for compatibility with the rest of the loader.
|
|
65
|
+
*/
|
|
66
|
+
function parseEnvFile(content) {
|
|
67
|
+
const parsed = (0, dotenv_1.parse)(content);
|
|
68
|
+
return new Map(Object.entries(parsed));
|
|
69
|
+
}
|
|
70
|
+
// ─── Loader ──────────────────────────────────────────────────────────────────
|
|
71
|
+
/**
|
|
72
|
+
* Default env directory path: ~/codedex/env/
|
|
73
|
+
*/
|
|
74
|
+
function getDefaultEnvDir() {
|
|
75
|
+
return process.env.CODEDEX_ENV_DIR || (0, path_1.join)((0, os_1.homedir)(), 'codedex', 'env');
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Load environment variables from the centralized env directory into process.env.
|
|
79
|
+
*
|
|
80
|
+
* Call this once at the top of your app's entry point, before any code
|
|
81
|
+
* that reads process.env.
|
|
82
|
+
*/
|
|
83
|
+
function loadCentralEnv(options = {}) {
|
|
84
|
+
const envDir = options.envDir || getDefaultEnvDir();
|
|
85
|
+
const silent = options.silent !== false;
|
|
86
|
+
const preserveExisting = options.preserveExisting === true;
|
|
87
|
+
const result = {
|
|
88
|
+
loaded: false,
|
|
89
|
+
envDir,
|
|
90
|
+
filesLoaded: [],
|
|
91
|
+
varsSet: 0,
|
|
92
|
+
varsSkipped: [],
|
|
93
|
+
};
|
|
94
|
+
// Check if directory exists
|
|
95
|
+
if (!(0, fs_1.existsSync)(envDir)) {
|
|
96
|
+
if (!silent) {
|
|
97
|
+
throw new Error(`Centralized env directory not found: ${envDir}\n` +
|
|
98
|
+
`Create it with: mkdir -p ${envDir}\n` +
|
|
99
|
+
`Or set CODEDEX_ENV_DIR to point to your env directory.`);
|
|
100
|
+
}
|
|
101
|
+
return result;
|
|
102
|
+
}
|
|
103
|
+
// Determine which files to load
|
|
104
|
+
let filesToLoad;
|
|
105
|
+
if (options.files) {
|
|
106
|
+
filesToLoad = options.files;
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
try {
|
|
110
|
+
filesToLoad = (0, fs_1.readdirSync)(envDir)
|
|
111
|
+
.filter(f => f.endsWith('.env'))
|
|
112
|
+
.sort(); // Alphabetical order for deterministic loading
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
if (!silent)
|
|
116
|
+
throw new Error(`Cannot read env directory: ${envDir}`);
|
|
117
|
+
return result;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
// Load each file
|
|
121
|
+
for (const fileName of filesToLoad) {
|
|
122
|
+
const filePath = (0, path_1.join)(envDir, fileName);
|
|
123
|
+
if (!(0, fs_1.existsSync)(filePath)) {
|
|
124
|
+
if (!silent) {
|
|
125
|
+
throw new Error(`Env file not found: ${filePath}`);
|
|
126
|
+
}
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
try {
|
|
130
|
+
const content = (0, fs_1.readFileSync)(filePath, 'utf-8');
|
|
131
|
+
const vars = parseEnvFile(content);
|
|
132
|
+
for (const [key, value] of vars) {
|
|
133
|
+
if (preserveExisting && process.env[key] !== undefined) {
|
|
134
|
+
result.varsSkipped.push(key);
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
process.env[key] = value;
|
|
138
|
+
result.varsSet++;
|
|
139
|
+
}
|
|
140
|
+
result.filesLoaded.push(fileName);
|
|
141
|
+
}
|
|
142
|
+
catch (err) {
|
|
143
|
+
if (!silent)
|
|
144
|
+
throw err;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
result.loaded = result.filesLoaded.length > 0;
|
|
148
|
+
return result;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Read a single value from the centralized env without loading everything.
|
|
152
|
+
* Useful for one-off lookups.
|
|
153
|
+
*/
|
|
154
|
+
function getCentralEnvVar(key, options) {
|
|
155
|
+
const envDir = options?.envDir || getDefaultEnvDir();
|
|
156
|
+
if (!(0, fs_1.existsSync)(envDir))
|
|
157
|
+
return undefined;
|
|
158
|
+
try {
|
|
159
|
+
const files = (0, fs_1.readdirSync)(envDir).filter(f => f.endsWith('.env')).sort();
|
|
160
|
+
for (const fileName of files) {
|
|
161
|
+
const content = (0, fs_1.readFileSync)((0, path_1.join)(envDir, fileName), 'utf-8');
|
|
162
|
+
const vars = parseEnvFile(content);
|
|
163
|
+
if (vars.has(key))
|
|
164
|
+
return vars.get(key);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
// Silent fail
|
|
169
|
+
}
|
|
170
|
+
return undefined;
|
|
171
|
+
}
|
|
172
|
+
//# sourceMappingURL=loader.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"loader.js","sourceRoot":"","sources":["../../src/env/loader.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;;AAgFH,4CAEC;AAQD,wCAwEC;AAMD,4CAkBC;AAxLD,2BAA2D;AAC3D,+BAA4B;AAC5B,2BAA6B;AAC7B,mCAA8C;AA+C9C,gFAAgF;AAEhF;;;;;;;;;;;;;;;GAeG;AACH,SAAS,YAAY,CAAC,OAAe;IACjC,MAAM,MAAM,GAAG,IAAA,cAAW,EAAC,OAAO,CAAC,CAAC;IACpC,OAAO,IAAI,GAAG,CAAC,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC;AAC3C,CAAC;AAED,gFAAgF;AAEhF;;GAEG;AACH,SAAgB,gBAAgB;IAC5B,OAAO,OAAO,CAAC,GAAG,CAAC,eAAe,IAAI,IAAA,WAAI,EAAC,IAAA,YAAO,GAAE,EAAE,SAAS,EAAE,KAAK,CAAC,CAAC;AAC5E,CAAC;AAED;;;;;GAKG;AACH,SAAgB,cAAc,CAAC,UAAiC,EAAE;IAC9D,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,IAAI,gBAAgB,EAAE,CAAC;IACpD,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,KAAK,KAAK,CAAC;IACxC,MAAM,gBAAgB,GAAG,OAAO,CAAC,gBAAgB,KAAK,IAAI,CAAC;IAE3D,MAAM,MAAM,GAAe;QACvB,MAAM,EAAE,KAAK;QACb,MAAM;QACN,WAAW,EAAE,EAAE;QACf,OAAO,EAAE,CAAC;QACV,WAAW,EAAE,EAAE;KAClB,CAAC;IAEF,4BAA4B;IAC5B,IAAI,CAAC,IAAA,eAAU,EAAC,MAAM,CAAC,EAAE,CAAC;QACtB,IAAI,CAAC,MAAM,EAAE,CAAC;YACV,MAAM,IAAI,KAAK,CACX,wCAAwC,MAAM,IAAI;gBAClD,4BAA4B,MAAM,IAAI;gBACtC,wDAAwD,CAC3D,CAAC;QACN,CAAC;QACD,OAAO,MAAM,CAAC;IAClB,CAAC;IAED,gCAAgC;IAChC,IAAI,WAAqB,CAAC;IAC1B,IAAI,OAAO,CAAC,KAAK,EAAE,CAAC;QAChB,WAAW,GAAG,OAAO,CAAC,KAAK,CAAC;IAChC,CAAC;SAAM,CAAC;QACJ,IAAI,CAAC;YACD,WAAW,GAAG,IAAA,gBAAW,EAAC,MAAM,CAAC;iBAC5B,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;iBAC/B,IAAI,EAAE,CAAC,CAAC,+CAA+C;QAChE,CAAC;QAAC,MAAM,CAAC;YACL,IAAI,CAAC,MAAM;gBAAE,MAAM,IAAI,KAAK,CAAC,8BAA8B,MAAM,EAAE,CAAC,CAAC;YACrE,OAAO,MAAM,CAAC;QAClB,CAAC;IACL,CAAC;IAED,iBAAiB;IACjB,KAAK,MAAM,QAAQ,IAAI,WAAW,EAAE,CAAC;QACjC,MAAM,QAAQ,GAAG,IAAA,WAAI,EAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;QAExC,IAAI,CAAC,IAAA,eAAU,EAAC,QAAQ,CAAC,EAAE,CAAC;YACxB,IAAI,CAAC,MAAM,EAAE,CAAC;gBACV,MAAM,IAAI,KAAK,CAAC,uBAAuB,QAAQ,EAAE,CAAC,CAAC;YACvD,CAAC;YACD,SAAS;QACb,CAAC;QAED,IAAI,CAAC;YACD,MAAM,OAAO,GAAG,IAAA,iBAAY,EAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;YAChD,MAAM,IAAI,GAAG,YAAY,CAAC,OAAO,CAAC,CAAC;YAEnC,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,IAAI,EAAE,CAAC;gBAC9B,IAAI,gBAAgB,IAAI,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,KAAK,SAAS,EAAE,CAAC;oBACrD,MAAM,CAAC,WAAW,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;oBAC7B,SAAS;gBACb,CAAC;gBACD,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;gBACzB,MAAM,CAAC,OAAO,EAAE,CAAC;YACrB,CAAC;YAED,MAAM,CAAC,WAAW,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QACtC,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACX,IAAI,CAAC,MAAM;gBAAE,MAAM,GAAG,CAAC;QAC3B,CAAC;IACL,CAAC;IAED,MAAM,CAAC,MAAM,GAAG,MAAM,CAAC,WAAW,CAAC,MAAM,GAAG,CAAC,CAAC;IAC9C,OAAO,MAAM,CAAC;AAClB,CAAC;AAED;;;GAGG;AACH,SAAgB,gBAAgB,CAAC,GAAW,EAAE,OAA6B;IACvE,MAAM,MAAM,GAAG,OAAO,EAAE,MAAM,IAAI,gBAAgB,EAAE,CAAC;IAErD,IAAI,CAAC,IAAA,eAAU,EAAC,MAAM,CAAC;QAAE,OAAO,SAAS,CAAC;IAE1C,IAAI,CAAC;QACD,MAAM,KAAK,GAAG,IAAA,gBAAW,EAAC,MAAM,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;QAEzE,KAAK,MAAM,QAAQ,IAAI,KAAK,EAAE,CAAC;YAC3B,MAAM,OAAO,GAAG,IAAA,iBAAY,EAAC,IAAA,WAAI,EAAC,MAAM,EAAE,QAAQ,CAAC,EAAE,OAAO,CAAC,CAAC;YAC9D,MAAM,IAAI,GAAG,YAAY,CAAC,OAAO,CAAC,CAAC;YACnC,IAAI,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC;gBAAE,OAAO,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAC5C,CAAC;IACL,CAAC;IAAC,MAAM,CAAC;QACL,cAAc;IAClB,CAAC;IAED,OAAO,SAAS,CAAC;AACrB,CAAC"}
|
package/dist/index.d.ts
CHANGED
|
@@ -6,8 +6,10 @@
|
|
|
6
6
|
* NEW in v5.0: Provider Presets (GeminiManager, OpenAIManager, MultiManager),
|
|
7
7
|
* Built-in Persistence (FileStorage, MemoryStorage)
|
|
8
8
|
* Gemini-Specific: finishReason handling, Safety blocks, RECITATION detection
|
|
9
|
+
* Infrastructure: cockatiel (Bulkhead queueing + ExponentialBackoff w/ decorrelated jitter)
|
|
9
10
|
*/
|
|
10
11
|
import { EventEmitter } from 'events';
|
|
12
|
+
import { z } from 'zod';
|
|
11
13
|
export { FileStorage } from './persistence/file';
|
|
12
14
|
export type { FileStorageOptions } from './persistence/file';
|
|
13
15
|
export { MemoryStorage } from './persistence/memory';
|
|
@@ -48,17 +50,19 @@ export interface ExecuteOptions {
|
|
|
48
50
|
finishReason?: string;
|
|
49
51
|
provider?: string;
|
|
50
52
|
}
|
|
51
|
-
export
|
|
52
|
-
storage
|
|
53
|
-
strategy
|
|
54
|
-
fallbackFn
|
|
55
|
-
concurrency
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
}
|
|
53
|
+
export declare const ApiKeyManagerOptionsSchema: z.ZodObject<{
|
|
54
|
+
storage: z.ZodOptional<z.ZodAny>;
|
|
55
|
+
strategy: z.ZodOptional<z.ZodAny>;
|
|
56
|
+
fallbackFn: z.ZodOptional<z.ZodCustom<() => any, () => any>>;
|
|
57
|
+
concurrency: z.ZodOptional<z.ZodNumber>;
|
|
58
|
+
concurrencyQueueSize: z.ZodOptional<z.ZodNumber>;
|
|
59
|
+
semanticCache: z.ZodOptional<z.ZodObject<{
|
|
60
|
+
threshold: z.ZodOptional<z.ZodNumber>;
|
|
61
|
+
ttlMs: z.ZodOptional<z.ZodNumber>;
|
|
62
|
+
getEmbedding: z.ZodCustom<(text: string) => Promise<number[]>, (text: string) => Promise<number[]>>;
|
|
63
|
+
}, z.core.$strip>>;
|
|
64
|
+
}, z.core.$loose>;
|
|
65
|
+
export type ApiKeyManagerOptions = z.infer<typeof ApiKeyManagerOptionsSchema>;
|
|
62
66
|
export interface CacheEntry {
|
|
63
67
|
vector: number[];
|
|
64
68
|
prompt: string;
|
|
@@ -135,10 +139,11 @@ export declare class ApiKeyManager extends EventEmitter {
|
|
|
135
139
|
private storage;
|
|
136
140
|
private strategy;
|
|
137
141
|
private fallbackFn?;
|
|
138
|
-
private
|
|
139
|
-
private activeCalls;
|
|
142
|
+
private bulkheadPolicy;
|
|
140
143
|
private healthCheckFn?;
|
|
141
144
|
private healthCheckInterval?;
|
|
145
|
+
private _saveTimer?;
|
|
146
|
+
private _saveDirty;
|
|
142
147
|
private semanticCache?;
|
|
143
148
|
private getEmbeddingFn?;
|
|
144
149
|
private _isResolvingEmbedding;
|
|
@@ -175,6 +180,15 @@ export declare class ApiKeyManager extends EventEmitter {
|
|
|
175
180
|
markSuccess(key: string, durationMs?: number): void;
|
|
176
181
|
markFailed(key: string, classification: ErrorClassification): void;
|
|
177
182
|
markFailedLegacy(key: string, isQuota?: boolean): void;
|
|
183
|
+
/**
|
|
184
|
+
* Calculate exponential backoff with decorrelated jitter using cockatiel.
|
|
185
|
+
* Decorrelated jitter avoids thundering-herd by randomizing retry intervals
|
|
186
|
+
* independent of the previous delay, which is statistically superior to
|
|
187
|
+
* simple `random * exponential` jitter.
|
|
188
|
+
*
|
|
189
|
+
* @param attempt - Zero-indexed attempt number
|
|
190
|
+
*/
|
|
191
|
+
private readonly _backoffFactory;
|
|
178
192
|
calculateBackoff(attempt: number): number;
|
|
179
193
|
getStats(): ApiKeyManagerStats;
|
|
180
194
|
_getKeys(): KeyState[];
|
|
@@ -205,6 +219,11 @@ export declare class ApiKeyManager extends EventEmitter {
|
|
|
205
219
|
executeStream<T>(fn: (key: string, signal?: AbortSignal) => AsyncGenerator<T, any, unknown>, options?: ExecuteOptions & {
|
|
206
220
|
prompt?: string;
|
|
207
221
|
}): AsyncGenerator<T, any, unknown>;
|
|
222
|
+
/**
|
|
223
|
+
* Helper: stores result in semantic cache then returns it.
|
|
224
|
+
* Called by the bulkhead execute() callback and the no-bulkhead path.
|
|
225
|
+
*/
|
|
226
|
+
private _executeWithSemanticAndRetry;
|
|
208
227
|
private _executeWithRetry;
|
|
209
228
|
private _executeWithTimeout;
|
|
210
229
|
private _sleep;
|
|
@@ -222,6 +241,17 @@ export declare class ApiKeyManager extends EventEmitter {
|
|
|
222
241
|
*/
|
|
223
242
|
stopHealthChecks(): void;
|
|
224
243
|
private _runHealthChecks;
|
|
244
|
+
/**
|
|
245
|
+
* Debounced save — marks state as dirty and flushes after 500ms of inactivity.
|
|
246
|
+
* Under heavy load (multiple getKey/markSuccess/markFailed calls), this coalesces
|
|
247
|
+
* dozens of writeFileSync calls into one.
|
|
248
|
+
*/
|
|
225
249
|
private saveState;
|
|
250
|
+
/**
|
|
251
|
+
* Immediately flush state to storage. Called by the debounce timer
|
|
252
|
+
* and by stopHealthChecks() to ensure clean shutdown.
|
|
253
|
+
*/
|
|
254
|
+
flushState(): void;
|
|
255
|
+
private _flushState;
|
|
226
256
|
private loadState;
|
|
227
257
|
}
|