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
@@ -346,3 +346,277 @@ describe("analyzeFilesystem — React Router", () => {
346
346
  deleteFakeRepo(repoPath);
347
347
  });
348
348
  });
349
+ // ─── Backend frameworks: Bun / Hono / Elysia / Express ───────────────────────────
350
+ //
351
+ // Covers three recognizers added for general backend route detection:
352
+ // 1. route-table (object-literal) — hand-rolled [{method,pattern,handler}] routers
353
+ // (the OSS shape); framework-agnostic, fires in bun scan-all mode.
354
+ // 2. imperative app.METHOD(path, handler) — Express/Hono/Elysia shared shape.
355
+ // 3. Bun.serve fetch-handler — inline switch/if URL dispatch for bare-Bun apps.
356
+ describe("analyzeFilesystem — Backend (Bun/Hono/Elysia/Express)", () => {
357
+ function createRepoWithFiles(files) {
358
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "devlens-be-test-"));
359
+ for (const [filePath, content] of Object.entries(files)) {
360
+ const fullPath = path.join(tmpDir, filePath);
361
+ fs.mkdirSync(path.dirname(fullPath), { recursive: true });
362
+ fs.writeFileSync(fullPath, content);
363
+ }
364
+ return tmpDir;
365
+ }
366
+ function backendFingerprint(framework) {
367
+ return makeFingerprint(framework, "none");
368
+ }
369
+ function findRoute(routes, method, urlPath) {
370
+ return routes.find((r) => r.type === "BACKEND_ROUTE" && r.httpMethod === method && r.urlPath === urlPath);
371
+ }
372
+ // ─── Route-table (object-literal) recognizer ─────────────────────────────
373
+ it("detects a route-table array (Bun.serve + hand-rolled router — the OSS shape)", () => {
374
+ const repoPath = createRepoWithFiles({
375
+ "src/server/router.ts": `
376
+ import { handlePreScan, handleGetGraph, handleAnalyze } from "./handlers.js";
377
+ interface Route { method: string; pattern: string; handler: (p: any, req: any) => any; }
378
+ const ROUTES: Route[] = [
379
+ { method: "GET", pattern: "/api/health", handler: () => Response.json({ status: "ok" }) },
380
+ { method: "GET", pattern: "/api/pre-scan", handler: (_p, req) => handlePreScan(req) },
381
+ { method: "POST", pattern: "/api/analyze", handler: (_p, req) => handleAnalyze(req) },
382
+ ];
383
+ export async function router(req: any) { return null; }
384
+ `,
385
+ });
386
+ const routes = analyzeFilesystem(repoPath, backendFingerprint("bun"));
387
+ expect(routes.length).toBe(3);
388
+ // Inline self-contained handler → inlineHandler populated (no handlerName).
389
+ const health = findRoute(routes, "GET", "/api/health");
390
+ expect(health).toBeDefined();
391
+ expect(health?.handlerName).toBeUndefined();
392
+ expect(health?.inlineHandler).toBeDefined();
393
+ // Arrow delegating to a bare-name call → handlerName extracted.
394
+ const preScan = findRoute(routes, "GET", "/api/pre-scan");
395
+ expect(preScan?.handlerName).toBe("handlePreScan");
396
+ expect(preScan?.inlineHandler).toBeUndefined();
397
+ const analyze = findRoute(routes, "POST", "/api/analyze");
398
+ expect(analyze?.handlerName).toBe("handleAnalyze");
399
+ });
400
+ it("resolves a bare-identifier route-table handler by name", () => {
401
+ const repoPath = createRepoWithFiles({
402
+ "src/server/router.ts": `
403
+ import { createUser } from "./handlers.js";
404
+ const ROUTES = [
405
+ { method: "POST", path: "/users", handler: createUser },
406
+ ];
407
+ `,
408
+ });
409
+ const routes = analyzeFilesystem(repoPath, backendFingerprint("bun"));
410
+ const route = findRoute(routes, "POST", "/users");
411
+ expect(route).toBeDefined();
412
+ expect(route?.handlerName).toBe("createUser");
413
+ });
414
+ it("accepts path-property aliases (url / route) and method alias (verb)", () => {
415
+ const repoPath = createRepoWithFiles({
416
+ "src/server/router.ts": `
417
+ const ROUTES = [
418
+ { verb: "GET", url: "/a", handler: () => null },
419
+ { method: "DELETE", route: "/b", handler: () => null },
420
+ ];
421
+ `,
422
+ });
423
+ const routes = analyzeFilesystem(repoPath, backendFingerprint("bun"));
424
+ expect(findRoute(routes, "GET", "/a")).toBeDefined();
425
+ expect(findRoute(routes, "DELETE", "/b")).toBeDefined();
426
+ });
427
+ it("extracts dynamic params from a route-table pattern", () => {
428
+ const repoPath = createRepoWithFiles({
429
+ "src/server/router.ts": `
430
+ const ROUTES = [
431
+ { method: "GET", pattern: "/users/:userId/posts/:postId", handler: () => null },
432
+ ];
433
+ `,
434
+ });
435
+ const routes = analyzeFilesystem(repoPath, backendFingerprint("bun"));
436
+ const route = findRoute(routes, "GET", "/users/:userId/posts/:postId");
437
+ expect(route?.isDynamic).toBe(true);
438
+ expect(route?.params).toEqual(expect.arrayContaining(["userId", "postId"]));
439
+ });
440
+ it("does NOT fire the route-table recognizer on a React Router children array", () => {
441
+ // React Router arrays use {path, element/component}, no `method` field.
442
+ const repoPath = createRepoWithFiles({
443
+ "src/router.tsx": `
444
+ const routes = [
445
+ { path: "/users", element: "<Users/>" },
446
+ { path: "/about", component: About },
447
+ ];
448
+ `,
449
+ });
450
+ // Use the react-router fingerprint so this never reaches the backend branch.
451
+ const routes = analyzeFilesystem(repoPath, makeFingerprint("react", "react-router"));
452
+ // React Router fixtures above aren't createBrowserRouter/useRoutes calls,
453
+ // so they produce no routes — but more importantly, no BACKEND_ROUTE.
454
+ expect(routes.every((r) => r.type !== "BACKEND_ROUTE")).toBe(true);
455
+ deleteFakeRepo(repoPath);
456
+ });
457
+ it("does NOT fire the route-table recognizer on an arbitrary config array", () => {
458
+ const repoPath = createRepoWithFiles({
459
+ "src/config.ts": `
460
+ const CONFIG = [
461
+ { name: "health", url: "/health" },
462
+ { name: "status", method: "ping", target: "/status" },
463
+ ];
464
+ `,
465
+ });
466
+ // Even in bun scan-all mode: first element has no method+"/path" combo, so the
467
+ // route-table gate rejects it and zero routes are emitted.
468
+ const routes = analyzeFilesystem(repoPath, backendFingerprint("bun"));
469
+ expect(routes.every((r) => r.type !== "BACKEND_ROUTE")).toBe(true);
470
+ deleteFakeRepo(repoPath);
471
+ });
472
+ // ─── Imperative recognizer (Express/Hono/Elysia share this shape) ────────
473
+ it("detects Hono routes via app.METHOD(path, handler)", () => {
474
+ const repoPath = createRepoWithFiles({
475
+ "src/server/index.ts": `
476
+ import { Hono } from "hono";
477
+ import { getUser, createUser } from "./handlers.js";
478
+ const app = new Hono();
479
+ app.get("/users/:id", getUser);
480
+ app.post("/users", createUser);
481
+ `,
482
+ });
483
+ const routes = analyzeFilesystem(repoPath, backendFingerprint("hono"));
484
+ expect(findRoute(routes, "GET", "/users/:id")?.handlerName).toBe("getUser");
485
+ expect(findRoute(routes, "POST", "/users")?.handlerName).toBe("createUser");
486
+ deleteFakeRepo(repoPath);
487
+ });
488
+ it("detects Elysia routes via the app.METHOD(path, handler) API", () => {
489
+ const repoPath = createRepoWithFiles({
490
+ "src/server/index.ts": `
491
+ import { Elysia } from "elysia";
492
+ import { echo } from "./handlers.js";
493
+ const app = new Elysia();
494
+ app.get("/", () => "hello");
495
+ app.post("/echo", echo);
496
+ `,
497
+ });
498
+ const routes = analyzeFilesystem(repoPath, backendFingerprint("elysia"));
499
+ const root = findRoute(routes, "GET", "/");
500
+ expect(root).toBeDefined();
501
+ expect(root?.inlineHandler).toBeDefined(); // self-contained arrow
502
+ expect(findRoute(routes, "POST", "/echo")?.handlerName).toBe("echo");
503
+ deleteFakeRepo(repoPath);
504
+ });
505
+ it("detects an inline arrow handler on an imperative route (Express)", () => {
506
+ const repoPath = createRepoWithFiles({
507
+ "src/server/index.ts": `
508
+ import express from "express";
509
+ const app = express();
510
+ app.get("/ping", (req, res) => res.json({ ok: true }));
511
+ `,
512
+ });
513
+ const routes = analyzeFilesystem(repoPath, backendFingerprint("express"));
514
+ const route = findRoute(routes, "GET", "/ping");
515
+ expect(route).toBeDefined();
516
+ expect(route?.inlineHandler).toBeDefined();
517
+ expect(route?.handlerName).toBeUndefined();
518
+ deleteFakeRepo(repoPath);
519
+ });
520
+ // ─── Bun.serve fetch-handler (inline switch / if dispatch) ────────────────
521
+ it("extracts ANY routes from a Bun.serve switch(pathname)", () => {
522
+ const repoPath = createRepoWithFiles({
523
+ "src/server/index.ts": `
524
+ Bun.serve({
525
+ port: 3000,
526
+ fetch(req) {
527
+ const url = new URL(req.url);
528
+ switch (url.pathname) {
529
+ case "/api/health": return Response.json({ ok: true });
530
+ case "/api/users": return Response.json([]);
531
+ default: return new Response("not found", { status: 404 });
532
+ }
533
+ }
534
+ });
535
+ `,
536
+ });
537
+ const routes = analyzeFilesystem(repoPath, backendFingerprint("bun"));
538
+ const health = findRoute(routes, "ANY", "/api/health");
539
+ const users = findRoute(routes, "ANY", "/api/users");
540
+ expect(health).toBeDefined();
541
+ expect(users).toBeDefined();
542
+ deleteFakeRepo(repoPath);
543
+ });
544
+ it("extracts ANY routes from a Bun.serve if-chain on req.url", () => {
545
+ const repoPath = createRepoWithFiles({
546
+ "src/server/index.ts": `
547
+ Bun.serve({
548
+ fetch: (req) => {
549
+ if (req.url === "/ping") return new Response("pong");
550
+ if ("/health" === req.url) return Response.json({ ok: true });
551
+ return new Response("404", { status: 404 });
552
+ }
553
+ });
554
+ `,
555
+ });
556
+ const routes = analyzeFilesystem(repoPath, backendFingerprint("bun"));
557
+ expect(findRoute(routes, "ANY", "/ping")).toBeDefined();
558
+ expect(findRoute(routes, "ANY", "/health")).toBeDefined();
559
+ deleteFakeRepo(repoPath);
560
+ });
561
+ it("does NOT misfire on unrelated string equality compares inside fetch", () => {
562
+ const repoPath = createRepoWithFiles({
563
+ "src/server/index.ts": `
564
+ Bun.serve({
565
+ fetch(req) {
566
+ const status = process.env.STATUS === "/done" ? "ok" : "bad";
567
+ if (req.url === "/real") return new Response("hit");
568
+ return new Response(status, { status: 500 });
569
+ }
570
+ });
571
+ `,
572
+ });
573
+ const routes = analyzeFilesystem(repoPath, backendFingerprint("bun"));
574
+ // Only the url-ish comparison should yield a route; the STATUS === "/done"
575
+ // compare has a non-url left side, so it must be ignored.
576
+ const backendRoutes = routes.filter((r) => r.type === "BACKEND_ROUTE" && r.urlPath === "/done");
577
+ expect(backendRoutes).toHaveLength(0);
578
+ expect(findRoute(routes, "ANY", "/real")).toBeDefined();
579
+ deleteFakeRepo(repoPath);
580
+ });
581
+ // ─── Regression ────────────────────────────────────────────────────────────
582
+ it("does not double-emit a route when recognized by multiple shapes", () => {
583
+ // A route-table entry and an imperative call for the same path+method must
584
+ // dedup to one node (shared `seen` set across recognizers).
585
+ const repoPath = createRepoWithFiles({
586
+ "src/server/index.ts": `
587
+ import { Hono } from "hono";
588
+ import { getUser } from "./handlers.js";
589
+ const app = new Hono();
590
+ app.get("/users/:id", getUser);
591
+ // Unrelated route table in the same file — different path, must still emit.
592
+ const EXTRA = [
593
+ { method: "GET", pattern: "/users/:id", handler: () => null },
594
+ ];
595
+ `,
596
+ });
597
+ const routes = analyzeFilesystem(repoPath, backendFingerprint("hono"));
598
+ const matches = routes.filter((r) => r.type === "BACKEND_ROUTE" && r.httpMethod === "GET" && r.urlPath === "/users/:id");
599
+ expect(matches).toHaveLength(1);
600
+ deleteFakeRepo(repoPath);
601
+ });
602
+ it("handles a Bun.serve app whose fetch delegates to a named router (OSS index.ts)", () => {
603
+ // The entry file calls router(req) — no if/switch inside fetch, so the
604
+ // Bun.serve recognizer must emit ZERO duplicates; routes come from the table.
605
+ const repoPath = createRepoWithFiles({
606
+ "src/server/index.ts": `
607
+ import { router } from "./router.js";
608
+ Bun.serve({ port: 3000, fetch: async (req) =>-await router(req) });
609
+ `,
610
+ "src/server/router.ts": `
611
+ const ROUTES = [
612
+ { method: "GET", pattern: "/api/health", handler: () => null },
613
+ ];
614
+ `,
615
+ });
616
+ const routes = analyzeFilesystem(repoPath, backendFingerprint("bun"));
617
+ // Only one route, from the route table; fetch delegation emits nothing.
618
+ expect(routes.filter((r) => r.type === "BACKEND_ROUTE")).toHaveLength(1);
619
+ expect(findRoute(routes, "GET", "/api/health")).toBeDefined();
620
+ deleteFakeRepo(repoPath);
621
+ });
622
+ });
@@ -2,7 +2,7 @@ import fs from "fs";
2
2
  import path from "path";
3
3
  // Framework category arrays — used instead of type casts
4
4
  const FRONTEND_FRAMEWORKS = ["nextjs", "react"];
5
- const BACKEND_FRAMEWORKS = ["express", "fastify", "koa"];
5
+ const BACKEND_FRAMEWORKS = ["express", "fastify", "koa", "hono", "elysia", "bun"];
6
6
  export function detectLanguage(repoPath) {
7
7
  if (fs.existsSync(path.join(repoPath, "tsconfig.json")))
8
8
  return "typescript";
@@ -19,6 +19,12 @@ export function detectFramework(deps) {
19
19
  return "fastify";
20
20
  if ("koa" in deps)
21
21
  return "koa";
22
+ if ("hono" in deps)
23
+ return "hono"; // hono is before bun for a reason -> hono may also use @types/bun, but we want to detect hono first or else wit would be detected as bun.
24
+ if ("elysia" in deps)
25
+ return "elysia";
26
+ if ("bun" in deps || "@types/bun" in deps)
27
+ return "bun"; //Many Bun-only projects don't list a bun package at all since Bun is the runtime, not a dependency). @types/bun is the most reliable single signal
22
28
  return "unknown";
23
29
  }
24
30
  export function detectRouter(deps, framework, repoPath) {
@@ -95,7 +101,7 @@ export function detectProjectType(framework, deps, repoPath) {
95
101
  ];
96
102
  const backendDeps = [
97
103
  "express", "fastify", "koa",
98
- "koa-router", "@fastify/router",
104
+ "koa-router", "@fastify/router", "hono", "elysia",
99
105
  ];
100
106
  const databaseDeps = [
101
107
  "prisma", "@prisma/client", "drizzle-orm",
@@ -40,6 +40,60 @@ describe("analyzeFingerprint", () => {
40
40
  expect(fingerprint.framework).toBe("unknown");
41
41
  deleteFakeRepo(repoPath);
42
42
  });
43
+ // ─── Bun / Hono / Elysia Detection ───────────────────────────────────────
44
+ it("should detect a bare Bun project via the bun package", () => {
45
+ const repoPath = createFakeRepo({ bun: "1.1.0" });
46
+ const fingerprint = analyzeFingerprint(repoPath);
47
+ expect(fingerprint.framework).toBe("bun");
48
+ deleteFakeRepo(repoPath);
49
+ });
50
+ it("should detect a bare Bun project via @types/bun (no bun package)", () => {
51
+ // Many Bun-only projects don't list a bun package — @types/bun is the signal.
52
+ const repoPath = createFakeRepo({ "@types/bun": "1.1.0" });
53
+ const fingerprint = analyzeFingerprint(repoPath);
54
+ expect(fingerprint.framework).toBe("bun");
55
+ deleteFakeRepo(repoPath);
56
+ });
57
+ it("should detect a Hono project", () => {
58
+ const repoPath = createFakeRepo({ hono: "4.0.0" });
59
+ const fingerprint = analyzeFingerprint(repoPath);
60
+ expect(fingerprint.framework).toBe("hono");
61
+ deleteFakeRepo(repoPath);
62
+ });
63
+ it("should detect an Elysia project", () => {
64
+ const repoPath = createFakeRepo({ elysia: "1.0.0" });
65
+ const fingerprint = analyzeFingerprint(repoPath);
66
+ expect(fingerprint.framework).toBe("elysia");
67
+ deleteFakeRepo(repoPath);
68
+ });
69
+ it("should detect Hono over Bun when both deps appear (shadowing)", () => {
70
+ // Hono apps on Bun often carry @types/bun — must label as hono, not bun,
71
+ // because their routes are app.METHOD-shaped and label matters downstream.
72
+ const repoPath = createFakeRepo({ hono: "4.0.0", "@types/bun": "1.1.0" });
73
+ const fingerprint = analyzeFingerprint(repoPath);
74
+ expect(fingerprint.framework).toBe("hono");
75
+ deleteFakeRepo(repoPath);
76
+ });
77
+ it("should detect Elysia over Bun when both deps appear (shadowing)", () => {
78
+ const repoPath = createFakeRepo({ elysia: "1.0.0", "@types/bun": "1.1.0" });
79
+ const fingerprint = analyzeFingerprint(repoPath);
80
+ expect(fingerprint.framework).toBe("elysia");
81
+ deleteFakeRepo(repoPath);
82
+ });
83
+ it("should keep React framing when @types/bun is present alongside react", () => {
84
+ // Frontend wins over the bare-Bun heuristic — a React app using Bun as a
85
+ // dev runner must not be mislabelled as a Bun backend.
86
+ const repoPath = createFakeRepo({ react: "18.0.0", "@types/bun": "1.1.0" });
87
+ const fingerprint = analyzeFingerprint(repoPath);
88
+ expect(fingerprint.framework).toBe("react");
89
+ deleteFakeRepo(repoPath);
90
+ });
91
+ it("should still detect express when present (regression)", () => {
92
+ const repoPath = createFakeRepo({ express: "4.18.0" });
93
+ const fingerprint = analyzeFingerprint(repoPath);
94
+ expect(fingerprint.framework).toBe("express");
95
+ deleteFakeRepo(repoPath);
96
+ });
43
97
  // ─── Router Detection ──────────────────────────────────────────────────────
44
98
  it("should detect Next.js app router", () => {
45
99
  const repoPath = createFakeRepo({ next: "14.0.0" }, ["src/app"]);
@@ -90,8 +90,18 @@ export function detectCallEdges(nodes, lookupMp) {
90
90
  }
91
91
  }
92
92
  // Skip non-third-party member-access calls (console.log, Math.round, etc.)
93
- if (calledName.includes("."))
94
- continue;
93
+ // const rootName = calledName.split(".")[0];
94
+ // const NOISE_ROOTS = new Set([
95
+ // "console", "Math", "Object", "JSON", "Date", "document",
96
+ // "Array", "String", "Number", "Boolean", "Promise", "Error",
97
+ // "setTimeout", "setInterval", "clearTimeout", "clearInterval",
98
+ // "parseInt", "parseFloat", "isNaN", "isFinite",
99
+ // ]);
100
+ // if (NOISE_ROOTS.has(rootName)) continue;
101
+ // // Check if a node exists with the full dotted name
102
+ // if (!lookupMp.nodesByName.has(calledName)) {
103
+ // continue;
104
+ // }
95
105
  // ── Local node lookup ─────────────────────────────────────────
96
106
  const targets = lookupMp.nodesByName.get(calledName);
97
107
  if (!targets || targets.length === 0)
package/dist/index.d.ts CHANGED
@@ -9,7 +9,8 @@ export * from "./pipeline/index.js";
9
9
  export * from "./config/types.js";
10
10
  export { queue } from "./jobs/index.js";
11
11
  export { storage } from "./storage/index.js";
12
- export { resolveConfig, initConfig, maskConfig, writeConfig } from "./config/index.js";
12
+ export { resolveConfig, initConfig, maskConfig, writeConfig, resolveAllProviders, setActiveProvider, removeProvider as removeProviderConfig, } from "./config/index.js";
13
+ export type { AllProvidersResult } from "./config/index.js";
13
14
  export { readPackageDependencies, categorizeLibrary } from "./graph/thirdPartyLibs.js";
14
15
  export { analyzePipeline } from "./pipeline/index.js";
15
16
  export { runSummarization } from "./summarizer/index.js";
@@ -18,3 +19,7 @@ export { EDGE_LABELS } from "./summarizer/prompts.js";
18
19
  export { CycleGroup, TopologicalResult } from "./summarizer/types.js";
19
20
  export { buildGraphIndex, getBlastRadius, getKHop, getSubgraph, findCycles, } from "./graph/traversal.js";
20
21
  export type { GraphIndex, TraversalHit, TraversalResult, TraversalOpts, SubgraphResult, } from "./graph/traversal.js";
22
+ export { loadCatalog, findProvider, saveProvider, removeProvider, resetUserCatalog, listUserProviders, } from "./config/providers/catalog.js";
23
+ export type { CatalogProvider, CatalogProviderInput } from "./config/providers/catalog.js";
24
+ export { listModels } from "./summarizer/providers/models.js";
25
+ export type { ListModelsOpts } from "./summarizer/providers/models.js";
package/dist/index.js CHANGED
@@ -11,7 +11,7 @@ export * from "./config/types.js";
11
11
  export { queue } from "./jobs/index.js";
12
12
  export { storage } from "./storage/index.js";
13
13
  // Config helpers
14
- export { resolveConfig, initConfig, maskConfig, writeConfig } from "./config/index.js";
14
+ export { resolveConfig, initConfig, maskConfig, writeConfig, resolveAllProviders, setActiveProvider, removeProvider as removeProviderConfig, } from "./config/index.js";
15
15
  // Pre-scan helpers
16
16
  export { readPackageDependencies, categorizeLibrary } from "./graph/thirdPartyLibs.js";
17
17
  // Pipeline & analysis
@@ -21,3 +21,7 @@ export { computeClusters } from "./clustering/index.js";
21
21
  export { EDGE_LABELS } from "./summarizer/prompts.js";
22
22
  // Graph traversal & queries (consumed by the MCP server)
23
23
  export { buildGraphIndex, getBlastRadius, getKHop, getSubgraph, findCycles, } from "./graph/traversal.js";
24
+ // Provider catalog (consumed by DevLens OSS frontend & CLI)
25
+ export { loadCatalog, findProvider, saveProvider, removeProvider, resetUserCatalog, listUserProviders, } from "./config/providers/catalog.js";
26
+ // Dynamic model discovery (consumed by DevLens OSS CLI)
27
+ export { listModels } from "./summarizer/providers/models.js";
@@ -2,6 +2,7 @@
2
2
  import { SyntaxKind } from "ts-morph";
3
3
  import { detectFunctionDirective } from "../directives.js";
4
4
  import { extractReturnTypeAnnotation, extractReferencedInterfaces, } from "../typeUtils.js";
5
+ import { JS_BUILTINS } from "./functions.js";
5
6
  // Generates a unique id for a node
6
7
  function makeId(filePath, name) {
7
8
  return `${filePath}::${name}`;
@@ -69,6 +70,8 @@ function extractAllCalls(node) {
69
70
  continue; // skip hooks
70
71
  if (rootName.startsWith("React"))
71
72
  continue;
73
+ if (JS_BUILTINS.has(rootName))
74
+ continue;
72
75
  if (innerFunctionNames.has(rootName))
73
76
  continue; // skip calls to inner functions
74
77
  // Capture the full expression (e.g. "axios.get" not just "axios") so
@@ -1,6 +1,7 @@
1
1
  import { SourceFile } from "ts-morph";
2
2
  import type { CodeNode } from "../../types.js";
3
3
  import { type RenderingBoundary } from "../directives.js";
4
+ export declare const JS_BUILTINS: Set<string>;
4
5
  export declare function extractFunctionCalls(node: any): string[];
5
6
  export declare function extractHookCalls(node: any): string[];
6
7
  export declare function extractApiCalls(node: any): string[];
@@ -4,6 +4,14 @@ import { detectFunctionDirective } from "../directives.js";
4
4
  import { extractParams, extractReturnTypeAnnotation, extractBareTypeNames, extractReferencedInterfaces, } from "../typeUtils.js";
5
5
  // these are used to detect the routes in the Nextjs
6
6
  const HTTP_METHOD_EXPORTS = new Set(["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"]);
7
+ export const JS_BUILTINS = new Set([
8
+ "console", "Math", "JSON", "Object", "Date",
9
+ "Promise", "Error", "Array", "String", "Number", "Boolean", "document",
10
+ "setTimeout", "setInterval", "clearTimeout", "clearInterval",
11
+ "parseInt", "parseFloat", "isNaN", "isFinite",
12
+ "Reflect", "Proxy", "Intl", "BigInt", "Symbol", "Map", "Set",
13
+ "WeakMap", "WeakSet", "ArrayBuffer", "DataView",
14
+ ]);
7
15
  function makeId(filePath, name) {
8
16
  return `${filePath}::${name}`;
9
17
  }
@@ -15,8 +23,9 @@ export function extractFunctionCalls(node) {
15
23
  // Skip hooks, they are handled by hooks extractor
16
24
  if (name.startsWith("use"))
17
25
  continue;
18
- // Skip console calls, they are noise
19
- if (name.startsWith("console"))
26
+ // Skip console calls and other noise calls, they are noise
27
+ const rootName = name.split(".")[0];
28
+ if (JS_BUILTINS.has(rootName))
20
29
  continue;
21
30
  names.push(name);
22
31
  }
@@ -1,6 +1,7 @@
1
1
  import { SyntaxKind } from "ts-morph";
2
2
  import { detectFunctionDirective } from "../directives.js";
3
3
  import { extractParams, extractBareTypeNames, extractReferencedInterfaces, } from "../typeUtils.js";
4
+ import { extractFunctionCalls } from "./functions.js";
4
5
  function makeId(filePath, name) {
5
6
  return `${filePath}::${name}`;
6
7
  }
@@ -58,6 +59,7 @@ export function extractHooks(file, fileDirective = null) {
58
59
  if (!/^use[A-Z]/.test(name))
59
60
  continue;
60
61
  const dependencies = extractDependencies(fn);
62
+ const calls = extractFunctionCalls(fn);
61
63
  const returnType = extractReturnType(fn);
62
64
  const isAsync = fn.isAsync();
63
65
  const contextRefs = extractContextRefs(fn);
@@ -75,6 +77,7 @@ export function extractHooks(file, fileDirective = null) {
75
77
  rawCode: fn.getText(),
76
78
  metadata: {
77
79
  dependencies,
80
+ calls,
78
81
  contextRefs,
79
82
  returnType,
80
83
  parameters: typedParams,
@@ -98,6 +101,7 @@ export function extractHooks(file, fileDirective = null) {
98
101
  if (!isArrow)
99
102
  continue;
100
103
  const dependencies = extractDependencies(initializer);
104
+ const calls = extractFunctionCalls(initializer);
101
105
  const returnType = extractReturnType(initializer);
102
106
  const isAsync = initializer.asKind(SyntaxKind.ArrowFunction)?.isAsync() ?? false;
103
107
  const contextRefs = extractContextRefs(initializer);
@@ -115,6 +119,7 @@ export function extractHooks(file, fileDirective = null) {
115
119
  rawCode: variable.getText(),
116
120
  metadata: {
117
121
  dependencies,
122
+ calls,
118
123
  contextRefs,
119
124
  returnType,
120
125
  parameters: typedParams,
@@ -133,6 +133,46 @@ describe("parseRepo", () => {
133
133
  expect(hook?.metadata.isAsync).toBe(true);
134
134
  deleteFakeRepo(repoPath);
135
135
  });
136
+ it("should capture non-hook calls in hook metadata.calls", () => {
137
+ const repoPath = createFakeRepo({
138
+ "src/useItems.ts": `
139
+ export const keys = {
140
+ all: () => ["items"] as const,
141
+ };
142
+ export function useItems() {
143
+ return useQuery({
144
+ queryKey: keys.all(),
145
+ queryFn: () => fetch("/api/items"),
146
+ });
147
+ }
148
+ `,
149
+ });
150
+ const result = parseRepo(repoPath);
151
+ const hook = result.nodes.find((n) => n.name === "useItems");
152
+ expect(hook?.metadata.calls).toBeDefined();
153
+ expect(hook?.metadata.calls).toContain("keys.all");
154
+ // fetch is not a hook and not a JS_BUILTIN, so it passes through
155
+ expect(hook?.metadata.calls).toContain("fetch");
156
+ deleteFakeRepo(repoPath);
157
+ });
158
+ it("should filter JS_BUILTINS calls from hook metadata.calls", () => {
159
+ const repoPath = createFakeRepo({
160
+ "src/useLogger.ts": `
161
+ export function useLogger() {
162
+ const data = JSON.parse(localStorage.getItem("x") ?? "{}");
163
+ console.log(data);
164
+ return data;
165
+ }
166
+ `,
167
+ });
168
+ const result = parseRepo(repoPath);
169
+ const hook = result.nodes.find((n) => n.name === "useLogger");
170
+ expect(hook?.metadata.calls).toBeDefined();
171
+ // JSON.* and console.* should be filtered as JS_BUILTINS
172
+ expect(hook?.metadata.calls).not.toContain("JSON.parse");
173
+ expect(hook?.metadata.calls).not.toContain("console.log");
174
+ deleteFakeRepo(repoPath);
175
+ });
136
176
  // ─── Function Detection ────────────────────────────────────────────────────
137
177
  it("should detect a regular function", () => {
138
178
  const repoPath = createFakeRepo({
@@ -1,9 +1,10 @@
1
1
  import type { LLMClient, LLMRequest, NodeSummaryOutput } from "./types.js";
2
2
  export declare class AnthropicClient implements LLMClient {
3
3
  readonly provider: "anthropic";
4
+ readonly providerName?: string;
4
5
  readonly model: string;
5
6
  private client;
6
- constructor(apiKey: string, model: 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
  }
@@ -33,10 +33,11 @@ function parseResponse(raw) {
33
33
  };
34
34
  }
35
35
  export class AnthropicClient {
36
- constructor(apiKey, model) {
36
+ constructor(apiKey, model, baseURL, providerName) {
37
37
  this.provider = "anthropic";
38
38
  this.model = model;
39
- this.client = new Anthropic({ apiKey });
39
+ this.providerName = providerName;
40
+ this.client = new Anthropic({ apiKey, ...(baseURL ? { baseURL } : {}) });
40
41
  }
41
42
  async summarize(request) {
42
43
  const systemMessage = request.messages.find(m => m.role === "system");