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.
Files changed (47) hide show
  1. package/LICENSE +0 -0
  2. package/README.md +212 -74
  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/backendRoutes.d.ts +2 -2
  19. package/dist/filesystem/backendRoutes.js +317 -18
  20. package/dist/filesystem/index.js +7 -3
  21. package/dist/filesystem/index.test.js +274 -0
  22. package/dist/fingerprint/detectors.js +8 -2
  23. package/dist/fingerprint/index.test.js +54 -0
  24. package/dist/graph/edges/callEdges.js +12 -2
  25. package/dist/index.d.ts +6 -1
  26. package/dist/index.js +5 -1
  27. package/dist/parser/extractors/components.js +3 -0
  28. package/dist/parser/extractors/functions.d.ts +1 -0
  29. package/dist/parser/extractors/functions.js +11 -2
  30. package/dist/parser/extractors/hooks.js +5 -0
  31. package/dist/parser/index.test.js +40 -0
  32. package/dist/summarizer/providers/anthropic.d.ts +2 -1
  33. package/dist/summarizer/providers/anthropic.js +3 -2
  34. package/dist/summarizer/providers/index.js +19 -35
  35. package/dist/summarizer/providers/models.d.ts +11 -0
  36. package/dist/summarizer/providers/models.js +113 -0
  37. package/dist/summarizer/providers/openai.d.ts +2 -1
  38. package/dist/summarizer/providers/openai.js +2 -1
  39. package/dist/summarizer/providers/types.d.ts +1 -6
  40. package/dist/types.d.ts +2 -2
  41. package/package.json +1 -1
  42. package/dist/summarizer/providers/gemini.d.ts +0 -9
  43. package/dist/summarizer/providers/gemini.js +0 -79
  44. package/dist/summarizer/providers/ollama.d.ts +0 -9
  45. package/dist/summarizer/providers/ollama.js +0 -23
  46. package/dist/summarizer/providers/openRouter.d.ts +0 -9
  47. package/dist/summarizer/providers/openRouter.js +0 -19
package/LICENSE CHANGED
File without changes
package/README.md CHANGED
@@ -1,136 +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] Route detection extract routes (Next.js app/pages, React Router / TanStack / wouter,
30
- Express, Fastify, Koa)
31
- [3] Parse (ts-morph) walk every .ts/.tsx/.js/.jsx → nodes (typed params, return types, prop types)
32
- [4] Edge detection many detectors → CALLS, IMPORTS, READS_FROM, WRITES_TO, PROP_PASS, EMITS,
33
- LISTENS, WRAPPED_BY, GUARDS, HANDLES, TESTS, USES, NEXTJS_API_CALL, NAVIGATES_TO
34
- [5] Scoring multi-pass importance scoring + noise filtering (no AI)
35
- [6] Clustering cohesive cluster assignment
36
- [7] Summarize (optional) topologically-ordered LLM summaries, checkpoint/resume, MapReduce
37
- │
38
- ▼
39
- Graph persisted to ~/.devlens → queried via the traversal API / CLI / MCP / UI
35
+ ```bash
36
+ npm install devlensio
37
+ # or
38
+ bun add devlensio
40
39
  ```
41
40
 
42
- 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.
43
42
 
44
43
  ---
45
44
 
46
45
  ## Public API
47
46
 
48
- ```ts
47
+ ```typescript
49
48
  import {
50
- analyzePipeline, // build the graph (nodes, edges, scores)
51
- runSummarization, // generate technical/business/security summaries
52
- computeClusters, // cohesive clustering
53
- buildGraphIndex, // index nodes+edges for traversal
54
- getBlastRadius, // upstream dependents ("what breaks if I change this")
55
- getKHop, // downstream dependencies ("what this needs")
56
- getSubgraph, // cohesive cluster around a seed
57
- findCycles, // circular-dependency groups
58
- resolveConfig, initConfig, // LLM provider config (~/.devlens/config.json)
59
- 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
60
65
  } from "devlensio";
61
66
 
62
- // Analyze a repo → graph
63
- const result = await analyzePipeline("/path/to/repo", /* isGithubRepo */ false);
64
- // result.allNodes, result.allEdges, result.nodeScores
67
+ // Analyze a repo
68
+ const result = await analyzePipeline("/path/to/repo");
65
69
 
66
- // Query the graph
67
- const index = buildGraphIndex(result.allNodes, result.allEdges);
70
+ // Traverse the graph
71
+ const index = buildGraphIndex(result.allNodes, result.allEdges);
68
72
  const impact = getBlastRadius(index, "src/auth/login.ts::login", { radius: 2 });
69
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
+ });
70
100
  ```
71
101
 
72
- 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**
73
123
 
74
- ### 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`
75
125
 
76
- - **Node types:** `COMPONENT`, `HOOK`, `FUNCTION`, `STATE_STORE`, `UTILITY`, `FILE`, `ROUTE`, `TEST`, `STORY`, `THIRD_PARTY` (+ internal `GHOST`).
77
- - **Route subtypes** (on `ROUTE` nodes via `metadata.routeNodeType`): `PAGE`, `LAYOUT`, `API_ROUTE`, `LOADING`, `ERROR`, `MIDDLEWARE`, `NOT_FOUND` (Next.js), and `REACT_ROUTER_ROUTE` (React Router / TanStack / wouter, defined in code rather than on the filesystem). Navigation targets that can't be resolved to a known route — dynamic paths, unmatched paths, or external URLs — become `[route]::…` ROUTE nodes flagged `isUnresolved` / `isExternal`.
78
- - **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), `NAVIGATES_TO` (client-side navigation — `navigate()`, `router.push/replace`, `history.*`, `window.location.*`, `<Link>`/`<NavLink>` — from a component/hook/function → the ROUTE it targets).
79
- - 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).
80
127
 
81
128
  ---
82
129
 
83
130
  ## Configuration
84
131
 
85
- 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
86
178
 
87
179
  ```env
88
- LLM_PROVIDER=openrouter # ollama | openai | anthropic | openrouter | gemini
89
- LLM_MODEL=grok-4.1-fast
90
- LLM_API_KEY=your_api_key # not needed for ollama
91
- 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
+ }
92
214
  ```
93
215
 
94
- 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`.
95
217
 
96
218
  ---
97
219
 
98
- ## 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
99
234
 
100
235
  ```
101
236
  src/
102
- ├── fingerprint/ # detect framework, language, router, state, data layer, databases
103
- ├── filesystem/ # route detection (Next.js app/pages, React Router/TanStack/wouter, Express, Fastify, Koa)
104
- ├── parser/ # ts-morph AST extraction → nodes
105
- ├── graph/ # edge detectors, traversal API, third-party libs, lookup maps
106
- ├── scoring/ # multi-pass importance scoring + noise filtering
107
- ├── clustering/ # cohesive cluster computation
108
- ├── summarizer/ # LLM summarization (technical/business/security), prompts, checkpoints
109
- ├── pipeline/ # analyzePipeline — orchestrates the whole analysis
110
- ├── jobs/ # job queue, concurrency, SSE progress events
111
- ├── storage/ # file-based graph persistence (~/.devlens)
112
- ├── config/ # provider config resolution
113
- ├── server/ # HTTP API server (consumed by the DevLens Web UI)
114
- └── 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
115
251
  ```
116
252
 
117
253
  ---
118
254
 
119
255
  ## Scripts
120
256
 
121
- | Script | Does |
257
+ | Command | What it does |
122
258
  | :-- | :-- |
123
- | `bun run dev` | watch-mode HTTP server (`src/server/index.ts`) |
124
- | `bun run start` | run the HTTP server |
125
- | `bun run build` | `tsc --project tsconfig.build.json` → `dist/` (the published artifact) |
126
- | `bun test` | run the test suite |
127
- | `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 |
128
264
 
129
265
  ---
130
266
 
131
267
  ## Relationship to DevLens OSS
132
268
 
133
- `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.
134
272
 
135
273
  ---
136
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;