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
@@ -1,43 +1,27 @@
1
- // providers/index.ts has one job — given the config, return the right LLMClient instance. It's the factory that the batch loop calls so it never has to know which provider is being used.
1
+ // providers/index.ts has one job — given the config, return the right LLMClient instance.
2
+ // It's the factory that the batch loop calls so it never has to know which provider is being used.
3
+ // The switch has exactly 2 arms: the two wire protocols (openai, anthropic).
4
+ // Brand identity (providerName) is data — resolved from the catalog or user input.
5
+ import { findProvider } from "../../config/providers/catalog.js";
2
6
  import { AnthropicClient } from "./anthropic.js";
3
- import { GeminiClient } from "./gemini.js";
4
- import { OllamaClient } from "./ollama.js";
5
7
  import { OpenAIClient } from "./openai.js";
6
- import { OpenRouterClient } from "./openRouter.js";
7
- // ─── Factory ──────────────────────────────────────────────────────────────────
8
- //
9
- // Single entry point for the batch loop — returns the right LLMClient
10
- // based on config. Caller never imports individual provider classes.
11
- //
12
- // Throws clearly if:
13
- // - provider is unknown
14
- // - apiKey is missing for cloud providers (fail fast before the batch loop)
15
8
  export function createLLMClient(config) {
16
- const { provider, model, apiKey, baseUrl } = config;
9
+ const { provider, providerName, model, apiKey, baseUrl } = config;
10
+ const entry = findProvider(providerName ?? "");
11
+ const needsKey = entry?.requiresKey ?? true; // unknown/custom → require key
12
+ const effectiveBase = baseUrl ?? entry?.baseUrl;
17
13
  switch (provider) {
18
- case "anthropic":
14
+ case "openai": {
15
+ if (needsKey && !apiKey)
16
+ throw new Error(`Provider "${providerName}" requires an API key`);
17
+ return new OpenAIClient(apiKey ?? "ollama", model, effectiveBase, providerName);
18
+ }
19
+ case "anthropic": {
19
20
  if (!apiKey)
20
- throw new Error("Anthropic provider requires an API key");
21
- return new AnthropicClient(apiKey, model);
22
- case "openai":
23
- if (!apiKey)
24
- throw new Error("OpenAI provider requires an API key");
25
- return new OpenAIClient(apiKey, model);
26
- case "openrouter":
27
- if (!apiKey)
28
- throw new Error("OpenRouter provider requires an API key");
29
- return new OpenRouterClient(apiKey, model);
30
- case "gemini":
31
- if (!apiKey)
32
- throw new Error("Gemini provider requires an API key");
33
- return new GeminiClient(apiKey, model);
34
- case "ollama":
35
- return new OllamaClient(model, baseUrl);
36
- case "managed":
37
- // Cloud SaaS only — platform injects the key via request headers.
38
- // Should never reach here in local mode.
39
- throw new Error("Managed provider is only available in cloud mode");
21
+ throw new Error(`Provider "${providerName}" requires an API key`);
22
+ return new AnthropicClient(apiKey, model, effectiveBase, providerName);
23
+ }
40
24
  default:
41
- throw new Error(`Unknown LLM provider: ${provider}`);
25
+ throw new Error(`Unknown provider protocol: ${provider}`);
42
26
  }
43
27
  }
@@ -0,0 +1,11 @@
1
+ export interface ListModelsOpts {
2
+ protocol: "openai" | "anthropic";
3
+ baseUrl: string;
4
+ apiKey?: string;
5
+ }
6
+ /**
7
+ * Fetches the live model list from a provider's models endpoint.
8
+ * Returns deduped (case-insensitive), sorted model ID strings.
9
+ * Throws on auth/network errors with a clear message.
10
+ */
11
+ export declare function listModels(opts: ListModelsOpts): Promise<string[]>;
@@ -0,0 +1,113 @@
1
+ const FETCH_TIMEOUT_MS = 10000;
2
+ /**
3
+ * Fetches the live model list from a provider's models endpoint.
4
+ * Returns deduped (case-insensitive), sorted model ID strings.
5
+ * Throws on auth/network errors with a clear message.
6
+ */
7
+ export async function listModels(opts) {
8
+ if (opts.protocol === "openai") {
9
+ return listOpenAIModels(opts.baseUrl, opts.apiKey);
10
+ }
11
+ return listAnthropicModels(opts.baseUrl, opts.apiKey);
12
+ }
13
+ // OpenAI-style lister
14
+ // GET {baseUrl}/models | Authorization: Bearer {apiKey}
15
+ // Tolerant of URLs with or without /v1 — tries as-is first, retries with /v1 if empty.
16
+ async function listOpenAIModels(baseUrl, apiKey) {
17
+ const headers = {
18
+ "Content-Type": "application/json",
19
+ };
20
+ if (apiKey) {
21
+ headers["Authorization"] = `Bearer ${apiKey}`;
22
+ }
23
+ async function tryFetch(url) {
24
+ const controller = new AbortController();
25
+ const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
26
+ try {
27
+ return await fetch(url, { method: "GET", headers, signal: controller.signal });
28
+ }
29
+ finally {
30
+ clearTimeout(timer);
31
+ }
32
+ }
33
+ const cleanBase = baseUrl.replace(/\/+$/, "");
34
+ let res = await tryFetch(`${cleanBase}/models`);
35
+ // If URL has no /v1 segment and the request returned nothing useful, retry with /v1
36
+ if (!res.ok && !cleanBase.includes("/v1")) {
37
+ res = await tryFetch(`${cleanBase}/v1/models`);
38
+ }
39
+ if (res.status === 401) {
40
+ throw new Error("Invalid API key for this provider. Check your key in config.");
41
+ }
42
+ if (!res.ok) {
43
+ throw new Error(`Failed to fetch models: ${res.status} ${res.statusText}`);
44
+ }
45
+ const body = (await res.json());
46
+ return normalizeModels((body.data ?? []).map((m) => m.id));
47
+ }
48
+ // Anthropic-style lister
49
+ // GET {baseUrl}/v1/models | x-api-key + anthropic-version
50
+ // Paginated — loops via after_id until has_more === false
51
+ async function listAnthropicModels(baseUrl, apiKey) {
52
+ if (!apiKey) {
53
+ throw new Error("Anthropic-style provider requires an API key to list models.");
54
+ }
55
+ const headers = {
56
+ "x-api-key": apiKey,
57
+ "anthropic-version": "2023-06-01",
58
+ "Content-Type": "application/json",
59
+ };
60
+ const base = `${baseUrl.replace(/\/+$/, "")}/v1/models`;
61
+ const allIds = [];
62
+ let afterId;
63
+ let hasMore = true;
64
+ const controller = new AbortController();
65
+ const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
66
+ try {
67
+ while (hasMore) {
68
+ const url = afterId
69
+ ? `${base}?after_id=${afterId}&limit=1000`
70
+ : `${base}?limit=1000`;
71
+ const res = await fetch(url, {
72
+ method: "GET",
73
+ headers,
74
+ signal: controller.signal,
75
+ });
76
+ if (res.status === 401) {
77
+ throw new Error("Invalid API key for this provider. Check your key in config.");
78
+ }
79
+ if (!res.ok) {
80
+ throw new Error(`Failed to fetch models: ${res.status} ${res.statusText}`);
81
+ }
82
+ const body = (await res.json());
83
+ allIds.push(...(body.data ?? []).map((m) => m.id));
84
+ hasMore = body.has_more ?? false;
85
+ afterId = body.last_id;
86
+ }
87
+ return normalizeModels(allIds);
88
+ }
89
+ catch (err) {
90
+ if (err.name === "AbortError") {
91
+ throw new Error(`Timed out fetching models from ${baseUrl} after ${FETCH_TIMEOUT_MS}ms`);
92
+ }
93
+ throw err;
94
+ }
95
+ finally {
96
+ clearTimeout(timer);
97
+ }
98
+ }
99
+ // Normalization
100
+ // Dedupe case-insensitively, sort ascending.
101
+ function normalizeModels(models) {
102
+ const seen = new Set();
103
+ const result = [];
104
+ for (const m of models) {
105
+ const key = m.toLowerCase();
106
+ if (!seen.has(key)) {
107
+ seen.add(key);
108
+ result.push(m);
109
+ }
110
+ }
111
+ result.sort((a, b) => a.localeCompare(b));
112
+ return result;
113
+ }
@@ -1,9 +1,10 @@
1
1
  import type { LLMClient, LLMRequest, NodeSummaryOutput } from "./types.js";
2
2
  export declare class OpenAIClient implements LLMClient {
3
3
  readonly provider: "openai";
4
+ readonly providerName?: string;
4
5
  readonly model: string;
5
6
  private client;
6
- constructor(apiKey: string, model: string, baseURL?: string);
7
+ constructor(apiKey: string, model: string, baseURL?: string, providerName?: string);
7
8
  summarize(request: LLMRequest): Promise<NodeSummaryOutput>;
8
9
  validateConnection(): Promise<void>;
9
10
  }
@@ -29,9 +29,10 @@ function parseResponse(raw) {
29
29
  // Also used as the base for OpenRouter and Ollama —
30
30
  // both expose OpenAI-compatible APIs, just with a different baseURL.
31
31
  export class OpenAIClient {
32
- constructor(apiKey, model, baseURL) {
32
+ constructor(apiKey, model, baseURL, providerName) {
33
33
  this.provider = "openai";
34
34
  this.model = model;
35
+ this.providerName = providerName;
35
36
  this.client = new OpenAI({ apiKey, ...(baseURL ? { baseURL } : {}) });
36
37
  }
37
38
  async summarize(request) {
@@ -20,13 +20,8 @@ export interface NodeSummaryOutput {
20
20
  }
21
21
  export interface LLMClient {
22
22
  readonly provider: LLMProvider;
23
+ readonly providerName?: string;
23
24
  readonly model: string;
24
25
  summarize(request: LLMRequest): Promise<NodeSummaryOutput>;
25
26
  validateConnection(): Promise<void>;
26
27
  }
27
- export type LLMClientFactory = (config: {
28
- provider: LLMProvider;
29
- model: string;
30
- apiKey?: string;
31
- baseUrl?: string;
32
- }) => LLMClient;
package/dist/types.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  export type Language = "javascript" | "typescript" | "python" | "unknown";
2
- export type Framework = "nextjs" | "react" | "express" | "fastify" | "koa" | "unknown";
2
+ export type Framework = "nextjs" | "react" | "express" | "fastify" | "koa" | "hono" | "elysia" | "bun" | "unknown";
3
3
  export type FrontendFramework = "nextjs" | "react";
4
- export type BackendFramework = "express" | "fastify" | "koa";
4
+ export type BackendFramework = "express" | "fastify" | "koa" | "hono" | "elysia" | "bun";
5
5
  export type RouterType = "app" | "pages" | "app+pages" | "react-router" | "none";
6
6
  export type ProjectType = "frontend" | "backend" | "fullstack" | "unknown";
7
7
  export type StateLibrary = "zustand" | "redux" | "recoil" | "jotai" | "context-only";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "devlensio",
3
- "version": "0.4.4",
3
+ "version": "0.6.1",
4
4
  "description": "Codebase intelligence engine for TypeScript/JavaScript/Reactjs/Nextjs repositories.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,9 +0,0 @@
1
- import type { LLMClient, LLMRequest, NodeSummaryOutput } from "./types.js";
2
- export declare class GeminiClient implements LLMClient {
3
- readonly provider: "gemini";
4
- readonly model: string;
5
- private ai;
6
- constructor(apiKey: string, model: string);
7
- summarize(request: LLMRequest): Promise<NodeSummaryOutput>;
8
- validateConnection(): Promise<void>;
9
- }
@@ -1,79 +0,0 @@
1
- import { GoogleGenAI } from "@google/genai";
2
- // ─── Response Parser ──────────────────────────────────────────────────────────
3
- //
4
- // Same XML format as all other providers — consistent across the board.
5
- const VALID_SEVERITIES = new Set(["none", "low", "medium", "high"]);
6
- function parseXmlTag(text, tag) {
7
- const match = text.match(new RegExp(`<${tag}>([\\s\\S]*?)<\\/${tag}>`));
8
- return match ? match[1].trim() : "";
9
- }
10
- function parseResponse(raw) {
11
- const technicalSummary = parseXmlTag(raw, "technical");
12
- const businessSummary = parseXmlTag(raw, "business");
13
- const severityRaw = parseXmlTag(raw, "security_severity").toLowerCase();
14
- const securitySummary = parseXmlTag(raw, "security_summary");
15
- const severity = VALID_SEVERITIES.has(severityRaw)
16
- ? severityRaw
17
- : "none";
18
- return {
19
- technicalSummary: technicalSummary || raw.trim(),
20
- businessSummary: businessSummary || "",
21
- security: {
22
- severity,
23
- summary: severity === "none" ? "" : securitySummary,
24
- },
25
- tokensUsed: 0,
26
- };
27
- }
28
- export class GeminiClient {
29
- constructor(apiKey, model) {
30
- this.provider = "gemini";
31
- this.model = model;
32
- this.ai = new GoogleGenAI({ apiKey });
33
- }
34
- async summarize(request) {
35
- const systemMessage = request.messages.find(m => m.role === "system");
36
- const userMessages = request.messages.filter(m => m.role !== "system");
37
- const contents = userMessages.map(m => ({
38
- role: "user",
39
- parts: [{ text: m.content }],
40
- }));
41
- const response = await this.ai.models.generateContent({
42
- model: this.model,
43
- contents,
44
- config: {
45
- systemInstruction: systemMessage?.content,
46
- temperature: request.temperature ?? 0,
47
- maxOutputTokens: request.maxTokens ?? 2048,
48
- },
49
- });
50
- const raw = response.text ?? "";
51
- const result = parseResponse(raw);
52
- result.tokensUsed =
53
- (response.usageMetadata?.promptTokenCount ?? 0) +
54
- (response.usageMetadata?.candidatesTokenCount ?? 0);
55
- return result;
56
- }
57
- async validateConnection() {
58
- try {
59
- await this.ai.models.generateContent({
60
- model: this.model,
61
- contents: [{ role: "user", parts: [{ text: "hi" }] }],
62
- config: { maxOutputTokens: 10 },
63
- });
64
- }
65
- catch (err) {
66
- const status = err?.status ?? err?.statusCode ?? err?.code;
67
- const message = err?.message ?? "";
68
- if (status === 400 || message.includes("API_KEY_INVALID") || message.includes("invalid api key"))
69
- throw new Error(`Gemini API key is invalid or missing. Check your key in config.`);
70
- if (status === 403 || message.includes("PERMISSION_DENIED"))
71
- throw new Error(`Gemini API key does not have permission to use model "${this.model}".`);
72
- if (status === 404 || message.includes("models/") || message.includes("not found"))
73
- throw new Error(`Gemini model "${this.model}" not found. Check model name in config.`);
74
- if (status === 429 || message.includes("RESOURCE_EXHAUSTED"))
75
- throw new Error(`Gemini rate limit hit during connection check. Try again shortly.`);
76
- throw new Error(`Gemini connection failed: ${message || "unknown error"}`);
77
- }
78
- }
79
- }
@@ -1,9 +0,0 @@
1
- import type { LLMClient, LLMRequest, NodeSummaryOutput } from "./types.js";
2
- export declare class OllamaClient implements LLMClient {
3
- readonly provider: "ollama";
4
- readonly model: string;
5
- private inner;
6
- constructor(model: string, baseURL?: string);
7
- summarize(request: LLMRequest): Promise<NodeSummaryOutput>;
8
- validateConnection(): Promise<void>;
9
- }
@@ -1,23 +0,0 @@
1
- import { OpenAIClient } from "./openai.js";
2
- const OLLAMA_DEFAULT_BASE_URL = "http://localhost:11434";
3
- // ─── OllamaClient ─────────────────────────────────────────────────────────────
4
- //
5
- // Ollama exposes an OpenAI-compatible API at /v1 — wraps OpenAIClient.
6
- // No API key needed for local usage — passes a placeholder to satisfy the SDK.
7
- // baseURL is configurable for users running Ollama on a non-default port or host.
8
- // /v1 is always appended — user-provided baseURL should not include it.
9
- export class OllamaClient {
10
- constructor(model, baseURL) {
11
- this.provider = "ollama";
12
- this.model = model;
13
- const base = (baseURL ?? OLLAMA_DEFAULT_BASE_URL).replace(/\/+$/, "");
14
- this.inner = new OpenAIClient("ollama", // placeholder — Ollama doesn't validate the key
15
- model, `${base}/v1`);
16
- }
17
- summarize(request) {
18
- return this.inner.summarize(request);
19
- }
20
- validateConnection() {
21
- return this.inner.validateConnection();
22
- }
23
- }
@@ -1,9 +0,0 @@
1
- import type { LLMClient, LLMRequest, NodeSummaryOutput } from "./types.js";
2
- export declare class OpenRouterClient implements LLMClient {
3
- readonly provider: "openrouter";
4
- readonly model: string;
5
- private inner;
6
- constructor(apiKey: string, model: string);
7
- summarize(request: LLMRequest): Promise<NodeSummaryOutput>;
8
- validateConnection(): Promise<void>;
9
- }
@@ -1,19 +0,0 @@
1
- import { OpenAIClient } from "./openai.js";
2
- const OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1";
3
- // ─── OpenRouterClient ─────────────────────────────────────────────────────────
4
- //
5
- // OpenRouter exposes an OpenAI-compatible API — no new logic needed.
6
- // Just wraps OpenAIClient with the correct baseURL and overrides provider. (OpenAI actually set the industry standard for the response of the LLMs through API.Everyone follows the OpenAI standard, Aside of course Anthropic, because its anthropic, who even go against pentagon.)
7
- export class OpenRouterClient {
8
- constructor(apiKey, model) {
9
- this.provider = "openrouter";
10
- this.model = model;
11
- this.inner = new OpenAIClient(apiKey, model, OPENROUTER_BASE_URL);
12
- }
13
- summarize(request) {
14
- return this.inner.summarize(request);
15
- }
16
- validateConnection() {
17
- return this.inner.validateConnection();
18
- }
19
- }