devlensio 0.4.3 → 0.6.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/LICENSE +0 -0
- package/README.md +212 -72
- 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/index.js +6 -0
- package/dist/filesystem/index.test.js +170 -0
- package/dist/filesystem/reactRouterRoutes.d.ts +2 -0
- package/dist/filesystem/reactRouterRoutes.js +243 -0
- package/dist/graph/edges/apiFetchEdges.js +1 -137
- package/dist/graph/edges/helpers/routeMatching.d.ts +8 -0
- package/dist/graph/edges/helpers/routeMatching.js +122 -0
- package/dist/graph/edges/navigationEdges.d.ts +5 -0
- package/dist/graph/edges/navigationEdges.js +339 -0
- package/dist/graph/edges/routeEdge.js +25 -0
- package/dist/graph/index.js +12 -1
- package/dist/graph/index.test.js +263 -8
- package/dist/index.d.ts +6 -1
- package/dist/index.js +5 -1
- package/dist/parser/extractors/objectMethods.js +2 -2
- package/dist/parser/index.test.js +0 -10
- package/dist/pipeline/index.js +26 -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 +3 -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
package/LICENSE
CHANGED
|
File without changes
|
package/README.md
CHANGED
|
@@ -1,134 +1,274 @@
|
|
|
1
|
-
# devlensio —
|
|
1
|
+
# `devlensio` — The DevLens Analysis Engine
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/devlensio)
|
|
4
4
|
[](https://www.gnu.org/licenses/agpl-3.0)
|
|
5
5
|
|
|
6
|
-
The core engine behind [DevLens](https://github.com/devlensio/devlensOSS). It turns a TypeScript / JavaScript / React / Next.js / Node.js repository into a **typed code graph**
|
|
6
|
+
The core engine behind [DevLens](https://github.com/devlensio/devlensOSS) — the codebase visualizer. It turns a TypeScript / JavaScript / React / Next.js / Node.js repository into a **typed code graph** with functional summaries, technical summaries, and security analysis on every node.
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
The user-facing tools — CLI, MCP server, Agent Skill, and Web UI — all live in [DevLens OSS](https://github.com/devlensio/devlensOSS) and consume this package.
|
|
9
9
|
|
|
10
10
|
---
|
|
11
11
|
|
|
12
|
-
##
|
|
12
|
+
## What it does
|
|
13
13
|
|
|
14
|
-
```
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
```
|
|
15
|
+
Repo path
|
|
16
|
+
│
|
|
17
|
+
[1] Fingerprint → detect language, framework, router, state manager, data layer
|
|
18
|
+
[2] Route detection → extract routes (Next.js, React Router, Express, Fastify, Koa)
|
|
19
|
+
[3] AST parsing → walk every .ts/.tsx/.js/.jsx → extract nodes with types
|
|
20
|
+
[4] Edge detection → map CALLS, IMPORTS, PROP_PASS, WRITES_TO, and 12 more edge types
|
|
21
|
+
[5] Scoring → multi-pass importance scoring (no AI, deterministic)
|
|
22
|
+
[6] Clustering → assign cohesive clusters
|
|
23
|
+
[7] Summarize (opt) → topological LLM summaries — functional + technical + security
|
|
24
|
+
│
|
|
25
|
+
▼
|
|
26
|
+
Graph saved to ~/.devlens → queried via the traversal API / CLI / MCP / UI
|
|
17
27
|
```
|
|
18
28
|
|
|
19
|
-
|
|
29
|
+
Structural analysis is fast and deterministic. Summarization is the only step that calls an LLM — and unchanged nodes are reused across commits (90%+ free on re-runs).
|
|
20
30
|
|
|
21
31
|
---
|
|
22
32
|
|
|
23
|
-
##
|
|
33
|
+
## Install
|
|
24
34
|
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
[2] Filesystem scan extract routes (Next.js app/pages, Express, Fastify, Koa)
|
|
30
|
-
[3] Parse (ts-morph) walk every .ts/.tsx/.js/.jsx → nodes (typed params, return types, prop types)
|
|
31
|
-
[4] Edge detection many detectors → CALLS, IMPORTS, READS_FROM, WRITES_TO, PROP_PASS, EMITS,
|
|
32
|
-
LISTENS, WRAPPED_BY, GUARDS, HANDLES, TESTS, USES, NEXTJS_API_CALL
|
|
33
|
-
[5] Scoring multi-pass importance scoring + noise filtering (no AI)
|
|
34
|
-
[6] Clustering cohesive cluster assignment
|
|
35
|
-
[7] Summarize (optional) topologically-ordered LLM summaries, checkpoint/resume, MapReduce
|
|
36
|
-
│
|
|
37
|
-
▼
|
|
38
|
-
Graph persisted to ~/.devlens → queried via the traversal API / CLI / MCP / UI
|
|
35
|
+
```bash
|
|
36
|
+
npm install devlensio
|
|
37
|
+
# or
|
|
38
|
+
bun add devlensio
|
|
39
39
|
```
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
Requires Node 18+ (or Bun). An LLM provider key is only needed for AI summarization — structural analysis works offline.
|
|
42
42
|
|
|
43
43
|
---
|
|
44
44
|
|
|
45
45
|
## Public API
|
|
46
46
|
|
|
47
|
-
```
|
|
47
|
+
```typescript
|
|
48
48
|
import {
|
|
49
|
-
analyzePipeline, //
|
|
50
|
-
runSummarization, //
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
49
|
+
analyzePipeline, // Build a graph from a repo
|
|
50
|
+
runSummarization, // Generate AI summaries (functional, technical, security)
|
|
51
|
+
buildGraphIndex, // Index nodes + edges for traversal
|
|
52
|
+
getBlastRadius, // Upstream dependents — "what breaks if I change this?"
|
|
53
|
+
getKHop, // Downstream dependencies — "what does this depend on?"
|
|
54
|
+
getSubgraph, // Cohesive cluster around a seed node
|
|
55
|
+
findCycles, // Circular dependency groups
|
|
56
|
+
resolveConfig, // Resolve active provider config (flat)
|
|
57
|
+
initConfig, // LLM provider config
|
|
58
|
+
loadCatalog, // Provider catalog (name, label, protocol, baseUrl, requiresKey)
|
|
59
|
+
findProvider, // Look up a provider by name in the catalog
|
|
60
|
+
listModels, // Fetch live model list from a provider's models endpoint
|
|
61
|
+
resolveAllProviders, // Get all configured providers + active pointer
|
|
62
|
+
setActiveProvider, // Switch the active provider by composite key
|
|
63
|
+
removeProviderConfig, // Remove a provider entry (refuses active entry)
|
|
64
|
+
writeConfig, // Upsert a provider entry and mark it active
|
|
59
65
|
} from "devlensio";
|
|
60
66
|
|
|
61
|
-
// Analyze a repo
|
|
62
|
-
const result = await analyzePipeline("/path/to/repo"
|
|
63
|
-
// result.allNodes, result.allEdges, result.nodeScores
|
|
67
|
+
// Analyze a repo
|
|
68
|
+
const result = await analyzePipeline("/path/to/repo");
|
|
64
69
|
|
|
65
|
-
//
|
|
66
|
-
const index
|
|
70
|
+
// Traverse the graph
|
|
71
|
+
const index = buildGraphIndex(result.allNodes, result.allEdges);
|
|
67
72
|
const impact = getBlastRadius(index, "src/auth/login.ts::login", { radius: 2 });
|
|
68
73
|
const cycles = findCycles(result.allNodes, result.allEdges);
|
|
74
|
+
|
|
75
|
+
// Discover providers and their models
|
|
76
|
+
const catalog = loadCatalog();
|
|
77
|
+
const deepseekModels = await listModels({
|
|
78
|
+
protocol: "openai",
|
|
79
|
+
baseUrl: "https://api.deepseek.com",
|
|
80
|
+
apiKey: "...",
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// Multi-provider management
|
|
84
|
+
const allProviders = resolveAllProviders();
|
|
85
|
+
// → { active: "openai:deepseek", providers: [{ provider:"openai", providerName:"deepseek", ... }] }
|
|
86
|
+
|
|
87
|
+
setActiveProvider("openai:deepseek"); // Switch active provider
|
|
88
|
+
removeProviderConfig("anthropic:anthropic"); // Remove entry (throws if active)
|
|
89
|
+
|
|
90
|
+
// Upsert a provider entry (marks it active)
|
|
91
|
+
writeConfig({
|
|
92
|
+
summarization: {
|
|
93
|
+
provider: "openai",
|
|
94
|
+
providerName: "deepseek",
|
|
95
|
+
model: "deepseek-chat",
|
|
96
|
+
baseUrl: "https://api.deepseek.com",
|
|
97
|
+
batchSize: 50,
|
|
98
|
+
},
|
|
99
|
+
});
|
|
69
100
|
```
|
|
70
101
|
|
|
71
|
-
Also exported: all core types (`CodeNode`, `CodeEdge`, `NodeType`, `EdgeType`,
|
|
102
|
+
Also exported: all core types (`CodeNode`, `CodeEdge`, `NodeType`, `EdgeType`, `CatalogProvider`, `ProviderConfigEntry`, `SummarizationConfig`, …) and config helpers. See `dist/index.d.ts` for the full surface.
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## Node & edge types
|
|
107
|
+
|
|
108
|
+
**Node types**
|
|
109
|
+
|
|
110
|
+
| Type | What it represents |
|
|
111
|
+
| :-- | :-- |
|
|
112
|
+
| `COMPONENT` | React / UI component |
|
|
113
|
+
| `HOOK` | React custom hook |
|
|
114
|
+
| `FUNCTION` | Plain function |
|
|
115
|
+
| `STATE_STORE` | State management (Zustand, Redux, etc.) |
|
|
116
|
+
| `UTILITY` | Utility / helper module |
|
|
117
|
+
| `FILE` | File-level node |
|
|
118
|
+
| `ROUTE` | Application route |
|
|
119
|
+
| `TEST` | Test file |
|
|
120
|
+
| `THIRD_PARTY` | External dependency |
|
|
121
|
+
|
|
122
|
+
**Edge types**
|
|
72
123
|
|
|
73
|
-
|
|
124
|
+
`CALLS`, `IMPORTS`, `READS_FROM`, `WRITES_TO`, `PROP_PASS`, `EMITS`, `LISTENS`, `WRAPPED_BY`, `GUARDS`, `HANDLES`, `TESTS`, `USES`, `NEXTJS_API_CALL`, `NAVIGATES_TO`
|
|
74
125
|
|
|
75
|
-
|
|
76
|
-
- **Edge types:** `CALLS`, `IMPORTS`, `READS_FROM`, `WRITES_TO`, `PROP_PASS`, `EMITS`, `LISTENS`, `WRAPPED_BY`, `GUARDS`, `HANDLES`, `TESTS`, `USES`, `NEXTJS_API_CALL` (frontend `fetch`/`axios`/`useSWR` call site → Next.js API route).
|
|
77
|
-
- Each node carries an importance score and (after summarization) a technical summary, a business summary, and a security assessment (`none|low|medium|high` + notes).
|
|
126
|
+
Each node carries: **importance score** + **functional summary** + **technical summary** + **security assessment** (severity + notes).
|
|
78
127
|
|
|
79
128
|
---
|
|
80
129
|
|
|
81
130
|
## Configuration
|
|
82
131
|
|
|
83
|
-
|
|
132
|
+
### Provider model: protocol vs brand
|
|
133
|
+
|
|
134
|
+
The engine splits provider identity into two orthogonal concerns:
|
|
135
|
+
|
|
136
|
+
| Field | Meaning | Values |
|
|
137
|
+
| :-- | :-- | :-- |
|
|
138
|
+
| `provider` | Wire protocol (`"openai"` or `"anthropic"`) — routes to the correct SDK | `"openai"` / `"anthropic"` |
|
|
139
|
+
| `providerName` | Brand identity — picks `baseUrl` + key rules from the catalog, or a custom name | Any string (e.g. `"deepseek"`, `"my-gateway"`) |
|
|
140
|
+
|
|
141
|
+
### Multi-provider storage
|
|
142
|
+
|
|
143
|
+
`~/.devlens/config.json` now holds a **registry** of all configured providers, keyed by composite key (`${protocol}:${providerName}`), with one marked `active`:
|
|
144
|
+
|
|
145
|
+
```json
|
|
146
|
+
{
|
|
147
|
+
"summarization": {
|
|
148
|
+
"active": "openai:deepseek",
|
|
149
|
+
"providers": {
|
|
150
|
+
"openai:deepseek": {
|
|
151
|
+
"provider": "openai",
|
|
152
|
+
"providerName": "deepseek",
|
|
153
|
+
"model": "deepseek-chat",
|
|
154
|
+
"apiKey": "sk-...",
|
|
155
|
+
"baseUrl": "https://api.deepseek.com",
|
|
156
|
+
"batchSize": 50
|
|
157
|
+
},
|
|
158
|
+
"anthropic:anthropic": {
|
|
159
|
+
"provider": "anthropic",
|
|
160
|
+
"providerName": "anthropic",
|
|
161
|
+
"model": "claude-haiku-4-5",
|
|
162
|
+
"apiKey": "sk-...",
|
|
163
|
+
"baseUrl": "https://api.anthropic.com",
|
|
164
|
+
"batchSize": 50
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Key behaviours:
|
|
172
|
+
- **Saving a provider upserts it into the registry and marks it active** — previous entries are preserved.
|
|
173
|
+
- **Switching active is a separate operation** (`setActiveProvider()`) that rewrites only the `active` pointer.
|
|
174
|
+
- **Removing a provider** (`removeProviderConfig()`) refuses to delete the active entry — switch first.
|
|
175
|
+
- **Legacy flat configs auto-migrate** to the multi-provider format on first load.
|
|
176
|
+
|
|
177
|
+
### Environment variables
|
|
84
178
|
|
|
85
179
|
```env
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
180
|
+
DEVLENS_LLM_PROVIDER=openai # Wire protocol — "openai" | "anthropic"
|
|
181
|
+
DEVLENS_LLM_PROVIDER_NAME=deepseek # Brand — picks baseUrl + key rules from catalog
|
|
182
|
+
DEVLENS_LLM_MODEL=deepseek-chat
|
|
183
|
+
DEVLENS_LLM_KEY=your_key
|
|
184
|
+
DEVLENS_LLM_BASE_URL= # Override base URL (optional)
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### Config resolution priority
|
|
188
|
+
|
|
189
|
+
```
|
|
190
|
+
request headers (cloud) > config.json > env vars > catalog defaults > code defaults
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
### Provider catalog
|
|
194
|
+
|
|
195
|
+
A built-in catalog ships with the engine (`providers.default.json`) — no config required for known providers. Models are **never hardcoded** in the catalog; they are discovered dynamically from each provider's `/models` endpoint at runtime.
|
|
196
|
+
|
|
197
|
+
**Built-in providers:** DeepSeek, OpenAI, Anthropic, Google Gemini, Groq, Mistral, xAI Grok, OpenRouter, Ollama (local).
|
|
198
|
+
|
|
199
|
+
Extend or override via `~/.devlens/providers.json` (deep-merged by `name`):
|
|
200
|
+
|
|
201
|
+
```json
|
|
202
|
+
{
|
|
203
|
+
"version": 2,
|
|
204
|
+
"providers": [
|
|
205
|
+
{
|
|
206
|
+
"name": "my-gateway",
|
|
207
|
+
"label": "My Gateway",
|
|
208
|
+
"protocol": "openai",
|
|
209
|
+
"baseUrl": "http://localhost:8080/v1",
|
|
210
|
+
"requiresKey": false
|
|
211
|
+
}
|
|
212
|
+
]
|
|
213
|
+
}
|
|
90
214
|
```
|
|
91
215
|
|
|
92
|
-
|
|
216
|
+
**Custom providers** can be added at any time — no engine update needed. Any provider not in the catalog is saved with its user-specified `baseUrl`, `protocol`, and `providerName`.
|
|
93
217
|
|
|
94
218
|
---
|
|
95
219
|
|
|
96
|
-
##
|
|
220
|
+
## How model discovery works
|
|
221
|
+
|
|
222
|
+
The engine exposes `listModels()` — a pure function with **two** lister branches matching the two protocols:
|
|
223
|
+
|
|
224
|
+
| Protocol | Endpoint | Covers |
|
|
225
|
+
| :-- | :-- | :-- |
|
|
226
|
+
| `openai` | `GET {baseUrl}/models` | openai, deepseek, groq, mistral, xai, gemini, openrouter, ollama, custom OpenAI-style |
|
|
227
|
+
| `anthropic` | `GET {baseUrl}/v1/models` (paginated) | anthropic, custom Anthropic-style |
|
|
228
|
+
|
|
229
|
+
A custom-model entry is always available — essential for OpenRouter's huge catalog and as a fallback when listing fails.
|
|
230
|
+
|
|
231
|
+
---
|
|
232
|
+
|
|
233
|
+
## Repository layout
|
|
97
234
|
|
|
98
235
|
```
|
|
99
236
|
src/
|
|
100
|
-
├── fingerprint/
|
|
101
|
-
├── filesystem/
|
|
102
|
-
├── parser/
|
|
103
|
-
├── graph/
|
|
104
|
-
├── scoring/
|
|
105
|
-
├── clustering/
|
|
106
|
-
├── summarizer/
|
|
107
|
-
|
|
108
|
-
├──
|
|
109
|
-
├──
|
|
110
|
-
├──
|
|
111
|
-
├──
|
|
112
|
-
|
|
237
|
+
├── fingerprint/ # Detect framework, language, router, state, data layer
|
|
238
|
+
├── filesystem/ # Route detection (Next.js, React Router, Express, etc.)
|
|
239
|
+
├── parser/ # AST extraction → nodes (ts-morph)
|
|
240
|
+
├── graph/ # Edge detectors, traversal API, lookup maps
|
|
241
|
+
├── scoring/ # Multi-pass importance scoring + noise filtering
|
|
242
|
+
├── clustering/ # Cohesive cluster computation
|
|
243
|
+
├── summarizer/ # LLM summarization pipeline, prompts, checkpoints
|
|
244
|
+
│ └── providers/ # Generic OpenAI & Anthropic clients, model discovery
|
|
245
|
+
├── pipeline/ # analyzePipeline — orchestrates everything
|
|
246
|
+
├── jobs/ # Job queue, concurrency, SSE progress
|
|
247
|
+
├── storage/ # File-based graph persistence (~/.devlens)
|
|
248
|
+
├── config/ # Provider config resolution (types, catalog, writer, env)
|
|
249
|
+
├── server/ # HTTP API server (consumed by Web UI)
|
|
250
|
+
└── debug/ # Export and dev utilities
|
|
113
251
|
```
|
|
114
252
|
|
|
115
253
|
---
|
|
116
254
|
|
|
117
255
|
## Scripts
|
|
118
256
|
|
|
119
|
-
|
|
|
257
|
+
| Command | What it does |
|
|
120
258
|
| :-- | :-- |
|
|
121
|
-
| `bun run dev` |
|
|
122
|
-
| `bun run start` |
|
|
123
|
-
| `bun run build` |
|
|
124
|
-
| `bun test` |
|
|
125
|
-
| `bun run export-graph` |
|
|
259
|
+
| `bun run dev` | Watch-mode HTTP server |
|
|
260
|
+
| `bun run start` | Run the HTTP server |
|
|
261
|
+
| `bun run build` | Build `dist/` (the published artifact) |
|
|
262
|
+
| `bun test` | Run the test suite |
|
|
263
|
+
| `bun run export-graph` | Dump a graph for debugging |
|
|
126
264
|
|
|
127
265
|
---
|
|
128
266
|
|
|
129
267
|
## Relationship to DevLens OSS
|
|
130
268
|
|
|
131
|
-
`devlensio` is published to npm and consumed by [DevLens OSS](https://github.com/devlensio/devlensOSS), which provides the
|
|
269
|
+
`devlensio` is published to npm and consumed by [DevLens OSS](https://github.com/devlensio/devlensOSS), which provides the CLI, MCP server, Agent Skill, and Web UI on top of this engine.
|
|
270
|
+
|
|
271
|
+
The CLI binaries bundle whatever version of `devlensio` resolves at build time — so engine fixes ship to users after a `devlensio` release **and** a bump of the dependency pin in DevLens OSS.
|
|
132
272
|
|
|
133
273
|
---
|
|
134
274
|
|
package/dist/config/index.d.ts
CHANGED
|
@@ -1,10 +1,22 @@
|
|
|
1
|
-
import { type DevLensConfig } from "./types.js";
|
|
1
|
+
import { type DevLensConfig, type ProviderConfigEntry } from "./types.js";
|
|
2
2
|
export declare function detectOllama(): Promise<boolean>;
|
|
3
3
|
export declare function initConfig(): Promise<void>;
|
|
4
4
|
export declare function resolveConfig(req?: Request): DevLensConfig;
|
|
5
5
|
export type { DevLensConfig } from "./types.js";
|
|
6
6
|
export type { SafeConfig } from "./writer.js";
|
|
7
|
-
export { maskConfig, writeConfig } from "./writer.js";
|
|
7
|
+
export { maskConfig, writeConfig, atomicWrite } from "./writer.js";
|
|
8
8
|
export { CONFIG_FILE, CONFIG_DIR, ENV } from "./providers/file.js";
|
|
9
9
|
export { sanitizeHeaders, CONFIG_HEADERS } from "./types.js";
|
|
10
10
|
export { OLLAMA_DEFAULTS, ANTHROPIC_DEFAULTS } from "./types.js";
|
|
11
|
+
export type { ProviderConfigEntry, MultiProviderStorage } from "./types.js";
|
|
12
|
+
export { makeProviderKey, parseProviderKey } from "./types.js";
|
|
13
|
+
export interface AllProvidersResult {
|
|
14
|
+
active: string;
|
|
15
|
+
providers: ProviderConfigEntry[];
|
|
16
|
+
}
|
|
17
|
+
/** Return ALL configured providers (from disk) — for the frontend settings UI. */
|
|
18
|
+
export declare function resolveAllProviders(): AllProvidersResult;
|
|
19
|
+
/** Switch the active provider by composite key. */
|
|
20
|
+
export declare function setActiveProvider(key: string): void;
|
|
21
|
+
/** Remove a provider entry by composite key. Cannot remove the active provider. */
|
|
22
|
+
export declare function removeProvider(key: string): void;
|
package/dist/config/index.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
-
import { OLLAMA_DEFAULTS, ANTHROPIC_DEFAULTS } from "./types.js";
|
|
1
|
+
import { OLLAMA_DEFAULTS, ANTHROPIC_DEFAULTS, makeProviderKey } from "./types.js";
|
|
2
2
|
import { loadFileConfig } from "./providers/file.js";
|
|
3
3
|
import { applyRequestHeaders } from "./providers/request.js";
|
|
4
|
+
import { atomicWrite } from "./writer.js";
|
|
5
|
+
import { CONFIG_FILE } from "./providers/paths.js";
|
|
6
|
+
import fs from "fs";
|
|
4
7
|
// Ollama Detection
|
|
5
8
|
//
|
|
6
9
|
// Pings Ollama's default endpoint at server startup.
|
|
@@ -72,7 +75,79 @@ export function resolveConfig(req) {
|
|
|
72
75
|
// Step 4 — apply header overrides for cloud users
|
|
73
76
|
return applyRequestHeaders(fileConfig, req);
|
|
74
77
|
}
|
|
75
|
-
export { maskConfig, writeConfig } from "./writer.js";
|
|
78
|
+
export { maskConfig, writeConfig, atomicWrite } from "./writer.js";
|
|
76
79
|
export { CONFIG_FILE, CONFIG_DIR, ENV } from "./providers/file.js";
|
|
77
80
|
export { sanitizeHeaders, CONFIG_HEADERS } from "./types.js";
|
|
78
81
|
export { OLLAMA_DEFAULTS, ANTHROPIC_DEFAULTS } from "./types.js";
|
|
82
|
+
export { makeProviderKey, parseProviderKey } from "./types.js";
|
|
83
|
+
/** Read the raw multi-provider storage from config.json (no defaults applied). */
|
|
84
|
+
function readProviderStorage() {
|
|
85
|
+
if (!fs.existsSync(CONFIG_FILE))
|
|
86
|
+
return null;
|
|
87
|
+
try {
|
|
88
|
+
const raw = JSON.parse(fs.readFileSync(CONFIG_FILE, "utf-8"));
|
|
89
|
+
const s = raw?.summarization;
|
|
90
|
+
// Detect multi-provider format
|
|
91
|
+
if (s && typeof s === "object" && s.providers && typeof s.providers === "object" && typeof s.active === "string") {
|
|
92
|
+
return s;
|
|
93
|
+
}
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/** Return ALL configured providers (from disk) — for the frontend settings UI. */
|
|
101
|
+
export function resolveAllProviders() {
|
|
102
|
+
const storage = readProviderStorage();
|
|
103
|
+
if (storage) {
|
|
104
|
+
return {
|
|
105
|
+
active: storage.active,
|
|
106
|
+
providers: Object.values(storage.providers),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
// Fallback: use the resolved active config to synthesise one entry
|
|
110
|
+
const config = loadFileConfig(ANTHROPIC_DEFAULTS);
|
|
111
|
+
const key = makeProviderKey(config.summarization.provider, config.summarization.providerName ?? config.summarization.provider);
|
|
112
|
+
return {
|
|
113
|
+
active: key,
|
|
114
|
+
providers: [{
|
|
115
|
+
provider: config.summarization.provider,
|
|
116
|
+
providerName: config.summarization.providerName ?? config.summarization.provider,
|
|
117
|
+
model: config.summarization.model,
|
|
118
|
+
apiKey: config.summarization.apiKey,
|
|
119
|
+
baseUrl: config.summarization.baseUrl,
|
|
120
|
+
batchSize: config.summarization.batchSize,
|
|
121
|
+
}],
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
/** Switch the active provider by composite key. */
|
|
125
|
+
export function setActiveProvider(key) {
|
|
126
|
+
const storage = readProviderStorage();
|
|
127
|
+
if (!storage)
|
|
128
|
+
throw new Error("No multi-provider config found. Save a provider first.");
|
|
129
|
+
if (!storage.providers[key])
|
|
130
|
+
throw new Error(`Provider "${key}" not found in config.`);
|
|
131
|
+
storage.active = key;
|
|
132
|
+
if (!fs.existsSync(CONFIG_FILE))
|
|
133
|
+
throw new Error("Config file missing.");
|
|
134
|
+
const raw = JSON.parse(fs.readFileSync(CONFIG_FILE, "utf-8"));
|
|
135
|
+
raw.summarization = storage;
|
|
136
|
+
atomicWrite(CONFIG_FILE, JSON.stringify(raw, null, 2));
|
|
137
|
+
}
|
|
138
|
+
/** Remove a provider entry by composite key. Cannot remove the active provider. */
|
|
139
|
+
export function removeProvider(key) {
|
|
140
|
+
const storage = readProviderStorage();
|
|
141
|
+
if (!storage)
|
|
142
|
+
throw new Error("No multi-provider config found.");
|
|
143
|
+
if (!storage.providers[key])
|
|
144
|
+
throw new Error(`Provider "${key}" not found.`);
|
|
145
|
+
if (storage.active === key)
|
|
146
|
+
throw new Error(`Cannot remove the active provider. Switch to another provider first.`);
|
|
147
|
+
delete storage.providers[key];
|
|
148
|
+
if (!fs.existsSync(CONFIG_FILE))
|
|
149
|
+
throw new Error("Config file missing.");
|
|
150
|
+
const raw = JSON.parse(fs.readFileSync(CONFIG_FILE, "utf-8"));
|
|
151
|
+
raw.summarization = storage;
|
|
152
|
+
atomicWrite(CONFIG_FILE, JSON.stringify(raw, null, 2));
|
|
153
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export interface CatalogProvider {
|
|
2
|
+
name: string;
|
|
3
|
+
label: string;
|
|
4
|
+
protocol: "openai" | "anthropic";
|
|
5
|
+
baseUrl: string;
|
|
6
|
+
requiresKey: boolean;
|
|
7
|
+
}
|
|
8
|
+
/** Same shape as CatalogProvider — input type for saveProvider. */
|
|
9
|
+
export type CatalogProviderInput = CatalogProvider;
|
|
10
|
+
export declare function loadCatalog(): CatalogProvider[];
|
|
11
|
+
export declare function findProvider(name: string): CatalogProvider | undefined;
|
|
12
|
+
/** Returns only the user's overrides (not merged with defaults). */
|
|
13
|
+
export declare function listUserProviders(): CatalogProvider[];
|
|
14
|
+
/**
|
|
15
|
+
* Upsert a provider entry in the user's providers.json.
|
|
16
|
+
* Validates the entry before writing. Returns the entry as written.
|
|
17
|
+
* The user file only stores the override layer — never the merged result.
|
|
18
|
+
*/
|
|
19
|
+
export declare function saveProvider(entry: CatalogProviderInput): CatalogProvider;
|
|
20
|
+
/**
|
|
21
|
+
* Remove a user override entry by name.
|
|
22
|
+
* Returns true if an entry was removed, false if it wasn't present.
|
|
23
|
+
* Only removes entries from the user file — default providers cannot be removed.
|
|
24
|
+
*/
|
|
25
|
+
export declare function removeProvider(name: string): boolean;
|
|
26
|
+
/** Delete the entire user providers.json — catalog reverts to shipped defaults. */
|
|
27
|
+
export declare function resetUserCatalog(): void;
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { CONFIG_DIR } from "./paths.js";
|
|
4
|
+
import { DEFAULT_PROVIDERS } from "./providers.default.js";
|
|
5
|
+
// ─── Paths ───────────────────────────────────────────────────────────────────
|
|
6
|
+
const USER_CATALOG_FILE = path.join(CONFIG_DIR, "providers.json");
|
|
7
|
+
// ─── Atomic write helper ─────────────────────────────────────────────────────
|
|
8
|
+
// Matches the existing pattern in writer.ts and fileStorage.ts.
|
|
9
|
+
function atomicWrite(filePath, content) {
|
|
10
|
+
const tmp = `${filePath}.tmp`;
|
|
11
|
+
fs.writeFileSync(tmp, content, "utf-8");
|
|
12
|
+
fs.renameSync(tmp, filePath);
|
|
13
|
+
}
|
|
14
|
+
// ─── Validation ──────────────────────────────────────────────────────────────
|
|
15
|
+
const VALID_PROTOCOLS = new Set(["openai", "anthropic"]);
|
|
16
|
+
const RESERVED_NAMES = new Set(["__custom__"]);
|
|
17
|
+
function validateProvider(entry) {
|
|
18
|
+
const { name, label, protocol, baseUrl, requiresKey } = entry;
|
|
19
|
+
if (typeof name !== "string" || !/^[a-z0-9-]+$/.test(name) || name.length === 0) {
|
|
20
|
+
throw new Error(`Invalid provider name: "${name}". Must be non-empty, lowercase alphanumeric with hyphens.`);
|
|
21
|
+
}
|
|
22
|
+
if (RESERVED_NAMES.has(name)) {
|
|
23
|
+
throw new Error(`"${name}" is a reserved name and cannot be used as a provider name.`);
|
|
24
|
+
}
|
|
25
|
+
if (typeof label !== "string" || label.trim().length === 0) {
|
|
26
|
+
throw new Error("Provider label is required and must be non-empty.");
|
|
27
|
+
}
|
|
28
|
+
if (!VALID_PROTOCOLS.has(protocol)) {
|
|
29
|
+
throw new Error(`Protocol must be "openai" or "anthropic", got "${protocol}".`);
|
|
30
|
+
}
|
|
31
|
+
if (typeof baseUrl !== "string" || baseUrl.length === 0) {
|
|
32
|
+
throw new Error("baseUrl is required.");
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
const parsed = new URL(baseUrl);
|
|
36
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
37
|
+
throw new Error();
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
throw new Error(`baseUrl must be a valid http(s) URL, got "${baseUrl}".`);
|
|
42
|
+
}
|
|
43
|
+
if (typeof requiresKey !== "boolean") {
|
|
44
|
+
throw new Error("requiresKey must be a boolean.");
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
// ─── Read user file (unmerged, warn on malformed) ────────────────────────────
|
|
48
|
+
function readUserFile() {
|
|
49
|
+
if (!fs.existsSync(USER_CATALOG_FILE))
|
|
50
|
+
return [];
|
|
51
|
+
try {
|
|
52
|
+
const raw = fs.readFileSync(USER_CATALOG_FILE, "utf-8");
|
|
53
|
+
const parsed = JSON.parse(raw);
|
|
54
|
+
return parsed.providers ?? [];
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
console.warn(`DevLens: ~/.devlens/providers.json contains invalid JSON. ` +
|
|
58
|
+
`Treating it as empty.`);
|
|
59
|
+
return [];
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
// ─── Ensure CONFIG_DIR exists ───────────────────────────────────────────────
|
|
63
|
+
function ensureConfigDir() {
|
|
64
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
65
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
// ─── Public read API ─────────────────────────────────────────────────────────
|
|
69
|
+
export function loadCatalog() {
|
|
70
|
+
const defaults = DEFAULT_PROVIDERS;
|
|
71
|
+
const userProviders = readUserFile();
|
|
72
|
+
if (userProviders.length === 0)
|
|
73
|
+
return defaults;
|
|
74
|
+
const merged = new Map();
|
|
75
|
+
for (const p of defaults)
|
|
76
|
+
merged.set(p.name, p);
|
|
77
|
+
for (const p of userProviders) {
|
|
78
|
+
merged.set(p.name, { ...p });
|
|
79
|
+
}
|
|
80
|
+
return [...merged.values()];
|
|
81
|
+
}
|
|
82
|
+
export function findProvider(name) {
|
|
83
|
+
return loadCatalog().find(p => p.name === name);
|
|
84
|
+
}
|
|
85
|
+
/** Returns only the user's overrides (not merged with defaults). */
|
|
86
|
+
export function listUserProviders() {
|
|
87
|
+
return readUserFile();
|
|
88
|
+
}
|
|
89
|
+
// ─── Public write API ────────────────────────────────────────────────────────
|
|
90
|
+
/**
|
|
91
|
+
* Upsert a provider entry in the user's providers.json.
|
|
92
|
+
* Validates the entry before writing. Returns the entry as written.
|
|
93
|
+
* The user file only stores the override layer — never the merged result.
|
|
94
|
+
*/
|
|
95
|
+
export function saveProvider(entry) {
|
|
96
|
+
validateProvider(entry);
|
|
97
|
+
const existing = readUserFile();
|
|
98
|
+
const idx = existing.findIndex(p => p.name === entry.name);
|
|
99
|
+
const clean = { name: entry.name, label: entry.label, protocol: entry.protocol, baseUrl: entry.baseUrl, requiresKey: entry.requiresKey };
|
|
100
|
+
if (idx >= 0) {
|
|
101
|
+
existing[idx] = clean;
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
existing.push(clean);
|
|
105
|
+
}
|
|
106
|
+
ensureConfigDir();
|
|
107
|
+
atomicWrite(USER_CATALOG_FILE, JSON.stringify({ version: 2, providers: existing }, null, 2));
|
|
108
|
+
return clean;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Remove a user override entry by name.
|
|
112
|
+
* Returns true if an entry was removed, false if it wasn't present.
|
|
113
|
+
* Only removes entries from the user file — default providers cannot be removed.
|
|
114
|
+
*/
|
|
115
|
+
export function removeProvider(name) {
|
|
116
|
+
const existing = readUserFile();
|
|
117
|
+
const idx = existing.findIndex(p => p.name === name);
|
|
118
|
+
if (idx === -1)
|
|
119
|
+
return false;
|
|
120
|
+
existing.splice(idx, 1);
|
|
121
|
+
ensureConfigDir();
|
|
122
|
+
atomicWrite(USER_CATALOG_FILE, JSON.stringify({ version: 2, providers: existing }, null, 2));
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
/** Delete the entire user providers.json — catalog reverts to shipped defaults. */
|
|
126
|
+
export function resetUserCatalog() {
|
|
127
|
+
if (fs.existsSync(USER_CATALOG_FILE)) {
|
|
128
|
+
fs.unlinkSync(USER_CATALOG_FILE);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { type DevLensConfig } from "../types.js";
|
|
2
|
-
export
|
|
3
|
-
export declare const CONFIG_FILE: string;
|
|
2
|
+
export { CONFIG_DIR, CONFIG_FILE } from "./paths.js";
|
|
4
3
|
export declare const ENV: {
|
|
5
4
|
readonly LLM_PROVIDER: "DEVLENS_LLM_PROVIDER";
|
|
5
|
+
readonly LLM_PROVIDER_NAME: "DEVLENS_LLM_PROVIDER_NAME";
|
|
6
6
|
readonly LLM_MODEL: "DEVLENS_LLM_MODEL";
|
|
7
7
|
readonly LLM_KEY: "DEVLENS_LLM_KEY";
|
|
8
8
|
readonly LLM_BASE_URL: "DEVLENS_LLM_BASE_URL";
|
|
@@ -16,4 +16,6 @@ export declare const ENV: {
|
|
|
16
16
|
readonly NEO4J_PASSWORD: "DEVLENS_NEO4J_PASSWORD";
|
|
17
17
|
readonly NEO4J_STORECODE: "NEO4J_STORE_CODE";
|
|
18
18
|
};
|
|
19
|
+
/** Public — reads the raw config.json as a plain object. Used by multi-provider helpers. */
|
|
20
|
+
export declare function readRawConfigFile(): Record<string, unknown>;
|
|
19
21
|
export declare function loadFileConfig(defaults?: DevLensConfig): DevLensConfig;
|