devlensio 0.4.4 → 0.6.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/LICENSE +0 -0
- package/README.md +212 -74
- package/dist/config/index.d.ts +14 -2
- package/dist/config/index.js +77 -2
- package/dist/config/providers/catalog.d.ts +27 -0
- package/dist/config/providers/catalog.js +130 -0
- package/dist/config/providers/file.d.ts +4 -2
- package/dist/config/providers/file.js +198 -32
- package/dist/config/providers/paths.d.ts +2 -0
- package/dist/config/providers/paths.js +4 -0
- package/dist/config/providers/providers.default.d.ts +3 -0
- package/dist/config/providers/providers.default.js +12 -0
- package/dist/config/providers/request.js +21 -9
- package/dist/config/types.d.ts +25 -2
- package/dist/config/types.js +22 -7
- package/dist/config/writer.d.ts +8 -2
- package/dist/config/writer.js +59 -12
- package/dist/filesystem/backendRoutes.d.ts +2 -2
- package/dist/filesystem/backendRoutes.js +317 -18
- package/dist/filesystem/index.js +7 -3
- package/dist/filesystem/index.test.js +274 -0
- package/dist/fingerprint/detectors.js +8 -2
- package/dist/fingerprint/index.test.js +54 -0
- package/dist/graph/edges/callEdges.js +12 -2
- package/dist/index.d.ts +6 -1
- package/dist/index.js +5 -1
- package/dist/parser/extractors/components.js +3 -0
- package/dist/parser/extractors/functions.d.ts +1 -0
- package/dist/parser/extractors/functions.js +11 -2
- package/dist/parser/extractors/hooks.js +5 -0
- package/dist/parser/index.test.js +40 -0
- package/dist/summarizer/providers/anthropic.d.ts +2 -1
- package/dist/summarizer/providers/anthropic.js +3 -2
- package/dist/summarizer/providers/index.js +19 -35
- package/dist/summarizer/providers/models.d.ts +11 -0
- package/dist/summarizer/providers/models.js +113 -0
- package/dist/summarizer/providers/openai.d.ts +2 -1
- package/dist/summarizer/providers/openai.js +2 -1
- package/dist/summarizer/providers/types.d.ts +1 -6
- package/dist/types.d.ts +2 -2
- package/package.json +1 -1
- package/dist/summarizer/providers/gemini.d.ts +0 -9
- package/dist/summarizer/providers/gemini.js +0 -79
- package/dist/summarizer/providers/ollama.d.ts +0 -9
- package/dist/summarizer/providers/ollama.js +0 -23
- package/dist/summarizer/providers/openRouter.d.ts +0 -9
- package/dist/summarizer/providers/openRouter.js +0 -19
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
import fs from "fs";
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
2
|
+
import { CONFIG_FILE } from "./paths.js";
|
|
3
|
+
import { ANTHROPIC_DEFAULTS, makeProviderKey, } from "../types.js";
|
|
4
|
+
import { findProvider } from "./catalog.js";
|
|
5
|
+
export { CONFIG_DIR, CONFIG_FILE } from "./paths.js";
|
|
5
6
|
// ─── Constants ────────────────────────────────────────────────────────────────
|
|
6
|
-
export const CONFIG_DIR = path.join(os.homedir(), ".devlens");
|
|
7
|
-
export const CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
|
|
8
|
-
// ─── Environment Variable Names ───────────────────────────────────────────────
|
|
9
7
|
//
|
|
10
8
|
// For Docker users who prefer env vars over config files.
|
|
11
9
|
// A Docker user running Ollama in the same network would set:
|
|
@@ -17,6 +15,7 @@ export const CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
|
|
|
17
15
|
export const ENV = {
|
|
18
16
|
// Summarization
|
|
19
17
|
LLM_PROVIDER: "DEVLENS_LLM_PROVIDER", //here DEVLENS_LLM_PROVIDER is the actual env variable
|
|
18
|
+
LLM_PROVIDER_NAME: "DEVLENS_LLM_PROVIDER_NAME",
|
|
20
19
|
LLM_MODEL: "DEVLENS_LLM_MODEL",
|
|
21
20
|
LLM_KEY: "DEVLENS_LLM_KEY",
|
|
22
21
|
LLM_BASE_URL: "DEVLENS_LLM_BASE_URL",
|
|
@@ -32,7 +31,7 @@ export const ENV = {
|
|
|
32
31
|
NEO4J_PASSWORD: "DEVLENS_NEO4J_PASSWORD",
|
|
33
32
|
NEO4J_STORECODE: "NEO4J_STORE_CODE"
|
|
34
33
|
};
|
|
35
|
-
//
|
|
34
|
+
// Deep Merge
|
|
36
35
|
//
|
|
37
36
|
// Merges user's partial config on top of defaults.
|
|
38
37
|
// Does NOT mutate either argument — returns a new object.
|
|
@@ -60,7 +59,73 @@ function deepMerge(base, partial) {
|
|
|
60
59
|
: base.neo4j,
|
|
61
60
|
};
|
|
62
61
|
}
|
|
63
|
-
// ───
|
|
62
|
+
// ─── v1→v2 Provider Migration ──────────────────────────────────────────────
|
|
63
|
+
// Old configs stored brand strings ("openrouter", "ollama", etc.) in
|
|
64
|
+
// `provider`. In v2, provider is the wire protocol and the brand goes in
|
|
65
|
+
// `providerName`. This maps old values via the catalog and persists the fix.
|
|
66
|
+
function migrateProviderConfig(config) {
|
|
67
|
+
const p = config.summarization.provider;
|
|
68
|
+
if (p === "openai" || p === "anthropic")
|
|
69
|
+
return config;
|
|
70
|
+
const entry = findProvider(p);
|
|
71
|
+
if (!entry)
|
|
72
|
+
throw new Error(`DevLens: "${p}" is not a valid provider protocol. Wire protocol must be "openai" or "anthropic". ` +
|
|
73
|
+
`Fix: set "provider" to "openai" and "providerName" to "${p}" in ~/.devlens/config.json`);
|
|
74
|
+
const migrated = { ...config, summarization: { ...config.summarization, provider: entry.protocol, providerName: entry.name } };
|
|
75
|
+
try {
|
|
76
|
+
const raw = JSON.parse(fs.readFileSync(CONFIG_FILE, "utf-8"));
|
|
77
|
+
raw.summarization = { ...raw.summarization, provider: entry.protocol };
|
|
78
|
+
if (!raw.summarization.providerName)
|
|
79
|
+
raw.summarization.providerName = entry.name;
|
|
80
|
+
const tmp = CONFIG_FILE + ".tmp";
|
|
81
|
+
fs.writeFileSync(tmp, JSON.stringify(raw, null, 2));
|
|
82
|
+
fs.renameSync(tmp, CONFIG_FILE);
|
|
83
|
+
}
|
|
84
|
+
catch { /* non-fatal — will re-migrate next load */ }
|
|
85
|
+
return migrated;
|
|
86
|
+
}
|
|
87
|
+
const VALID_LLM_PROTOCOLS = new Set(["openai", "anthropic"]);
|
|
88
|
+
const VALID_EMBED_PROTOCOLS = new Set(["openai", "anthropic", "openrouter", "gemini", "ollama"]);
|
|
89
|
+
function sanitizeProviderEnv(raw) {
|
|
90
|
+
if (!raw)
|
|
91
|
+
return undefined;
|
|
92
|
+
if (VALID_LLM_PROTOCOLS.has(raw))
|
|
93
|
+
return raw;
|
|
94
|
+
// It's a junk/placeholder value — skip it so the default stays in place.
|
|
95
|
+
return undefined;
|
|
96
|
+
}
|
|
97
|
+
function sanitizeEmbedProviderEnv(raw) {
|
|
98
|
+
if (!raw)
|
|
99
|
+
return undefined;
|
|
100
|
+
if (VALID_EMBED_PROTOCOLS.has(raw))
|
|
101
|
+
return raw;
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|
|
104
|
+
function sanitizeApiKeyEnv(raw) {
|
|
105
|
+
if (!raw)
|
|
106
|
+
return undefined;
|
|
107
|
+
// Reject dot-notation field references: "embedding.apiKey", "summarization.key", etc.
|
|
108
|
+
if (/^[a-z]+\.[a-zA-Z]/.test(raw))
|
|
109
|
+
return undefined;
|
|
110
|
+
return raw;
|
|
111
|
+
}
|
|
112
|
+
function sanitizeBaseUrlEnv(raw) {
|
|
113
|
+
if (!raw)
|
|
114
|
+
return undefined;
|
|
115
|
+
if (raw.startsWith("http://") || raw.startsWith("https://"))
|
|
116
|
+
return raw;
|
|
117
|
+
// Not a URL — likely a placeholder like "embedding.baseUrl"
|
|
118
|
+
return undefined;
|
|
119
|
+
}
|
|
120
|
+
function sanitizeModelEnv(raw) {
|
|
121
|
+
if (!raw)
|
|
122
|
+
return undefined;
|
|
123
|
+
// Reject dot-notation references like "summarization.model"
|
|
124
|
+
if (/^[a-z]+\.[a-zA-Z]/.test(raw))
|
|
125
|
+
return undefined;
|
|
126
|
+
return raw;
|
|
127
|
+
}
|
|
128
|
+
// Env Var Application
|
|
64
129
|
//
|
|
65
130
|
// Applies environment variables onto an already-merged config.
|
|
66
131
|
// Only fills fields that are still empty after the file merge.
|
|
@@ -78,20 +143,21 @@ function applyEnvVars(config) {
|
|
|
78
143
|
// Only inject env var if config file didn't already set this field
|
|
79
144
|
provider: s.provider !== ANTHROPIC_DEFAULTS.summarization.provider
|
|
80
145
|
? s.provider
|
|
81
|
-
: process.env[ENV.LLM_PROVIDER] ?? s.provider,
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
146
|
+
: sanitizeProviderEnv(process.env[ENV.LLM_PROVIDER]) ?? s.provider,
|
|
147
|
+
providerName: s.providerName ?? sanitizeModelEnv(process.env[ENV.LLM_PROVIDER_NAME]),
|
|
148
|
+
model: s.model ?? sanitizeModelEnv(process.env[ENV.LLM_MODEL]),
|
|
149
|
+
apiKey: s.apiKey ?? sanitizeApiKeyEnv(process.env[ENV.LLM_KEY]),
|
|
150
|
+
baseUrl: s.baseUrl ?? sanitizeBaseUrlEnv(process.env[ENV.LLM_BASE_URL]),
|
|
85
151
|
batchSize: s.batchSize ?? (parseInt(process.env[ENV.BATCH_SIZE] ?? "", 10) ?? s.batchSize),
|
|
86
152
|
},
|
|
87
153
|
embedding: {
|
|
88
154
|
...e,
|
|
89
155
|
provider: e.provider !== ANTHROPIC_DEFAULTS.embedding.provider
|
|
90
156
|
? e.provider
|
|
91
|
-
: process.env[ENV.EMBED_PROVIDER] ?? e.provider,
|
|
92
|
-
model: e.model ?? process.env[ENV.EMBED_MODEL],
|
|
93
|
-
apiKey: e.apiKey ?? process.env[ENV.EMBED_KEY],
|
|
94
|
-
baseUrl: e.baseUrl ?? process.env[ENV.EMBED_BASE_URL],
|
|
157
|
+
: sanitizeEmbedProviderEnv(process.env[ENV.EMBED_PROVIDER]) ?? e.provider,
|
|
158
|
+
model: e.model ?? sanitizeModelEnv(process.env[ENV.EMBED_MODEL]),
|
|
159
|
+
apiKey: e.apiKey ?? sanitizeApiKeyEnv(process.env[ENV.EMBED_KEY]),
|
|
160
|
+
baseUrl: e.baseUrl ?? sanitizeBaseUrlEnv(process.env[ENV.EMBED_BASE_URL]),
|
|
95
161
|
},
|
|
96
162
|
// Neo4j: only build from env vars if config file didn't set it
|
|
97
163
|
// AND all three required env vars are present
|
|
@@ -110,15 +176,14 @@ function buildNeo4jFromEnv() {
|
|
|
110
176
|
return undefined;
|
|
111
177
|
return { url, username, password, storeRawCode };
|
|
112
178
|
}
|
|
113
|
-
//
|
|
179
|
+
// Validation
|
|
114
180
|
//
|
|
115
181
|
// Only validates what cannot have a sensible default.
|
|
116
|
-
//
|
|
117
|
-
//
|
|
118
|
-
// managed never needs an apiKey — platform provides it via request headers.
|
|
182
|
+
// Key requirement is resolved from the catalog's `requiresKey` per provider.
|
|
183
|
+
// If a providerName isn't in the catalog (custom), we default to requiring a key.
|
|
119
184
|
//
|
|
120
185
|
// Error messages are actionable — they tell the user exactly how to fix the problem.
|
|
121
|
-
const
|
|
186
|
+
const EMBEDDING_PROVIDERS_NEEDING_KEY = new Set([
|
|
122
187
|
"anthropic",
|
|
123
188
|
"openai",
|
|
124
189
|
"openrouter",
|
|
@@ -126,10 +191,11 @@ const PROVIDERS_NEEDING_KEY = new Set([
|
|
|
126
191
|
]);
|
|
127
192
|
function validate(config) {
|
|
128
193
|
const { summarization, embedding } = config;
|
|
129
|
-
// Summarization apiKey
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
194
|
+
// Summarization apiKey — resolved from catalog
|
|
195
|
+
const entry = findProvider(summarization.providerName ?? "");
|
|
196
|
+
const needsKey = entry?.requiresKey ?? true; // unknown/custom → require key
|
|
197
|
+
if (needsKey && !summarization.apiKey) {
|
|
198
|
+
throw new Error(`DevLens config error: summarization.apiKey is required for "${summarization.providerName ?? summarization.provider}".\n` +
|
|
133
199
|
` Fix option 1 — add to ${CONFIG_FILE}:\n` +
|
|
134
200
|
` { "summarization": { "apiKey": "your-key-here" } }\n` +
|
|
135
201
|
` Fix option 2 — set environment variable:\n` +
|
|
@@ -142,7 +208,7 @@ function validate(config) {
|
|
|
142
208
|
const rawFile = readFileConfig();
|
|
143
209
|
const userSetEmbedding = !!rawFile.embedding?.provider;
|
|
144
210
|
if (userSetEmbedding &&
|
|
145
|
-
|
|
211
|
+
EMBEDDING_PROVIDERS_NEEDING_KEY.has(embedding.provider) &&
|
|
146
212
|
!embedding.apiKey) {
|
|
147
213
|
throw new Error(`DevLens config error: embedding.apiKey is required when provider is "${embedding.provider}".\n` +
|
|
148
214
|
` Fix option 1 — add to ${CONFIG_FILE}:\n` +
|
|
@@ -165,15 +231,15 @@ function validate(config) {
|
|
|
165
231
|
}
|
|
166
232
|
}
|
|
167
233
|
// Ollama baseUrl format
|
|
168
|
-
if (summarization.
|
|
169
|
-
const base = summarization.baseUrl ?? "http://localhost:11434";
|
|
234
|
+
if (summarization.providerName === "ollama") {
|
|
235
|
+
const base = summarization.baseUrl ?? "http://localhost:11434/v1";
|
|
170
236
|
if (!base.startsWith("http://") && !base.startsWith("https://")) {
|
|
171
237
|
throw new Error(`DevLens config error: summarization.baseUrl must start with http:// or https://.\n` +
|
|
172
238
|
` Got: "${base}"`);
|
|
173
239
|
}
|
|
174
240
|
}
|
|
175
241
|
}
|
|
176
|
-
//
|
|
242
|
+
// readFileConfig
|
|
177
243
|
//
|
|
178
244
|
// Reads ~/.devlens/config.json and returns a PartialConfig.
|
|
179
245
|
// Returns empty object if file doesn't exist — first run, caller uses defaults.
|
|
@@ -192,7 +258,57 @@ function readFileConfig() {
|
|
|
192
258
|
` Tip: use a JSON validator at https://jsonlint.com`);
|
|
193
259
|
}
|
|
194
260
|
}
|
|
195
|
-
|
|
261
|
+
/** Public — reads the raw config.json as a plain object. Used by multi-provider helpers. */
|
|
262
|
+
export function readRawConfigFile() {
|
|
263
|
+
if (!fs.existsSync(CONFIG_FILE))
|
|
264
|
+
return {};
|
|
265
|
+
try {
|
|
266
|
+
return JSON.parse(fs.readFileSync(CONFIG_FILE, "utf-8"));
|
|
267
|
+
}
|
|
268
|
+
catch {
|
|
269
|
+
return {};
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
// ── Multi-provider detection ──────────────────────────────────────────────
|
|
273
|
+
/** Returns true if the raw summarization block is in multi-provider format. */
|
|
274
|
+
function isMultiProviderFormat(s) {
|
|
275
|
+
if (!s || typeof s !== "object")
|
|
276
|
+
return false;
|
|
277
|
+
const obj = s;
|
|
278
|
+
return (typeof obj.active === "string" &&
|
|
279
|
+
obj.providers !== undefined &&
|
|
280
|
+
typeof obj.providers === "object" &&
|
|
281
|
+
!Array.isArray(obj.providers));
|
|
282
|
+
}
|
|
283
|
+
/** Extract the active provider from multi-provider storage, returning a flat summarization partial. */
|
|
284
|
+
function extractActiveProvider(storage) {
|
|
285
|
+
const entry = storage.providers[storage.active];
|
|
286
|
+
if (!entry) {
|
|
287
|
+
// Fallback: pick the first available provider
|
|
288
|
+
const first = Object.values(storage.providers)[0];
|
|
289
|
+
if (!first)
|
|
290
|
+
throw new Error("Multi-provider config has no provider entries.");
|
|
291
|
+
// Fix the active key to match what exists
|
|
292
|
+
storage.active = Object.keys(storage.providers)[0];
|
|
293
|
+
return {
|
|
294
|
+
provider: first.provider,
|
|
295
|
+
providerName: first.providerName,
|
|
296
|
+
model: first.model,
|
|
297
|
+
apiKey: first.apiKey,
|
|
298
|
+
baseUrl: first.baseUrl,
|
|
299
|
+
batchSize: first.batchSize,
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
return {
|
|
303
|
+
provider: entry.provider,
|
|
304
|
+
providerName: entry.providerName,
|
|
305
|
+
model: entry.model,
|
|
306
|
+
apiKey: entry.apiKey,
|
|
307
|
+
baseUrl: entry.baseUrl,
|
|
308
|
+
batchSize: entry.batchSize,
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
// loadFileConfig
|
|
196
312
|
//
|
|
197
313
|
// Public entry point — called by resolveConfig() in config/index.ts.
|
|
198
314
|
//
|
|
@@ -207,9 +323,59 @@ function readFileConfig() {
|
|
|
207
323
|
// 4. Validate — throw clear errors for anything missing or invalid
|
|
208
324
|
// 5. Return fully resolved DevLensConfig — never partial, never undefined fields
|
|
209
325
|
export function loadFileConfig(defaults = ANTHROPIC_DEFAULTS) {
|
|
210
|
-
|
|
326
|
+
let partial = readFileConfig();
|
|
327
|
+
// ── Multi-provider detection & migration ─────────────────────────────────
|
|
328
|
+
if (partial.summarization) {
|
|
329
|
+
const sum = partial.summarization;
|
|
330
|
+
if (isMultiProviderFormat(sum)) {
|
|
331
|
+
// New multi-provider format — extract the active entry
|
|
332
|
+
const activeFlat = extractActiveProvider(sum);
|
|
333
|
+
partial = {
|
|
334
|
+
...partial,
|
|
335
|
+
summarization: activeFlat,
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
else if (typeof sum.provider === "string" && !sum.providers) {
|
|
339
|
+
// Old flat format — migrate to multi-provider automatically
|
|
340
|
+
const protocol = sum.provider;
|
|
341
|
+
const providerName = sum.providerName ?? protocol;
|
|
342
|
+
const key = makeProviderKey(protocol, providerName);
|
|
343
|
+
const newStorage = {
|
|
344
|
+
active: key,
|
|
345
|
+
providers: {
|
|
346
|
+
[key]: {
|
|
347
|
+
provider: protocol,
|
|
348
|
+
providerName: providerName,
|
|
349
|
+
model: sum.model ?? "",
|
|
350
|
+
apiKey: sum.apiKey,
|
|
351
|
+
baseUrl: sum.baseUrl,
|
|
352
|
+
batchSize: sum.batchSize ?? 50,
|
|
353
|
+
},
|
|
354
|
+
},
|
|
355
|
+
};
|
|
356
|
+
// Write the migrated format back atomically
|
|
357
|
+
const raw = JSON.parse(fs.readFileSync(CONFIG_FILE, "utf-8"));
|
|
358
|
+
raw.summarization = newStorage;
|
|
359
|
+
const tmp = CONFIG_FILE + ".tmp";
|
|
360
|
+
fs.writeFileSync(tmp, JSON.stringify(raw, null, 2));
|
|
361
|
+
fs.renameSync(tmp, CONFIG_FILE);
|
|
362
|
+
// Proceed with the active entry as the flat summarization
|
|
363
|
+
partial = {
|
|
364
|
+
...partial,
|
|
365
|
+
summarization: {
|
|
366
|
+
provider: protocol,
|
|
367
|
+
providerName: providerName,
|
|
368
|
+
model: sum.model ?? "",
|
|
369
|
+
apiKey: sum.apiKey,
|
|
370
|
+
baseUrl: sum.baseUrl,
|
|
371
|
+
batchSize: sum.batchSize ?? 50,
|
|
372
|
+
},
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
}
|
|
211
376
|
const merged = deepMerge(defaults, partial);
|
|
212
|
-
const
|
|
377
|
+
const migrated = migrateProviderConfig(merged);
|
|
378
|
+
const withEnv = applyEnvVars(migrated);
|
|
213
379
|
validate(withEnv);
|
|
214
380
|
return withEnv;
|
|
215
381
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export const DEFAULT_PROVIDERS = [
|
|
2
|
+
{ name: "deepseek", label: "DeepSeek", protocol: "openai", baseUrl: "https://api.deepseek.com", requiresKey: true },
|
|
3
|
+
{ name: "openai", label: "OpenAI", protocol: "openai", baseUrl: "https://api.openai.com/v1", requiresKey: true },
|
|
4
|
+
{ name: "anthropic", label: "Anthropic", protocol: "anthropic", baseUrl: "https://api.anthropic.com", requiresKey: true },
|
|
5
|
+
{ name: "gemini", label: "Google Gemini", protocol: "openai", baseUrl: "https://generativelanguage.googleapis.com/v1beta/openai", requiresKey: true },
|
|
6
|
+
{ name: "groq", label: "Groq", protocol: "openai", baseUrl: "https://api.groq.com/openai/v1", requiresKey: true },
|
|
7
|
+
{ name: "mistral", label: "Mistral", protocol: "openai", baseUrl: "https://api.mistral.ai/v1", requiresKey: true },
|
|
8
|
+
{ name: "xai", label: "xAI Grok", protocol: "openai", baseUrl: "https://api.x.ai/v1", requiresKey: true },
|
|
9
|
+
{ name: "openrouter", label: "OpenRouter", protocol: "openai", baseUrl: "https://openrouter.ai/api/v1", requiresKey: true },
|
|
10
|
+
{ name: "ollama", label: "Ollama (local)", protocol: "openai", baseUrl: "http://localhost:11434/v1", requiresKey: false },
|
|
11
|
+
];
|
|
12
|
+
export const CATALOG_VERSION = 2;
|
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
import { CONFIG_HEADERS } from "../types.js";
|
|
2
|
-
|
|
2
|
+
const VALID_LLM_PROTOCOLS = new Set(["openai", "anthropic"]);
|
|
3
|
+
const VALID_EMBED_PROTOCOLS = new Set(["openai", "anthropic", "openrouter", "gemini", "ollama"]);
|
|
4
|
+
function rejectPlaceholder(v) {
|
|
5
|
+
if (!v)
|
|
6
|
+
return undefined;
|
|
7
|
+
// Dot-notation field references like "embedding.provider", "summarization.key"
|
|
8
|
+
if (/^[a-z]+\.[a-zA-Z]/.test(v))
|
|
9
|
+
return undefined;
|
|
10
|
+
return v;
|
|
11
|
+
}
|
|
12
|
+
// ─ applyRequestHeaders
|
|
3
13
|
//
|
|
4
14
|
// Public — called by resolveConfig() in config/index.ts AFTER loadFileConfig().
|
|
5
15
|
//
|
|
@@ -27,15 +37,16 @@ export function applyRequestHeaders(base, req) {
|
|
|
27
37
|
}
|
|
28
38
|
// Summarization overrides ─
|
|
29
39
|
const provider = get(CONFIG_HEADERS.PROVIDER);
|
|
30
|
-
const
|
|
31
|
-
const
|
|
32
|
-
const
|
|
40
|
+
const providerName = rejectPlaceholder(get(CONFIG_HEADERS.PROVIDER_NAME));
|
|
41
|
+
const model = rejectPlaceholder(get(CONFIG_HEADERS.MODEL));
|
|
42
|
+
const apiKey = rejectPlaceholder(get(CONFIG_HEADERS.API_KEY));
|
|
43
|
+
const baseUrl = (() => { const v = get(CONFIG_HEADERS.BASE_URL); return v && (v.startsWith("http://") || v.startsWith("https://")) ? v : undefined; })();
|
|
33
44
|
const batchSize = get(CONFIG_HEADERS.BATCH_SIZE);
|
|
34
45
|
// Embedding overrides ─
|
|
35
46
|
const embedProvider = get(CONFIG_HEADERS.EMBED_PROVIDER);
|
|
36
|
-
const embedModel = get(CONFIG_HEADERS.EMBED_MODEL);
|
|
37
|
-
const embedKey = get(CONFIG_HEADERS.EMBED_KEY);
|
|
38
|
-
const embedBaseUrl = get(CONFIG_HEADERS.EMBED_BASE_URL);
|
|
47
|
+
const embedModel = rejectPlaceholder(get(CONFIG_HEADERS.EMBED_MODEL));
|
|
48
|
+
const embedKey = rejectPlaceholder(get(CONFIG_HEADERS.EMBED_KEY));
|
|
49
|
+
const embedBaseUrl = (() => { const v = get(CONFIG_HEADERS.EMBED_BASE_URL); return v && (v.startsWith("http://") || v.startsWith("https://")) ? v : undefined; })();
|
|
39
50
|
// Neo4j overrides ─
|
|
40
51
|
const neo4jUrl = get(CONFIG_HEADERS.NEO4J_URL);
|
|
41
52
|
const neo4jUser = get(CONFIG_HEADERS.NEO4J_USER);
|
|
@@ -48,7 +59,8 @@ export function applyRequestHeaders(base, req) {
|
|
|
48
59
|
deploymentMode: base.deploymentMode,
|
|
49
60
|
summarization: {
|
|
50
61
|
...base.summarization,
|
|
51
|
-
...(provider && { provider: provider }),
|
|
62
|
+
...(provider && VALID_LLM_PROTOCOLS.has(provider) && { provider: provider }),
|
|
63
|
+
...(providerName && { providerName }),
|
|
52
64
|
...(model && { model }),
|
|
53
65
|
...(apiKey && { apiKey }),
|
|
54
66
|
...(baseUrl && { baseUrl }),
|
|
@@ -56,7 +68,7 @@ export function applyRequestHeaders(base, req) {
|
|
|
56
68
|
},
|
|
57
69
|
embedding: {
|
|
58
70
|
...base.embedding,
|
|
59
|
-
...(embedProvider && { provider: embedProvider }),
|
|
71
|
+
...(embedProvider && VALID_EMBED_PROTOCOLS.has(embedProvider) && { provider: embedProvider }),
|
|
60
72
|
...(embedModel && { model: embedModel }),
|
|
61
73
|
...(embedKey && { apiKey: embedKey }),
|
|
62
74
|
...(embedBaseUrl && { baseUrl: embedBaseUrl }),
|
package/dist/config/types.d.ts
CHANGED
|
@@ -1,13 +1,35 @@
|
|
|
1
|
-
export type LLMProvider = "
|
|
2
|
-
export type EmbeddingProvider = "openai" | "anthropic" | "openrouter" | "gemini" | "ollama"
|
|
1
|
+
export type LLMProvider = "openai" | "anthropic";
|
|
2
|
+
export type EmbeddingProvider = "openai" | "anthropic" | "openrouter" | "gemini" | "ollama";
|
|
3
3
|
export type DeploymentMode = "local" | "cloud";
|
|
4
4
|
export interface SummarizationConfig {
|
|
5
5
|
provider: LLMProvider;
|
|
6
|
+
providerName?: string;
|
|
6
7
|
model: string;
|
|
7
8
|
apiKey?: string;
|
|
8
9
|
baseUrl?: string;
|
|
9
10
|
batchSize: number;
|
|
10
11
|
}
|
|
12
|
+
/** A single provider entry stored in the multi-provider map on disk. */
|
|
13
|
+
export interface ProviderConfigEntry {
|
|
14
|
+
provider: LLMProvider;
|
|
15
|
+
providerName: string;
|
|
16
|
+
model: string;
|
|
17
|
+
apiKey?: string;
|
|
18
|
+
baseUrl?: string;
|
|
19
|
+
batchSize: number;
|
|
20
|
+
}
|
|
21
|
+
/** Storage shape for `summarization` inside config.json (v2 multi-provider). */
|
|
22
|
+
export interface MultiProviderStorage {
|
|
23
|
+
active: string;
|
|
24
|
+
providers: Record<string, ProviderConfigEntry>;
|
|
25
|
+
}
|
|
26
|
+
/** Derive the composite key from a protocol + providerName pair. */
|
|
27
|
+
export declare function makeProviderKey(protocol: string, providerName: string): string;
|
|
28
|
+
/** Parse a composite key back into its parts. */
|
|
29
|
+
export declare function parseProviderKey(key: string): {
|
|
30
|
+
protocol: string;
|
|
31
|
+
providerName: string;
|
|
32
|
+
};
|
|
11
33
|
export interface EmbeddingConfig {
|
|
12
34
|
provider: EmbeddingProvider;
|
|
13
35
|
model: string;
|
|
@@ -30,6 +52,7 @@ export declare const OLLAMA_DEFAULTS: DevLensConfig;
|
|
|
30
52
|
export declare const ANTHROPIC_DEFAULTS: DevLensConfig;
|
|
31
53
|
export declare const CONFIG_HEADERS: {
|
|
32
54
|
readonly PROVIDER: "x-llm-provider";
|
|
55
|
+
readonly PROVIDER_NAME: "x-llm-provider-name";
|
|
33
56
|
readonly MODEL: "x-llm-model";
|
|
34
57
|
readonly API_KEY: "x-llm-key";
|
|
35
58
|
readonly BASE_URL: "x-llm-base-url";
|
package/dist/config/types.js
CHANGED
|
@@ -1,18 +1,30 @@
|
|
|
1
|
+
/** Derive the composite key from a protocol + providerName pair. */
|
|
2
|
+
export function makeProviderKey(protocol, providerName) {
|
|
3
|
+
return `${protocol}:${providerName}`;
|
|
4
|
+
}
|
|
5
|
+
/** Parse a composite key back into its parts. */
|
|
6
|
+
export function parseProviderKey(key) {
|
|
7
|
+
const idx = key.indexOf(":");
|
|
8
|
+
if (idx === -1)
|
|
9
|
+
throw new Error(`Invalid provider key: "${key}"`);
|
|
10
|
+
return { protocol: key.slice(0, idx), providerName: key.slice(idx + 1) };
|
|
11
|
+
}
|
|
1
12
|
// Note: apiKey is intentionally absent from all defaults.
|
|
2
13
|
// If a user reaches the defaults with no key configured anywhere,
|
|
3
14
|
// the system fails clearly at the LLM call — never silently sends an empty key.
|
|
4
15
|
export const OLLAMA_DEFAULTS = {
|
|
5
16
|
deploymentMode: "local",
|
|
6
17
|
summarization: {
|
|
7
|
-
provider: "
|
|
18
|
+
provider: "openai",
|
|
19
|
+
providerName: "ollama",
|
|
8
20
|
model: "qwen2.5-coder:3b", // code-aware, 3B params, runs on ~2GB RAM
|
|
9
|
-
baseUrl: "http://localhost:11434",
|
|
21
|
+
baseUrl: "http://localhost:11434/v1",
|
|
10
22
|
batchSize: 50,
|
|
11
23
|
},
|
|
12
24
|
embedding: {
|
|
13
25
|
provider: "ollama",
|
|
14
26
|
model: "nomic-embed-text", // best local embedding model, 768 dims
|
|
15
|
-
baseUrl: "http://localhost:11434",
|
|
27
|
+
baseUrl: "http://localhost:11434/v1",
|
|
16
28
|
},
|
|
17
29
|
// neo4j absent — file-only mode is the safe default
|
|
18
30
|
};
|
|
@@ -20,7 +32,9 @@ export const ANTHROPIC_DEFAULTS = {
|
|
|
20
32
|
deploymentMode: "local",
|
|
21
33
|
summarization: {
|
|
22
34
|
provider: "anthropic",
|
|
35
|
+
providerName: "anthropic",
|
|
23
36
|
model: "claude-haiku-4-5", // fastest Claude, cheapest, good code understanding
|
|
37
|
+
baseUrl: "https://api.anthropic.com",
|
|
24
38
|
batchSize: 50,
|
|
25
39
|
// apiKey intentionally absent — user must set in config.json
|
|
26
40
|
},
|
|
@@ -31,7 +45,7 @@ export const ANTHROPIC_DEFAULTS = {
|
|
|
31
45
|
},
|
|
32
46
|
// neo4j absent
|
|
33
47
|
};
|
|
34
|
-
//
|
|
48
|
+
// Request Header Names
|
|
35
49
|
//
|
|
36
50
|
// Exact header names the cloud backend sends to this Bun backend.
|
|
37
51
|
// Defined as constants so they are never mistyped across files.
|
|
@@ -40,7 +54,8 @@ export const ANTHROPIC_DEFAULTS = {
|
|
|
40
54
|
// Headers marked "NEVER LOG" must never appear in any log output.
|
|
41
55
|
export const CONFIG_HEADERS = {
|
|
42
56
|
// LLM provider for summarization
|
|
43
|
-
PROVIDER: "x-llm-provider", // e.g. "anthropic"
|
|
57
|
+
PROVIDER: "x-llm-provider", // e.g. "anthropic" — wire protocol
|
|
58
|
+
PROVIDER_NAME: "x-llm-provider-name", // e.g. "deepseek" — brand identity
|
|
44
59
|
MODEL: "x-llm-model", // e.g. "claude-haiku-4-5"
|
|
45
60
|
API_KEY: "x-llm-key",
|
|
46
61
|
BASE_URL: "x-llm-base-url", // for Ollama: "http://localhost:11434"
|
|
@@ -54,7 +69,7 @@ export const CONFIG_HEADERS = {
|
|
|
54
69
|
NEO4J_PASSWORD: "x-neo4j-password",
|
|
55
70
|
NEO4J_STORECODE: "false",
|
|
56
71
|
};
|
|
57
|
-
//
|
|
72
|
+
// Sensitive Headers Set
|
|
58
73
|
//
|
|
59
74
|
// Used by sanitizeHeaders() below.
|
|
60
75
|
// Add any new secret header here the moment it is added to CONFIG_HEADERS.
|
|
@@ -63,7 +78,7 @@ const SENSITIVE_HEADERS = new Set([
|
|
|
63
78
|
CONFIG_HEADERS.EMBED_KEY,
|
|
64
79
|
CONFIG_HEADERS.NEO4J_PASSWORD,
|
|
65
80
|
]);
|
|
66
|
-
//
|
|
81
|
+
// sanitizeHeaders
|
|
67
82
|
//
|
|
68
83
|
// Call this before logging ANY request headers anywhere in the codebase.
|
|
69
84
|
// Replaces secret values with "[REDACTED]" so API keys never appear in logs.
|
package/dist/config/writer.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { DevLensConfig } from "./types.js";
|
|
1
|
+
import type { DevLensConfig, ProviderConfigEntry } from "./types.js";
|
|
2
2
|
type DeepPartial<T> = {
|
|
3
3
|
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
|
|
4
4
|
};
|
|
@@ -7,10 +7,15 @@ export interface SafeConfig {
|
|
|
7
7
|
deploymentMode: DevLensConfig["deploymentMode"];
|
|
8
8
|
summarization: {
|
|
9
9
|
provider: string;
|
|
10
|
+
providerName?: string;
|
|
10
11
|
model: string;
|
|
11
12
|
baseUrl?: string;
|
|
12
13
|
batchSize: number;
|
|
13
|
-
|
|
14
|
+
apiKey?: string;
|
|
15
|
+
};
|
|
16
|
+
allProviders?: {
|
|
17
|
+
active: string;
|
|
18
|
+
providers: ProviderConfigEntry[];
|
|
14
19
|
};
|
|
15
20
|
embedding: {
|
|
16
21
|
provider: string;
|
|
@@ -24,6 +29,7 @@ export interface SafeConfig {
|
|
|
24
29
|
storeRawCode: boolean;
|
|
25
30
|
};
|
|
26
31
|
}
|
|
32
|
+
export declare function atomicWrite(filePath: string, content: string): void;
|
|
27
33
|
export declare function writeConfig(partial: PartialConfig): void;
|
|
28
34
|
export declare function maskConfig(config: DevLensConfig): SafeConfig;
|
|
29
35
|
export {};
|