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.
Files changed (50) hide show
  1. package/LICENSE +0 -0
  2. package/README.md +212 -72
  3. package/dist/config/index.d.ts +14 -2
  4. package/dist/config/index.js +77 -2
  5. package/dist/config/providers/catalog.d.ts +27 -0
  6. package/dist/config/providers/catalog.js +130 -0
  7. package/dist/config/providers/file.d.ts +4 -2
  8. package/dist/config/providers/file.js +198 -32
  9. package/dist/config/providers/paths.d.ts +2 -0
  10. package/dist/config/providers/paths.js +4 -0
  11. package/dist/config/providers/providers.default.d.ts +3 -0
  12. package/dist/config/providers/providers.default.js +12 -0
  13. package/dist/config/providers/request.js +21 -9
  14. package/dist/config/types.d.ts +25 -2
  15. package/dist/config/types.js +22 -7
  16. package/dist/config/writer.d.ts +8 -2
  17. package/dist/config/writer.js +59 -12
  18. package/dist/filesystem/index.js +6 -0
  19. package/dist/filesystem/index.test.js +170 -0
  20. package/dist/filesystem/reactRouterRoutes.d.ts +2 -0
  21. package/dist/filesystem/reactRouterRoutes.js +243 -0
  22. package/dist/graph/edges/apiFetchEdges.js +1 -137
  23. package/dist/graph/edges/helpers/routeMatching.d.ts +8 -0
  24. package/dist/graph/edges/helpers/routeMatching.js +122 -0
  25. package/dist/graph/edges/navigationEdges.d.ts +5 -0
  26. package/dist/graph/edges/navigationEdges.js +339 -0
  27. package/dist/graph/edges/routeEdge.js +25 -0
  28. package/dist/graph/index.js +12 -1
  29. package/dist/graph/index.test.js +263 -8
  30. package/dist/index.d.ts +6 -1
  31. package/dist/index.js +5 -1
  32. package/dist/parser/extractors/objectMethods.js +2 -2
  33. package/dist/parser/index.test.js +0 -10
  34. package/dist/pipeline/index.js +26 -0
  35. package/dist/summarizer/providers/anthropic.d.ts +2 -1
  36. package/dist/summarizer/providers/anthropic.js +3 -2
  37. package/dist/summarizer/providers/index.js +19 -35
  38. package/dist/summarizer/providers/models.d.ts +11 -0
  39. package/dist/summarizer/providers/models.js +113 -0
  40. package/dist/summarizer/providers/openai.d.ts +2 -1
  41. package/dist/summarizer/providers/openai.js +2 -1
  42. package/dist/summarizer/providers/types.d.ts +1 -6
  43. package/dist/types.d.ts +3 -2
  44. package/package.json +1 -1
  45. package/dist/summarizer/providers/gemini.d.ts +0 -9
  46. package/dist/summarizer/providers/gemini.js +0 -79
  47. package/dist/summarizer/providers/ollama.d.ts +0 -9
  48. package/dist/summarizer/providers/ollama.js +0 -23
  49. package/dist/summarizer/providers/openRouter.d.ts +0 -9
  50. package/dist/summarizer/providers/openRouter.js +0 -19
package/LICENSE CHANGED
File without changes
package/README.md CHANGED
@@ -1,134 +1,274 @@
1
- # devlensio — the DevLens analysis engine
1
+ # `devlensio` — The DevLens Analysis Engine
2
2
 
3
3
  [![npm: devlensio](https://img.shields.io/badge/npm-devlensio-cb3837?logo=npm)](https://www.npmjs.com/package/devlensio)
4
4
  [![License: AGPL v3](https://img.shields.io/badge/License-AGPL_v3-blue.svg)](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** — nodes (components, hooks, functions, stores, routes, files, …) joined by typed edges — scores every node by architectural importance, optionally **summarizes** each node with an LLM (technical / business / security), and exposes a **traversal/query API**.
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
- `devlensio` is a **library + local server**. The user-facing tools — the `devlens` CLI, the MCP server, the Agent Skill, and the Web UI — live in [DevLens OSS](https://github.com/devlensio/devlensOSS) and consume this package.
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
- ## Install
12
+ ## What it does
13
13
 
14
- ```bash
15
- npm install devlensio
16
- # or: bun add devlensio
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
- Requires Node 18+ (or Bun). An LLM provider key is needed only for summarization, not for structural analysis.
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
- ## What it does (the pipeline)
33
+ ## Install
24
34
 
25
- ```
26
- Repo path
27
- │
28
- [1] Fingerprint detect language, framework, router, state manager, data layer, databases
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
- Structural analysis is fast and deterministic; summarization is the only step that calls an LLM and reuses unchanged nodes across commits.
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
- ```ts
47
+ ```typescript
48
48
  import {
49
- analyzePipeline, // build the graph (nodes, edges, scores)
50
- runSummarization, // generate technical/business/security summaries
51
- computeClusters, // cohesive clustering
52
- buildGraphIndex, // index nodes+edges for traversal
53
- getBlastRadius, // upstream dependents ("what breaks if I change this")
54
- getKHop, // downstream dependencies ("what this needs")
55
- getSubgraph, // cohesive cluster around a seed
56
- findCycles, // circular-dependency groups
57
- resolveConfig, initConfig, // LLM provider config (~/.devlens/config.json)
58
- storage, queue, // file-based graph storage + job queue singletons
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 → graph
62
- const result = await analyzePipeline("/path/to/repo", /* isGithubRepo */ false);
63
- // result.allNodes, result.allEdges, result.nodeScores
67
+ // Analyze a repo
68
+ const result = await analyzePipeline("/path/to/repo");
64
69
 
65
- // Query the graph
66
- const index = buildGraphIndex(result.allNodes, result.allEdges);
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`, …), config helpers (`maskConfig`, `writeConfig`), pre-scan helpers (`readPackageDependencies`, `categorizeLibrary`), and `EDGE_LABELS`. See `dist/index.d.ts` for the full surface.
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
- ### Node & edge types
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
- - **Node types:** `COMPONENT`, `HOOK`, `FUNCTION`, `STATE_STORE`, `UTILITY`, `FILE`, `ROUTE`, `TEST`, `STORY`, `THIRD_PARTY` (+ internal `GHOST`).
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
- Summarization config lives in `~/.devlens/config.json` (set via `initConfig`/`writeConfig`, or env vars loaded with dotenv). Supported providers: **Anthropic**, **OpenAI**, **OpenRouter**, **Gemini**, **Ollama** (local).
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
- LLM_PROVIDER=openrouter # ollama | openai | anthropic | openrouter | gemini
87
- LLM_MODEL=grok-4.1-fast
88
- LLM_API_KEY=your_api_key # not needed for ollama
89
- LLM_BASE_URL= # e.g. http://localhost:11434 for Ollama
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
- Graphs and config are stored under `~/.devlens` and shared with all DevLens tools.
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
- ## Repo layout
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/ # detect framework, language, router, state, data layer, databases
101
- ├── filesystem/ # route detection (Next.js app/pages, Express, Fastify, Koa)
102
- ├── parser/ # ts-morph AST extraction → nodes
103
- ├── graph/ # edge detectors, traversal API, third-party libs, lookup maps
104
- ├── scoring/ # multi-pass importance scoring + noise filtering
105
- ├── clustering/ # cohesive cluster computation
106
- ├── summarizer/ # LLM summarization (technical/business/security), prompts, checkpoints
107
- ├── pipeline/ # analyzePipeline — orchestrates the whole analysis
108
- ├── jobs/ # job queue, concurrency, SSE progress events
109
- ├── storage/ # file-based graph persistence (~/.devlens)
110
- ├── config/ # provider config resolution
111
- ├── server/ # HTTP API server (consumed by the DevLens Web UI)
112
- └── debug/ # exportGraph and dev utilities
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
- | Script | Does |
257
+ | Command | What it does |
120
258
  | :-- | :-- |
121
- | `bun run dev` | watch-mode HTTP server (`src/server/index.ts`) |
122
- | `bun run start` | run the HTTP server |
123
- | `bun run build` | `tsc --project tsconfig.build.json` → `dist/` (the published artifact) |
124
- | `bun test` | run the test suite |
125
- | `bun run export-graph` | dump a graph for debugging |
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 `devlens` CLI (`@devlensio/cli`), the MCP server, the `/devlens` Agent Skill, and the Web UI on top of this engine. 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.
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
 
@@ -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;
@@ -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 declare const CONFIG_DIR: string;
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;