codesight 1.0.1 → 1.1.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.
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  ### Your AI assistant wastes thousands of tokens every conversation just figuring out your project. codesight fixes that in one command.
4
4
 
5
- **Zero dependencies. 11 framework detectors. 4 ORM parsers. MCP server. One `npx` call.**
5
+ **Zero dependencies. 25+ framework detectors. 4 ORM parsers. MCP server. One `npx` call.**
6
6
 
7
7
  [![npm version](https://img.shields.io/npm/v/codesight?style=for-the-badge&logo=npm&color=CB3837)](https://www.npmjs.com/package/codesight)
8
8
  [![npm downloads](https://img.shields.io/npm/dm/codesight?style=for-the-badge&logo=npm&color=blue&label=Monthly%20Downloads)](https://www.npmjs.com/package/codesight)
@@ -26,9 +26,13 @@
26
26
  ---
27
27
 
28
28
  ```
29
- 0 dependencies · Node.js >= 18 · MIT
29
+ 0 dependencies · Node.js >= 18 · 27 tests · MIT
30
30
  ```
31
31
 
32
+ ## Works With
33
+
34
+ **Claude Code, Cursor, GitHub Copilot, OpenAI Codex, Windsurf, Cline, Aider**, and anything that reads markdown.
35
+
32
36
  ## Install
33
37
 
34
38
  ```bash
@@ -41,6 +45,7 @@ That's it. Run it in any project root. No config, no setup, no API keys.
41
45
  npx codesight --init # Also generate CLAUDE.md, .cursorrules, codex.md, AGENTS.md
42
46
  npx codesight --open # Also open interactive HTML report in browser
43
47
  npx codesight --mcp # Start as MCP server for Claude Code / Cursor
48
+ npx codesight --benchmark # Show detailed token savings breakdown
44
49
  ```
45
50
 
46
51
  ## What It Does
@@ -55,8 +60,6 @@ Exploration cost: ~52,000 tokens (without codesight)
55
60
  Saved: ~48,800 tokens per conversation
56
61
  ```
57
62
 
58
- Works with **Claude Code, Cursor, GitHub Copilot, OpenAI Codex, Windsurf, Cline**, and anything that reads markdown.
59
-
60
63
  ## What It Generates
61
64
 
62
65
  ```
@@ -74,13 +77,14 @@ Works with **Claude Code, Cursor, GitHub Copilot, OpenAI Codex, Windsurf, Cline*
74
77
 
75
78
  ## Routes
76
79
 
77
- Not just paths. Methods, URL parameters, what each route touches (auth, database, cache, payments, AI, email, queues), and where the handler lives. Detects routes across 11 frameworks automatically.
80
+ Not just paths. Methods, URL parameters, what each route touches (auth, database, cache, payments, AI, email, queues), and where the handler lives. Detects routes across 25+ frameworks automatically.
78
81
 
79
82
  ```markdown
80
83
  - `POST` `/auth/login` [auth, db, email]
81
84
  - `GET` `/api/projects/:id/analytics` params(id) [auth, db, cache]
82
85
  - `POST` `/api/billing/checkout` [auth, db, payment]
83
- - `POST` `/api/webhooks/stripe` [payment]
86
+ - `QUERY` `getUsers` [db] # tRPC procedures
87
+ - `MUTATION` `createProject` [db, ai] # tRPC mutations
84
88
  ```
85
89
 
86
90
  ## Schema
@@ -123,18 +127,44 @@ Every env var across your codebase, flagged as required or has default, with the
123
127
  - `PORT` (has default) — apps/api/src/index.ts
124
128
  ```
125
129
 
130
+ ## Token Benchmark
131
+
132
+ See exactly where your token savings come from:
133
+
134
+ ```bash
135
+ npx codesight --benchmark
136
+ ```
137
+
138
+ ```
139
+ Token Savings Breakdown:
140
+ ┌──────────────────────────────────────────────────┐
141
+ │ What codesight found │ Exploration cost │
142
+ ├──────────────────────────────┼────────────────────┤
143
+ │ 65 routes │ ~26,000 tokens │
144
+ │ 18 schema models │ ~ 5,400 tokens │
145
+ │ 16 components │ ~ 4,000 tokens │
146
+ │ 36 library files │ ~ 7,200 tokens │
147
+ │ 22 env vars │ ~ 2,200 tokens │
148
+ │ 92 files (search overhead) │ ~ 4,000 tokens │
149
+ ├──────────────────────────────┼────────────────────┤
150
+ │ codesight output │ ~ 4,041 tokens │
151
+ │ SAVED PER CONVERSATION │ ~64,599 tokens │
152
+ └──────────────────────────────┴────────────────────┘
153
+ ```
154
+
126
155
  ## Supported Stacks
127
156
 
128
157
  | Category | Supported |
129
158
  |---|---|
130
- | **Routes** | Hono, Express, Fastify, Next.js (App + Pages), Koa, FastAPI, Flask, Django, Go (net/http, Gin, Fiber) |
131
- | **Schema** | Drizzle, Prisma, TypeORM, SQLAlchemy |
159
+ | **Routes** | Hono, Express, Fastify, Next.js (App + Pages), Koa, NestJS, tRPC, Elysia, AdonisJS, SvelteKit, Remix, Nuxt, FastAPI, Flask, Django, Go (net/http, Gin, Fiber, Echo, Chi), Rails, Phoenix, Spring Boot, Actix, Axum, raw http.createServer |
160
+ | **Schema** | Drizzle, Prisma, TypeORM, Mongoose, Sequelize, SQLAlchemy, ActiveRecord, Ecto |
132
161
  | **Components** | React, Vue, Svelte (auto-filters shadcn/ui and Radix primitives) |
133
- | **Libraries** | TypeScript, JavaScript, Python, Go (exports with function signatures) |
162
+ | **Libraries** | TypeScript, JavaScript, Python, Go, Ruby, Elixir, Java, Kotlin, Rust (exports with function signatures) |
134
163
  | **Middleware** | Auth, rate limiting, CORS, validation, logging, error handlers |
135
164
  | **Dependencies** | Import graph with hot file detection (most imported = highest blast radius) |
136
165
  | **Contracts** | URL params, request types, response types from route handlers |
137
166
  | **Monorepos** | pnpm, npm, yarn workspaces (cross-workspace detection) |
167
+ | **Languages** | TypeScript, JavaScript, Python, Go, Ruby, Elixir, Java, Kotlin, Rust, PHP |
138
168
 
139
169
  ## AI Config Generation
140
170
 
@@ -183,6 +213,28 @@ npx codesight --open
183
213
 
184
214
  Opens an interactive HTML dashboard in your browser. Routes table with method badges and tags. Schema cards with fields and relations. Dependency hot files with impact bars. Env var audit. Token savings breakdown. Useful for onboarding or just seeing your project from above.
185
215
 
216
+ ## GitHub Action
217
+
218
+ Add to your CI pipeline to keep context fresh on every push:
219
+
220
+ ```yaml
221
+ name: codesight
222
+ on: [push]
223
+ jobs:
224
+ scan:
225
+ runs-on: ubuntu-latest
226
+ steps:
227
+ - uses: actions/checkout@v4
228
+ - uses: actions/setup-node@v4
229
+ with:
230
+ node-version: 20
231
+ - run: npm install -g codesight && codesight
232
+ - uses: actions/upload-artifact@v4
233
+ with:
234
+ name: codesight
235
+ path: .codesight/
236
+ ```
237
+
186
238
  ## Watch Mode and Git Hook
187
239
 
188
240
  **Watch mode** re-scans when files change:
@@ -210,11 +262,27 @@ npx codesight --html # Generate HTML report without opening
210
262
  npx codesight --mcp # Start MCP server
211
263
  npx codesight --watch # Watch mode
212
264
  npx codesight --hook # Install git pre-commit hook
265
+ npx codesight --benchmark # Detailed token savings breakdown
213
266
  npx codesight --json # Output as JSON
214
267
  npx codesight -o .ai-context # Custom output directory
215
268
  npx codesight -d 5 # Limit directory depth
216
269
  ```
217
270
 
271
+ ## How It Compares
272
+
273
+ Most AI context tools dump your entire codebase into one file. codesight takes a different approach: it **parses** your code to extract structured information.
274
+
275
+ | | codesight | File concatenation tools |
276
+ |---|---|---|
277
+ | **Output** | Structured routes, schema, components, deps | Raw file contents |
278
+ | **Token cost** | ~3,000-5,000 tokens | 50,000-500,000+ tokens |
279
+ | **Route detection** | 25+ frameworks auto-detected | None |
280
+ | **Schema parsing** | ORM-aware with relations | None |
281
+ | **Dependency graph** | Hot file detection | None |
282
+ | **AI config generation** | CLAUDE.md, .cursorrules, etc. | None |
283
+ | **MCP server** | Built-in | Varies |
284
+ | **Dependencies** | Zero | Varies |
285
+
218
286
  ## Contributing
219
287
 
220
288
  ```bash
@@ -223,6 +291,7 @@ cd codesight
223
291
  pnpm install
224
292
  pnpm dev # Run locally
225
293
  pnpm build # Compile TypeScript
294
+ pnpm test # Run 27 tests
226
295
  ```
227
296
 
228
297
  PRs welcome. Open an issue first for large changes.
@@ -36,9 +36,16 @@ export async function enrichRouteContracts(routes, project) {
36
36
  case "express":
37
37
  case "fastify":
38
38
  case "koa":
39
+ case "nestjs":
40
+ case "elysia":
41
+ case "adonis":
42
+ case "raw-http":
39
43
  enrichTSRoute(route, content);
40
44
  break;
41
45
  case "next-app":
46
+ case "sveltekit":
47
+ case "remix":
48
+ case "nuxt":
42
49
  enrichNextRoute(route, content);
43
50
  break;
44
51
  case "fastapi":
@@ -3,7 +3,15 @@ import { readFileSafe } from "../scanner.js";
3
3
  export async function detectDependencyGraph(files, project) {
4
4
  const edges = [];
5
5
  const importCount = new Map();
6
- const codeFiles = files.filter((f) => f.match(/\.(ts|tsx|js|jsx|mjs|py|go)$/));
6
+ const codeFiles = files.filter((f) => f.match(/\.(ts|tsx|js|jsx|mjs|py|go|rb|ex|exs|java|kt|rs|php)$/));
7
+ // Build a lookup map for faster resolution: relative path -> true
8
+ const relPathSet = new Set();
9
+ const relPaths = [];
10
+ for (const file of files) {
11
+ const rel = relative(project.root, file);
12
+ relPathSet.add(rel);
13
+ relPaths.push(rel);
14
+ }
7
15
  for (const file of codeFiles) {
8
16
  const content = await readFileSafe(file);
9
17
  if (!content)
@@ -16,8 +24,20 @@ export async function detectDependencyGraph(files, project) {
16
24
  else if (ext === ".go") {
17
25
  extractGoImports(content, rel, edges, importCount);
18
26
  }
27
+ else if (ext === ".rb") {
28
+ extractRubyImports(content, rel, edges, importCount);
29
+ }
30
+ else if (ext === ".ex" || ext === ".exs") {
31
+ extractElixirImports(content, rel, edges, importCount);
32
+ }
33
+ else if (ext === ".java" || ext === ".kt") {
34
+ extractJavaImports(content, rel, edges, importCount, relPaths);
35
+ }
36
+ else if (ext === ".rs") {
37
+ extractRustImports(content, rel, edges, importCount);
38
+ }
19
39
  else {
20
- extractTSImports(content, rel, file, project, files, edges, importCount);
40
+ extractTSImports(content, rel, file, project, relPathSet, edges, importCount);
21
41
  }
22
42
  }
23
43
  // Sort by most imported
@@ -27,7 +47,7 @@ export async function detectDependencyGraph(files, project) {
27
47
  .slice(0, 20);
28
48
  return { edges, hotFiles };
29
49
  }
30
- function extractTSImports(content, rel, absPath, project, allFiles, edges, importCount) {
50
+ function extractTSImports(content, rel, absPath, project, relPathSet, edges, importCount) {
31
51
  // Match: import ... from "./path" or import("./path") or require("./path")
32
52
  const patterns = [
33
53
  /(?:import|export)\s+.*?from\s+['"]([^'"]+)['"]/g,
@@ -50,8 +70,10 @@ function extractTSImports(content, rel, absPath, project, allFiles, edges, impor
50
70
  const dir = dirname(absPath);
51
71
  resolvedPath = relative(project.root, resolve(dir, importPath));
52
72
  }
53
- // Strip extension and try to find the actual file
54
- const normalized = normalizeImportPath(resolvedPath, allFiles, project.root);
73
+ // Strip .js/.mjs extension that TypeScript adds for ESM compatibility
74
+ // e.g., import { foo } from "./bar.js" actually refers to ./bar.ts
75
+ const stripped = resolvedPath.replace(/\.(js|mjs|cjs)$/, "");
76
+ const normalized = normalizeImportPath(stripped, relPathSet);
55
77
  if (normalized && normalized !== rel) {
56
78
  edges.push({ from: rel, to: normalized });
57
79
  importCount.set(normalized, (importCount.get(normalized) || 0) + 1);
@@ -70,11 +92,9 @@ function extractPythonImports(content, rel, edges, importCount) {
70
92
  }
71
93
  }
72
94
  function extractGoImports(content, rel, edges, importCount) {
73
- // Go doesn't have relative imports in the same way, but we can track internal package imports
74
95
  const importBlock = content.match(/import\s*\(([\s\S]*?)\)/);
75
96
  if (!importBlock)
76
97
  return;
77
- // Look for internal package paths (not standard library)
78
98
  const lines = importBlock[1].split("\n");
79
99
  for (const line of lines) {
80
100
  const pathMatch = line.match(/["']([^"']+)["']/);
@@ -85,29 +105,73 @@ function extractGoImports(content, rel, edges, importCount) {
85
105
  }
86
106
  }
87
107
  }
88
- function normalizeImportPath(importPath, allFiles, root) {
89
- // Try exact match first
90
- for (const file of allFiles) {
91
- const rel = relative(root, file);
92
- if (rel === importPath)
93
- return rel;
108
+ function extractRubyImports(content, rel, edges, importCount) {
109
+ // require_relative "./path"
110
+ const pattern = /require_relative\s+['"]([^'"]+)['"]/g;
111
+ let match;
112
+ while ((match = pattern.exec(content)) !== null) {
113
+ const target = match[1].replace(/^\.\//, "") + ".rb";
114
+ edges.push({ from: rel, to: target });
115
+ importCount.set(target, (importCount.get(target) || 0) + 1);
116
+ }
117
+ }
118
+ function extractElixirImports(content, rel, edges, importCount) {
119
+ // alias MyApp.Accounts.User
120
+ const pattern = /(?:alias|import|use)\s+([\w.]+)/g;
121
+ let match;
122
+ while ((match = pattern.exec(content)) !== null) {
123
+ const mod = match[1];
124
+ // Convert module path to potential file: MyApp.Accounts.User -> lib/my_app/accounts/user.ex
125
+ if (mod.includes(".") && !mod.startsWith("Ecto") && !mod.startsWith("Phoenix") && !mod.startsWith("Plug")) {
126
+ const target = "lib/" + mod.split(".").map(s => s.replace(/([A-Z])/g, "_$1").toLowerCase().replace(/^_/, "")).join("/") + ".ex";
127
+ edges.push({ from: rel, to: target });
128
+ importCount.set(target, (importCount.get(target) || 0) + 1);
129
+ }
130
+ }
131
+ }
132
+ function extractJavaImports(content, rel, edges, importCount, relPaths) {
133
+ // import com.myapp.service.UserService;
134
+ const pattern = /^import\s+([\w.]+);/gm;
135
+ let match;
136
+ while ((match = pattern.exec(content)) !== null) {
137
+ const imp = match[1];
138
+ // Skip standard library and common third-party
139
+ if (imp.startsWith("java.") || imp.startsWith("javax.") || imp.startsWith("org.springframework") || imp.startsWith("org.apache"))
140
+ continue;
141
+ // Convert to path pattern: com.myapp.service.UserService -> UserService
142
+ const className = imp.split(".").pop();
143
+ const found = relPaths.find(p => p.endsWith(`/${className}.java`) || p.endsWith(`/${className}.kt`));
144
+ if (found && found !== rel) {
145
+ edges.push({ from: rel, to: found });
146
+ importCount.set(found, (importCount.get(found) || 0) + 1);
147
+ }
94
148
  }
149
+ }
150
+ function extractRustImports(content, rel, edges, importCount) {
151
+ // mod my_module; or use crate::my_module::something;
152
+ const modPattern = /^mod\s+(\w+)\s*;/gm;
153
+ let match;
154
+ while ((match = modPattern.exec(content)) !== null) {
155
+ const dir = dirname(rel);
156
+ const target = dir === "." ? `${match[1]}.rs` : `${dir}/${match[1]}.rs`;
157
+ edges.push({ from: rel, to: target });
158
+ importCount.set(target, (importCount.get(target) || 0) + 1);
159
+ }
160
+ }
161
+ function normalizeImportPath(importPath, relPathSet) {
162
+ // Try exact match first
163
+ if (relPathSet.has(importPath))
164
+ return importPath;
95
165
  // Try with extensions
96
166
  const extensions = [".ts", ".tsx", ".js", ".jsx", ".mjs"];
97
167
  for (const ext of extensions) {
98
- for (const file of allFiles) {
99
- const rel = relative(root, file);
100
- if (rel === importPath + ext)
101
- return rel;
102
- }
168
+ if (relPathSet.has(importPath + ext))
169
+ return importPath + ext;
103
170
  }
104
171
  // Try index files
105
172
  for (const ext of extensions) {
106
- for (const file of allFiles) {
107
- const rel = relative(root, file);
108
- if (rel === importPath + "/index" + ext)
109
- return rel;
110
- }
173
+ if (relPathSet.has(importPath + "/index" + ext))
174
+ return importPath + "/index" + ext;
111
175
  }
112
176
  return null;
113
177
  }
@@ -42,6 +42,27 @@ export async function detectRoutes(files, project) {
42
42
  case "koa":
43
43
  routes.push(...(await detectKoaRoutes(files, project)));
44
44
  break;
45
+ case "nestjs":
46
+ routes.push(...(await detectNestJSRoutes(files, project)));
47
+ break;
48
+ case "elysia":
49
+ routes.push(...(await detectElysiaRoutes(files, project)));
50
+ break;
51
+ case "adonis":
52
+ routes.push(...(await detectAdonisRoutes(files, project)));
53
+ break;
54
+ case "trpc":
55
+ routes.push(...(await detectTRPCRoutes(files, project)));
56
+ break;
57
+ case "sveltekit":
58
+ routes.push(...(await detectSvelteKitRoutes(files, project)));
59
+ break;
60
+ case "remix":
61
+ routes.push(...(await detectRemixRoutes(files, project)));
62
+ break;
63
+ case "nuxt":
64
+ routes.push(...(await detectNuxtRoutes(files, project)));
65
+ break;
45
66
  case "fastapi":
46
67
  routes.push(...(await detectFastAPIRoutes(files, project)));
47
68
  break;
@@ -54,8 +75,26 @@ export async function detectRoutes(files, project) {
54
75
  case "gin":
55
76
  case "go-net-http":
56
77
  case "fiber":
78
+ case "echo":
79
+ case "chi":
57
80
  routes.push(...(await detectGoRoutes(files, project, fw)));
58
81
  break;
82
+ case "rails":
83
+ routes.push(...(await detectRailsRoutes(files, project)));
84
+ break;
85
+ case "phoenix":
86
+ routes.push(...(await detectPhoenixRoutes(files, project)));
87
+ break;
88
+ case "spring":
89
+ routes.push(...(await detectSpringRoutes(files, project)));
90
+ break;
91
+ case "actix":
92
+ case "axum":
93
+ routes.push(...(await detectRustRoutes(files, project, fw)));
94
+ break;
95
+ case "raw-http":
96
+ routes.push(...(await detectRawHttpRoutes(files, project)));
97
+ break;
59
98
  }
60
99
  }
61
100
  return routes;
@@ -67,7 +106,6 @@ async function detectNextAppRoutes(files, project) {
67
106
  for (const file of routeFiles) {
68
107
  const content = await readFileSafe(file);
69
108
  const rel = relative(project.root, file);
70
- // Extract API path from file path
71
109
  const pathMatch = rel.match(/(?:src\/)?app(.*)\/route\./);
72
110
  const apiPath = pathMatch ? pathMatch[1] || "/" : "/";
73
111
  for (const method of HTTP_METHODS) {
@@ -95,7 +133,6 @@ async function detectNextPagesApi(files, project) {
95
133
  const pathMatch = rel.match(/(?:src\/)?pages(\/api\/.*)\.(?:ts|js|tsx|jsx)$/);
96
134
  let apiPath = pathMatch ? pathMatch[1] : "/api";
97
135
  apiPath = apiPath.replace(/\/index$/, "").replace(/\[([^\]]+)\]/g, ":$1");
98
- // Detect methods from handler
99
136
  const methods = [];
100
137
  for (const method of HTTP_METHODS) {
101
138
  if (content.includes(`req.method === '${method}'`) || content.includes(`req.method === "${method}"`)) {
@@ -125,12 +162,10 @@ async function detectHonoRoutes(files, project) {
125
162
  if (!content.includes("hono") && !content.includes("Hono"))
126
163
  continue;
127
164
  const rel = relative(project.root, file);
128
- // Match: app.get("/path", ...), router.post("/path", ...), .route("/base", ...)
129
165
  const routePattern = /\.\s*(get|post|put|patch|delete|options|all)\s*\(\s*['"`]([^'"`]+)['"`]/gi;
130
166
  let match;
131
167
  while ((match = routePattern.exec(content)) !== null) {
132
168
  const path = match[2];
133
- // Skip non-path strings (middleware keys like "user", "userId", etc.)
134
169
  if (!path.startsWith("/") && !path.startsWith(":"))
135
170
  continue;
136
171
  routes.push({
@@ -153,7 +188,6 @@ async function detectExpressRoutes(files, project) {
153
188
  if (!content.includes("express") && !content.includes("Router"))
154
189
  continue;
155
190
  const rel = relative(project.root, file);
156
- // Match: app.get("/path", ...), router.post("/path", ...)
157
191
  const routePattern = /(?:app|router|server)\s*\.\s*(get|post|put|patch|delete|options|all)\s*\(\s*['"`]([^'"`]+)['"`]/gi;
158
192
  let match;
159
193
  while ((match = routePattern.exec(content)) !== null) {
@@ -177,7 +211,6 @@ async function detectFastifyRoutes(files, project) {
177
211
  if (!content.includes("fastify"))
178
212
  continue;
179
213
  const rel = relative(project.root, file);
180
- // Match: fastify.get("/path", ...) or server.route({ method: 'GET', url: '/path' })
181
214
  const routePattern = /(?:fastify|server|app)\s*\.\s*(get|post|put|patch|delete|options|all)\s*\(\s*['"`]([^'"`]+)['"`]/gi;
182
215
  let match;
183
216
  while ((match = routePattern.exec(content)) !== null) {
@@ -226,6 +259,233 @@ async function detectKoaRoutes(files, project) {
226
259
  }
227
260
  return routes;
228
261
  }
262
+ // --- NestJS ---
263
+ async function detectNestJSRoutes(files, project) {
264
+ const tsFiles = files.filter((f) => f.match(/\.(ts|js)$/));
265
+ const routes = [];
266
+ for (const file of tsFiles) {
267
+ const content = await readFileSafe(file);
268
+ if (!content.includes("@Controller") && !content.includes("@Get") && !content.includes("@Post"))
269
+ continue;
270
+ const rel = relative(project.root, file);
271
+ // Extract controller base path: @Controller('users') or @Controller('/users')
272
+ const controllerMatch = content.match(/@Controller\s*\(\s*['"`]([^'"`]*)['"`]\s*\)/);
273
+ const basePath = controllerMatch ? "/" + controllerMatch[1].replace(/^\//, "") : "";
274
+ // Match method decorators: @Get(), @Post('/create'), @Put(':id'), etc.
275
+ const decoratorPattern = /@(Get|Post|Put|Patch|Delete|Options|Head|All)\s*\(\s*(?:['"`]([^'"`]*)['"`])?\s*\)/gi;
276
+ let match;
277
+ while ((match = decoratorPattern.exec(content)) !== null) {
278
+ const method = match[1].toUpperCase();
279
+ const subPath = match[2] || "";
280
+ const fullPath = basePath + (subPath ? "/" + subPath.replace(/^\//, "") : "") || "/";
281
+ routes.push({
282
+ method,
283
+ path: fullPath,
284
+ file: rel,
285
+ tags: detectTags(content),
286
+ framework: "nestjs",
287
+ });
288
+ }
289
+ }
290
+ return routes;
291
+ }
292
+ // --- Elysia (Bun) ---
293
+ async function detectElysiaRoutes(files, project) {
294
+ const tsFiles = files.filter((f) => f.match(/\.(ts|js|mjs)$/));
295
+ const routes = [];
296
+ for (const file of tsFiles) {
297
+ const content = await readFileSafe(file);
298
+ if (!content.includes("elysia") && !content.includes("Elysia"))
299
+ continue;
300
+ const rel = relative(project.root, file);
301
+ const routePattern = /\.\s*(get|post|put|patch|delete|options|all)\s*\(\s*['"`]([^'"`]+)['"`]/gi;
302
+ let match;
303
+ while ((match = routePattern.exec(content)) !== null) {
304
+ const path = match[2];
305
+ if (!path.startsWith("/") && !path.startsWith(":"))
306
+ continue;
307
+ routes.push({
308
+ method: match[1].toUpperCase(),
309
+ path,
310
+ file: rel,
311
+ tags: detectTags(content),
312
+ framework: "elysia",
313
+ });
314
+ }
315
+ }
316
+ return routes;
317
+ }
318
+ // --- AdonisJS ---
319
+ async function detectAdonisRoutes(files, project) {
320
+ // AdonisJS uses start/routes.ts with Route.get(), Route.post(), router.get(), etc.
321
+ const routeFiles = files.filter((f) => f.match(/routes\.(ts|js)$/) || f.match(/\/routes\/.*\.(ts|js)$/));
322
+ const routes = [];
323
+ for (const file of routeFiles) {
324
+ const content = await readFileSafe(file);
325
+ const rel = relative(project.root, file);
326
+ const routePattern = /(?:Route|router)\s*\.\s*(get|post|put|patch|delete|any)\s*\(\s*['"`]([^'"`]+)['"`]/gi;
327
+ let match;
328
+ while ((match = routePattern.exec(content)) !== null) {
329
+ routes.push({
330
+ method: match[1].toUpperCase() === "ANY" ? "ALL" : match[1].toUpperCase(),
331
+ path: match[2],
332
+ file: rel,
333
+ tags: detectTags(content),
334
+ framework: "adonis",
335
+ });
336
+ }
337
+ }
338
+ return routes;
339
+ }
340
+ // --- tRPC ---
341
+ async function detectTRPCRoutes(files, project) {
342
+ const tsFiles = files.filter((f) => f.match(/\.(ts|js)$/));
343
+ const routes = [];
344
+ for (const file of tsFiles) {
345
+ const content = await readFileSafe(file);
346
+ if (!content.includes("Procedure") && !content.includes("procedure") && !content.includes("router"))
347
+ continue;
348
+ if (!content.includes("trpc") && !content.includes("TRPC") && !content.includes("createTRPCRouter") && !content.includes("publicProcedure") && !content.includes("protectedProcedure"))
349
+ continue;
350
+ const rel = relative(project.root, file);
351
+ // Match tRPC procedure definitions like:
352
+ // list: publicProcedure.query(...)
353
+ // create: publicProcedure.input(schema).mutation(...)
354
+ // getById: t.procedure.input(z.object({...})).query(...)
355
+ // Strategy: find lines with "query(" or "mutation(" and extract the procedure name
356
+ const lines = content.split("\n");
357
+ for (const line of lines) {
358
+ const queryMatch = line.match(/^\s*(\w+)\s*:\s*.*\.(query)\s*\(/);
359
+ const mutationMatch = line.match(/^\s*(\w+)\s*:\s*.*\.(mutation)\s*\(/);
360
+ const m = queryMatch || mutationMatch;
361
+ if (m) {
362
+ const procName = m[1];
363
+ const isQuery = m[2] === "query";
364
+ if (!routes.some((r) => r.path === procName && r.file === rel)) {
365
+ routes.push({
366
+ method: isQuery ? "QUERY" : "MUTATION",
367
+ path: procName,
368
+ file: rel,
369
+ tags: detectTags(content),
370
+ framework: "trpc",
371
+ });
372
+ }
373
+ }
374
+ }
375
+ }
376
+ return routes;
377
+ }
378
+ // --- SvelteKit ---
379
+ async function detectSvelteKitRoutes(files, project) {
380
+ // SvelteKit API routes: src/routes/**/+server.ts
381
+ const routeFiles = files.filter((f) => f.match(/\/routes\/.*\+server\.(ts|js)$/));
382
+ const routes = [];
383
+ for (const file of routeFiles) {
384
+ const content = await readFileSafe(file);
385
+ const rel = relative(project.root, file);
386
+ // Extract path from file structure: src/routes/api/users/+server.ts -> /api/users
387
+ const pathMatch = rel.match(/(?:src\/)?routes(.*)\/\+server\./);
388
+ let apiPath = pathMatch ? pathMatch[1] || "/" : "/";
389
+ // Convert [param] to :param
390
+ apiPath = apiPath.replace(/\[([^\]]+)\]/g, ":$1");
391
+ for (const method of HTTP_METHODS) {
392
+ const pattern = new RegExp(`export\\s+(?:async\\s+)?function\\s+${method}\\b`);
393
+ if (pattern.test(content)) {
394
+ routes.push({
395
+ method,
396
+ path: apiPath,
397
+ file: rel,
398
+ tags: detectTags(content),
399
+ framework: "sveltekit",
400
+ });
401
+ }
402
+ }
403
+ // Also detect: export const GET = ...
404
+ for (const method of HTTP_METHODS) {
405
+ const constPattern = new RegExp(`export\\s+const\\s+${method}\\s*[=:]`);
406
+ if (constPattern.test(content) && !routes.some((r) => r.method === method && r.path === apiPath)) {
407
+ routes.push({
408
+ method,
409
+ path: apiPath,
410
+ file: rel,
411
+ tags: detectTags(content),
412
+ framework: "sveltekit",
413
+ });
414
+ }
415
+ }
416
+ }
417
+ return routes;
418
+ }
419
+ // --- Remix ---
420
+ async function detectRemixRoutes(files, project) {
421
+ // Remix routes: app/routes/*.tsx with loader/action exports
422
+ const routeFiles = files.filter((f) => f.match(/\/routes\/.*\.(ts|tsx|js|jsx)$/));
423
+ const routes = [];
424
+ for (const file of routeFiles) {
425
+ const content = await readFileSafe(file);
426
+ const rel = relative(project.root, file);
427
+ // Convert filename to route path
428
+ const pathMatch = rel.match(/(?:app\/)?routes\/(.+)\.(ts|tsx|js|jsx)$/);
429
+ if (!pathMatch)
430
+ continue;
431
+ let routePath = "/" + pathMatch[1]
432
+ .replace(/\./g, "/") // dots become path segments
433
+ .replace(/_index$/, "") // _index -> root of parent
434
+ .replace(/\$/g, ":") // $param -> :param
435
+ .replace(/\[([^\]]+)\]/g, ":$1");
436
+ if (content.match(/export\s+(?:async\s+)?function\s+loader\b/) || content.match(/export\s+const\s+loader\b/)) {
437
+ routes.push({
438
+ method: "GET",
439
+ path: routePath,
440
+ file: rel,
441
+ tags: detectTags(content),
442
+ framework: "remix",
443
+ });
444
+ }
445
+ if (content.match(/export\s+(?:async\s+)?function\s+action\b/) || content.match(/export\s+const\s+action\b/)) {
446
+ routes.push({
447
+ method: "POST",
448
+ path: routePath,
449
+ file: rel,
450
+ tags: detectTags(content),
451
+ framework: "remix",
452
+ });
453
+ }
454
+ }
455
+ return routes;
456
+ }
457
+ // --- Nuxt ---
458
+ async function detectNuxtRoutes(files, project) {
459
+ // Nuxt server routes: server/api/**/*.ts
460
+ const routeFiles = files.filter((f) => f.match(/\/server\/(?:api|routes)\/.*\.(ts|js|mjs)$/));
461
+ const routes = [];
462
+ for (const file of routeFiles) {
463
+ const content = await readFileSafe(file);
464
+ const rel = relative(project.root, file);
465
+ // Extract path from file structure
466
+ const pathMatch = rel.match(/server\/((?:api|routes)\/.+)\.(ts|js|mjs)$/);
467
+ if (!pathMatch)
468
+ continue;
469
+ let routePath = "/" + pathMatch[1]
470
+ .replace(/\/index$/, "")
471
+ .replace(/\[([^\]]+)\]/g, ":$1");
472
+ // Detect method from filename (e.g., users.get.ts, users.post.ts)
473
+ const methodFromFile = basename(file).match(/\.(get|post|put|patch|delete)\.(ts|js|mjs)$/);
474
+ const method = methodFromFile ? methodFromFile[1].toUpperCase() : "ALL";
475
+ // Clean path: remove method suffix from path
476
+ if (methodFromFile) {
477
+ routePath = routePath.replace(new RegExp(`\\.${methodFromFile[1]}$`), "");
478
+ }
479
+ routes.push({
480
+ method,
481
+ path: routePath,
482
+ file: rel,
483
+ tags: detectTags(content),
484
+ framework: "nuxt",
485
+ });
486
+ }
487
+ return routes;
488
+ }
229
489
  // --- FastAPI ---
230
490
  async function detectFastAPIRoutes(files, project) {
231
491
  const pyFiles = files.filter((f) => f.endsWith(".py"));
@@ -235,7 +495,6 @@ async function detectFastAPIRoutes(files, project) {
235
495
  if (!content.includes("fastapi") && !content.includes("FastAPI") && !content.includes("APIRouter"))
236
496
  continue;
237
497
  const rel = relative(project.root, file);
238
- // Match: @app.get("/path") or @router.post("/path") or @api_router.get("/path")
239
498
  const routePattern = /@\w+\s*\.\s*(get|post|put|patch|delete|options)\s*\(\s*['"]([^'"]+)['"]/gi;
240
499
  let match;
241
500
  while ((match = routePattern.exec(content)) !== null) {
@@ -259,8 +518,7 @@ async function detectFlaskRoutes(files, project) {
259
518
  if (!content.includes("flask") && !content.includes("Flask") && !content.includes("Blueprint"))
260
519
  continue;
261
520
  const rel = relative(project.root, file);
262
- // Match: @app.route("/path", methods=["GET", "POST"])
263
- const routePattern = /@(?:app|bp|blueprint)\s*\.\s*route\s*\(\s*['"]([^'"]+)['"](?:\s*,\s*methods\s*=\s*\[([^\]]+)\])?\s*\)/gi;
521
+ const routePattern = /@(?:app|bp|blueprint|\w+)\s*\.\s*route\s*\(\s*['"]([^'"]+)['"](?:\s*,\s*methods\s*=\s*\[([^\]]+)\])?\s*\)/gi;
264
522
  let match;
265
523
  while ((match = routePattern.exec(content)) !== null) {
266
524
  const path = match[1];
@@ -287,7 +545,7 @@ async function detectDjangoRoutes(files, project) {
287
545
  for (const file of pyFiles) {
288
546
  const content = await readFileSafe(file);
289
547
  const rel = relative(project.root, file);
290
- // Match: path("api/v1/users/", views.UserView.as_view())
548
+ // path("api/v1/users/", views.UserView.as_view())
291
549
  const pathPattern = /path\s*\(\s*['"]([^'"]*)['"]\s*,/g;
292
550
  let match;
293
551
  while ((match = pathPattern.exec(content)) !== null) {
@@ -302,7 +560,7 @@ async function detectDjangoRoutes(files, project) {
302
560
  }
303
561
  return routes;
304
562
  }
305
- // --- Go ---
563
+ // --- Go (net/http, Gin, Fiber, Echo, Chi) ---
306
564
  async function detectGoRoutes(files, project, fw) {
307
565
  const goFiles = files.filter((f) => f.endsWith(".go"));
308
566
  const routes = [];
@@ -310,7 +568,6 @@ async function detectGoRoutes(files, project, fw) {
310
568
  const content = await readFileSafe(file);
311
569
  const rel = relative(project.root, file);
312
570
  if (fw === "gin") {
313
- // Match: r.GET("/path", handler)
314
571
  const pattern = /\.\s*(GET|POST|PUT|PATCH|DELETE|OPTIONS|HEAD)\s*\(\s*["']([^"']+)["']/g;
315
572
  let match;
316
573
  while ((match = pattern.exec(content)) !== null) {
@@ -324,7 +581,34 @@ async function detectGoRoutes(files, project, fw) {
324
581
  }
325
582
  }
326
583
  else if (fw === "fiber") {
327
- // Match: app.Get("/path", handler)
584
+ const pattern = /\.\s*(Get|Post|Put|Patch|Delete|Options|Head)\s*\(\s*["']([^"']+)["']/g;
585
+ let match;
586
+ while ((match = pattern.exec(content)) !== null) {
587
+ routes.push({
588
+ method: match[1].toUpperCase(),
589
+ path: match[2],
590
+ file: rel,
591
+ tags: detectTags(content),
592
+ framework: fw,
593
+ });
594
+ }
595
+ }
596
+ else if (fw === "echo") {
597
+ // e.GET("/path", handler) or g.POST("/path", handler)
598
+ const pattern = /\.\s*(GET|POST|PUT|PATCH|DELETE|OPTIONS|HEAD)\s*\(\s*["']([^"']+)["']/g;
599
+ let match;
600
+ while ((match = pattern.exec(content)) !== null) {
601
+ routes.push({
602
+ method: match[1],
603
+ path: match[2],
604
+ file: rel,
605
+ tags: detectTags(content),
606
+ framework: fw,
607
+ });
608
+ }
609
+ }
610
+ else if (fw === "chi") {
611
+ // r.Get("/path", handler), r.Post("/path", handler)
328
612
  const pattern = /\.\s*(Get|Post|Put|Patch|Delete|Options|Head)\s*\(\s*["']([^"']+)["']/g;
329
613
  let match;
330
614
  while ((match = pattern.exec(content)) !== null) {
@@ -354,3 +638,225 @@ async function detectGoRoutes(files, project, fw) {
354
638
  }
355
639
  return routes;
356
640
  }
641
+ // --- Rails ---
642
+ async function detectRailsRoutes(files, project) {
643
+ const routeFiles = files.filter((f) => f.match(/routes\.rb$/));
644
+ const routes = [];
645
+ for (const file of routeFiles) {
646
+ const content = await readFileSafe(file);
647
+ const rel = relative(project.root, file);
648
+ // get '/users', to: 'users#index'
649
+ const routePattern = /\b(get|post|put|patch|delete)\s+['"]([^'"]+)['"]/gi;
650
+ let match;
651
+ while ((match = routePattern.exec(content)) !== null) {
652
+ routes.push({
653
+ method: match[1].toUpperCase(),
654
+ path: match[2],
655
+ file: rel,
656
+ tags: detectTags(content),
657
+ framework: "rails",
658
+ });
659
+ }
660
+ // resources :users (generates RESTful routes)
661
+ const resourcePattern = /resources?\s+:(\w+)/g;
662
+ while ((match = resourcePattern.exec(content)) !== null) {
663
+ const name = match[1];
664
+ for (const [method, suffix] of [
665
+ ["GET", ""], ["GET", "/:id"], ["POST", ""],
666
+ ["PUT", "/:id"], ["PATCH", "/:id"], ["DELETE", "/:id"],
667
+ ]) {
668
+ routes.push({
669
+ method,
670
+ path: `/${name}${suffix}`,
671
+ file: rel,
672
+ tags: detectTags(content),
673
+ framework: "rails",
674
+ });
675
+ }
676
+ }
677
+ }
678
+ return routes;
679
+ }
680
+ // --- Phoenix (Elixir) ---
681
+ async function detectPhoenixRoutes(files, project) {
682
+ const routeFiles = files.filter((f) => f.match(/router\.ex$/));
683
+ const routes = [];
684
+ for (const file of routeFiles) {
685
+ const content = await readFileSafe(file);
686
+ const rel = relative(project.root, file);
687
+ // get "/users", UserController, :index
688
+ const routePattern = /\b(get|post|put|patch|delete)\s+["']([^"']+)["']/gi;
689
+ let match;
690
+ while ((match = routePattern.exec(content)) !== null) {
691
+ routes.push({
692
+ method: match[1].toUpperCase(),
693
+ path: match[2],
694
+ file: rel,
695
+ tags: detectTags(content),
696
+ framework: "phoenix",
697
+ });
698
+ }
699
+ // resources "/users", UserController
700
+ const resourcePattern = /resources\s+["']([^"']+)["']/g;
701
+ while ((match = resourcePattern.exec(content)) !== null) {
702
+ const basePath = match[1];
703
+ for (const [method, suffix] of [
704
+ ["GET", ""], ["GET", "/:id"], ["POST", ""],
705
+ ["PUT", "/:id"], ["PATCH", "/:id"], ["DELETE", "/:id"],
706
+ ]) {
707
+ routes.push({
708
+ method,
709
+ path: `${basePath}${suffix}`,
710
+ file: rel,
711
+ tags: detectTags(content),
712
+ framework: "phoenix",
713
+ });
714
+ }
715
+ }
716
+ }
717
+ return routes;
718
+ }
719
+ // --- Spring Boot (Java/Kotlin) ---
720
+ async function detectSpringRoutes(files, project) {
721
+ const javaFiles = files.filter((f) => f.match(/\.(java|kt)$/));
722
+ const routes = [];
723
+ for (const file of javaFiles) {
724
+ const content = await readFileSafe(file);
725
+ if (!content.includes("@RestController") && !content.includes("@Controller") && !content.includes("@RequestMapping"))
726
+ continue;
727
+ const rel = relative(project.root, file);
728
+ // Extract class-level @RequestMapping
729
+ const classMapping = content.match(/@RequestMapping\s*\(\s*(?:value\s*=\s*)?["']([^"']+)["']/);
730
+ const basePath = classMapping ? classMapping[1] : "";
731
+ // @GetMapping("/path"), @PostMapping("/path"), etc.
732
+ const mappingPattern = /@(Get|Post|Put|Patch|Delete)Mapping\s*\(\s*(?:value\s*=\s*)?(?:["']([^"']*)["'])?\s*\)/gi;
733
+ let match;
734
+ while ((match = mappingPattern.exec(content)) !== null) {
735
+ const method = match[1].toUpperCase();
736
+ const subPath = match[2] || "";
737
+ routes.push({
738
+ method,
739
+ path: basePath + subPath || "/",
740
+ file: rel,
741
+ tags: detectTags(content),
742
+ framework: "spring",
743
+ });
744
+ }
745
+ // @RequestMapping(method = RequestMethod.GET, value = "/path")
746
+ const reqMappingPattern = /@RequestMapping\s*\([^)]*method\s*=\s*RequestMethod\.(\w+)[^)]*value\s*=\s*["']([^"']+)["']/gi;
747
+ while ((match = reqMappingPattern.exec(content)) !== null) {
748
+ routes.push({
749
+ method: match[1].toUpperCase(),
750
+ path: basePath + match[2],
751
+ file: rel,
752
+ tags: detectTags(content),
753
+ framework: "spring",
754
+ });
755
+ }
756
+ }
757
+ return routes;
758
+ }
759
+ // --- Rust (Actix-web, Axum) ---
760
+ async function detectRustRoutes(files, project, fw) {
761
+ const rsFiles = files.filter((f) => f.endsWith(".rs"));
762
+ const routes = [];
763
+ for (const file of rsFiles) {
764
+ const content = await readFileSafe(file);
765
+ const rel = relative(project.root, file);
766
+ if (fw === "actix") {
767
+ // #[get("/path")], #[post("/path")], etc.
768
+ const attrPattern = /#\[(get|post|put|patch|delete)\s*\(\s*"([^"]+)"\s*\)\s*\]/gi;
769
+ let match;
770
+ while ((match = attrPattern.exec(content)) !== null) {
771
+ routes.push({
772
+ method: match[1].toUpperCase(),
773
+ path: match[2],
774
+ file: rel,
775
+ tags: detectTags(content),
776
+ framework: "actix",
777
+ });
778
+ }
779
+ // .route("/path", web::get().to(handler))
780
+ const routePattern = /\.route\s*\(\s*"([^"]+)"\s*,\s*web::(get|post|put|patch|delete)\s*\(\s*\)/gi;
781
+ while ((match = routePattern.exec(content)) !== null) {
782
+ routes.push({
783
+ method: match[2].toUpperCase(),
784
+ path: match[1],
785
+ file: rel,
786
+ tags: detectTags(content),
787
+ framework: "actix",
788
+ });
789
+ }
790
+ }
791
+ else if (fw === "axum") {
792
+ // .route("/path", get(handler)) or .route("/path", post(handler).get(handler))
793
+ const routePattern = /\.route\s*\(\s*"([^"]+)"\s*,\s*(get|post|put|patch|delete)\s*\(/gi;
794
+ let match;
795
+ while ((match = routePattern.exec(content)) !== null) {
796
+ routes.push({
797
+ method: match[2].toUpperCase(),
798
+ path: match[1],
799
+ file: rel,
800
+ tags: detectTags(content),
801
+ framework: "axum",
802
+ });
803
+ }
804
+ }
805
+ }
806
+ return routes;
807
+ }
808
+ // --- Raw HTTP (Node.js http.createServer, Deno, Bun.serve) ---
809
+ async function detectRawHttpRoutes(files, project) {
810
+ const tsFiles = files.filter((f) => f.match(/\.(ts|js|mjs|cjs)$/));
811
+ const routes = [];
812
+ const globalSeen = new Set();
813
+ for (const file of tsFiles) {
814
+ const content = await readFileSafe(file);
815
+ // Only scan files that handle HTTP requests
816
+ if (!content.match(/(?:createServer|http\.|req\.|request\.|url|pathname|Bun\.serve|Deno\.serve)/))
817
+ continue;
818
+ const rel = relative(project.root, file);
819
+ const patterns = [
820
+ // Direct comparison: url === "/path" or pathname === "/path"
821
+ /(?:url|pathname|parsedUrl\.pathname)\s*===?\s*['"`](\/[a-zA-Z0-9/_:.\-]+)['"`]/g,
822
+ // startsWith: url.startsWith("/api")
823
+ /(?:url|pathname)\s*\.startsWith\s*\(\s*['"`](\/[a-zA-Z0-9/_:.\-]+)['"`]\s*\)/g,
824
+ // Switch case: case "/path":
825
+ /case\s+['"`](\/[a-zA-Z0-9/_:.\-]+)['"`]\s*:/g,
826
+ ];
827
+ const fileTags = detectTags(content);
828
+ for (const pattern of patterns) {
829
+ let match;
830
+ while ((match = pattern.exec(content)) !== null) {
831
+ const path = match[1];
832
+ // Skip paths that are clearly not routes
833
+ if (path.includes("\\") || path.length > 100 || path.includes(".."))
834
+ continue;
835
+ // Skip file extensions
836
+ if (path.match(/\.\w{2,4}$/))
837
+ continue;
838
+ const key = `${rel}:${path}`;
839
+ if (globalSeen.has(key))
840
+ continue;
841
+ globalSeen.add(key);
842
+ // Try to detect method from surrounding context (within 300 chars)
843
+ const surroundingStart = Math.max(0, match.index - 300);
844
+ const surroundingEnd = Math.min(content.length, match.index + 300);
845
+ const surrounding = content.substring(surroundingStart, surroundingEnd);
846
+ let method = "ALL";
847
+ const methodMatch = surrounding.match(/method\s*===?\s*['"`](GET|POST|PUT|PATCH|DELETE)['"`]/i);
848
+ if (methodMatch) {
849
+ method = methodMatch[1].toUpperCase();
850
+ }
851
+ routes.push({
852
+ method,
853
+ path,
854
+ file: rel,
855
+ tags: fileTags,
856
+ framework: "raw-http",
857
+ });
858
+ }
859
+ }
860
+ }
861
+ return routes;
862
+ }
package/dist/formatter.js CHANGED
@@ -253,14 +253,27 @@ function formatCombined(result, sections) {
253
253
  }
254
254
  function filterNotableDeps(deps) {
255
255
  const notable = new Set([
256
+ // Frameworks
256
257
  "next", "react", "vue", "svelte", "hono", "express", "fastify", "koa",
257
- "drizzle-orm", "prisma", "@prisma/client", "typeorm", "tailwindcss",
258
- "stripe", "@polar-sh/sdk", "resend", "bullmq", "redis", "ioredis",
259
- "zod", "trpc", "@trpc/server", "better-auth", "@clerk/nextjs",
260
- "next-auth", "lucia", "passport", "@anthropic-ai/sdk", "openai",
261
- "ai", "langchain", "supabase", "@supabase/supabase-js", "mongoose",
262
- "pg", "mysql2", "better-sqlite3", "playwright", "puppeteer",
263
- "socket.io", "graphql", "@apollo/server",
258
+ "@nestjs/core", "@nestjs/common", "elysia", "@adonisjs/core",
259
+ "@sveltejs/kit", "@remix-run/node", "@remix-run/react", "nuxt",
260
+ // ORMs & DB
261
+ "drizzle-orm", "prisma", "@prisma/client", "typeorm", "mongoose", "sequelize",
262
+ "pg", "mysql2", "better-sqlite3", "knex",
263
+ // Auth
264
+ "better-auth", "@clerk/nextjs", "next-auth", "lucia", "passport", "@auth/core",
265
+ // Payments
266
+ "stripe", "@polar-sh/sdk", "resend", "@lemonsqueezy/lemonsqueezy.js",
267
+ // Infrastructure
268
+ "bullmq", "redis", "ioredis", "tailwindcss",
269
+ // API
270
+ "zod", "@trpc/server", "graphql", "@apollo/server",
271
+ // AI
272
+ "@anthropic-ai/sdk", "openai", "ai", "langchain", "@google/generative-ai",
273
+ // Services
274
+ "supabase", "@supabase/supabase-js", "firebase", "@firebase/app",
275
+ // Testing/tools
276
+ "playwright", "puppeteer", "socket.io",
264
277
  ]);
265
278
  return Object.entries(deps)
266
279
  .filter(([name]) => notable.has(name))
package/dist/index.js CHANGED
@@ -14,7 +14,7 @@ import { calculateTokenStats } from "./detectors/tokens.js";
14
14
  import { writeOutput } from "./formatter.js";
15
15
  import { generateAIConfigs } from "./generators/ai-config.js";
16
16
  import { generateHtmlReport } from "./generators/html-report.js";
17
- const VERSION = "1.0.1";
17
+ const VERSION = "1.1.1";
18
18
  const BRAND = "codesight";
19
19
  function printHelp() {
20
20
  console.log(`
@@ -32,6 +32,7 @@ function printHelp() {
32
32
  --open Generate HTML report and open in browser
33
33
  --mcp Start as MCP server (for Claude Code, Cursor)
34
34
  --json Output JSON instead of markdown
35
+ --benchmark Show detailed token savings breakdown
35
36
  -v, --version Show version
36
37
  -h, --help Show this help
37
38
 
@@ -218,6 +219,7 @@ async function main() {
218
219
  let doHtml = false;
219
220
  let doOpen = false;
220
221
  let doMcp = false;
222
+ let doBenchmark = false;
221
223
  for (let i = 0; i < args.length; i++) {
222
224
  const arg = args[i];
223
225
  if ((arg === "-o" || arg === "--output") && args[i + 1]) {
@@ -248,6 +250,9 @@ async function main() {
248
250
  else if (arg === "--mcp") {
249
251
  doMcp = true;
250
252
  }
253
+ else if (arg === "--benchmark") {
254
+ doBenchmark = true;
255
+ }
251
256
  else if (!arg.startsWith("-")) {
252
257
  targetDir = resolve(arg);
253
258
  }
@@ -293,6 +298,37 @@ async function main() {
293
298
  console.log(" Opening in browser...");
294
299
  }
295
300
  }
301
+ // Benchmark output
302
+ if (doBenchmark) {
303
+ const ts = result.tokenStats;
304
+ const r = result;
305
+ console.log(`
306
+ Token Savings Breakdown:
307
+ ┌──────────────────────────────────────────────────┐
308
+ │ What codesight found │ Exploration cost │
309
+ ├──────────────────────────────┼────────────────────┤
310
+ │ ${String(r.routes.length).padStart(3)} routes │ ~${(r.routes.length * 400).toLocaleString().padStart(6)} tokens │
311
+ │ ${String(r.schemas.length).padStart(3)} schema models │ ~${(r.schemas.length * 300).toLocaleString().padStart(6)} tokens │
312
+ │ ${String(r.components.length).padStart(3)} components │ ~${(r.components.length * 250).toLocaleString().padStart(6)} tokens │
313
+ │ ${String(r.libs.length).padStart(3)} library files │ ~${(r.libs.length * 200).toLocaleString().padStart(6)} tokens │
314
+ │ ${String(r.config.envVars.length).padStart(3)} env vars │ ~${(r.config.envVars.length * 100).toLocaleString().padStart(6)} tokens │
315
+ │ ${String(r.middleware.length).padStart(3)} middleware │ ~${(r.middleware.length * 200).toLocaleString().padStart(6)} tokens │
316
+ │ ${String(r.graph.hotFiles.length).padStart(3)} hot files │ ~${(r.graph.hotFiles.length * 150).toLocaleString().padStart(6)} tokens │
317
+ │ ${String(ts.fileCount).padStart(3)} files (search overhead) │ ~${(Math.min(ts.fileCount, 50) * 80).toLocaleString().padStart(6)} tokens │
318
+ ├──────────────────────────────┼────────────────────┤
319
+ │ codesight output │ ~${ts.outputTokens.toLocaleString().padStart(6)} tokens │
320
+ │ Manual exploration (1.3x) │ ~${ts.estimatedExplorationTokens.toLocaleString().padStart(6)} tokens │
321
+ │ SAVED PER CONVERSATION │ ~${ts.saved.toLocaleString().padStart(6)} tokens │
322
+ └──────────────────────────────┴────────────────────┘
323
+
324
+ How this is calculated:
325
+ - Each route found saves ~400 tokens of file reading + grep exploration
326
+ - Each schema model saves ~300 tokens of migration/ORM file parsing
327
+ - Each component saves ~250 tokens of prop discovery
328
+ - Search overhead: AI typically runs ${Math.min(ts.fileCount, 50)} glob/grep operations
329
+ - 1.3x multiplier: AI revisits files during multi-turn exploration
330
+ `);
331
+ }
296
332
  // Watch mode (blocks)
297
333
  if (doWatch) {
298
334
  await watchMode(root, outputDirName, maxDepth);
package/dist/scanner.js CHANGED
@@ -35,6 +35,13 @@ const CODE_EXTENSIONS = new Set([
35
35
  ".go",
36
36
  ".vue",
37
37
  ".svelte",
38
+ ".rb",
39
+ ".ex",
40
+ ".exs",
41
+ ".java",
42
+ ".kt",
43
+ ".rs",
44
+ ".php",
38
45
  ]);
39
46
  export async function collectFiles(root, maxDepth = 10) {
40
47
  const files = [];
@@ -174,6 +181,27 @@ async function detectFrameworks(root, pkg) {
174
181
  // Koa
175
182
  if (deps["koa"])
176
183
  frameworks.push("koa");
184
+ // NestJS
185
+ if (deps["@nestjs/core"] || deps["@nestjs/common"])
186
+ frameworks.push("nestjs");
187
+ // Elysia (Bun)
188
+ if (deps["elysia"])
189
+ frameworks.push("elysia");
190
+ // AdonisJS
191
+ if (deps["@adonisjs/core"])
192
+ frameworks.push("adonis");
193
+ // tRPC
194
+ if (deps["@trpc/server"])
195
+ frameworks.push("trpc");
196
+ // SvelteKit
197
+ if (deps["@sveltejs/kit"])
198
+ frameworks.push("sveltekit");
199
+ // Remix
200
+ if (deps["@remix-run/node"] || deps["@remix-run/react"])
201
+ frameworks.push("remix");
202
+ // Nuxt
203
+ if (deps["nuxt"])
204
+ frameworks.push("nuxt");
177
205
  // Python frameworks - check for requirements.txt or pyproject.toml
178
206
  const pyDeps = await getPythonDeps(root);
179
207
  if (pyDeps.includes("flask"))
@@ -190,6 +218,59 @@ async function detectFrameworks(root, pkg) {
190
218
  frameworks.push("gin");
191
219
  if (goDeps.includes("gofiber/fiber"))
192
220
  frameworks.push("fiber");
221
+ if (goDeps.some((d) => d.includes("labstack/echo")))
222
+ frameworks.push("echo");
223
+ if (goDeps.some((d) => d.includes("go-chi/chi")))
224
+ frameworks.push("chi");
225
+ // Ruby on Rails
226
+ const hasGemfile = await fileExists(join(root, "Gemfile"));
227
+ if (hasGemfile) {
228
+ try {
229
+ const gemfile = await readFile(join(root, "Gemfile"), "utf-8");
230
+ if (gemfile.includes("rails"))
231
+ frameworks.push("rails");
232
+ }
233
+ catch { }
234
+ }
235
+ // Phoenix (Elixir)
236
+ const hasMixFile = await fileExists(join(root, "mix.exs"));
237
+ if (hasMixFile) {
238
+ try {
239
+ const mix = await readFile(join(root, "mix.exs"), "utf-8");
240
+ if (mix.includes("phoenix"))
241
+ frameworks.push("phoenix");
242
+ }
243
+ catch { }
244
+ }
245
+ // Spring Boot (Java/Kotlin)
246
+ const hasPomXml = await fileExists(join(root, "pom.xml"));
247
+ const hasBuildGradle = await fileExists(join(root, "build.gradle")) || await fileExists(join(root, "build.gradle.kts"));
248
+ if (hasPomXml || hasBuildGradle) {
249
+ try {
250
+ const buildFile = hasPomXml
251
+ ? await readFile(join(root, "pom.xml"), "utf-8")
252
+ : await readFile(join(root, hasBuildGradle ? "build.gradle.kts" : "build.gradle"), "utf-8");
253
+ if (buildFile.includes("spring"))
254
+ frameworks.push("spring");
255
+ }
256
+ catch { }
257
+ }
258
+ // Rust web frameworks
259
+ const hasCargoToml = await fileExists(join(root, "Cargo.toml"));
260
+ if (hasCargoToml) {
261
+ try {
262
+ const cargo = await readFile(join(root, "Cargo.toml"), "utf-8");
263
+ if (cargo.includes("actix-web"))
264
+ frameworks.push("actix");
265
+ else if (cargo.includes("axum"))
266
+ frameworks.push("axum");
267
+ }
268
+ catch { }
269
+ }
270
+ // Fallback: detect raw http.createServer if no other frameworks found
271
+ if (frameworks.length === 0) {
272
+ frameworks.push("raw-http");
273
+ }
193
274
  return frameworks;
194
275
  }
195
276
  async function detectORMs(root, pkg) {
@@ -201,12 +282,36 @@ async function detectORMs(root, pkg) {
201
282
  orms.push("prisma");
202
283
  if (deps["typeorm"])
203
284
  orms.push("typeorm");
285
+ if (deps["mongoose"])
286
+ orms.push("mongoose");
287
+ if (deps["sequelize"])
288
+ orms.push("sequelize");
204
289
  const pyDeps = await getPythonDeps(root);
205
290
  if (pyDeps.includes("sqlalchemy"))
206
291
  orms.push("sqlalchemy");
207
292
  const goDeps = await getGoDeps(root);
208
293
  if (goDeps.some((d) => d.includes("gorm")))
209
294
  orms.push("gorm");
295
+ // Rails ActiveRecord
296
+ const hasGemfile = await fileExists(join(root, "Gemfile"));
297
+ if (hasGemfile) {
298
+ try {
299
+ const gemfile = await readFile(join(root, "Gemfile"), "utf-8");
300
+ if (gemfile.includes("activerecord") || gemfile.includes("rails"))
301
+ orms.push("activerecord");
302
+ }
303
+ catch { }
304
+ }
305
+ // Phoenix Ecto
306
+ const hasMixFile = await fileExists(join(root, "mix.exs"));
307
+ if (hasMixFile) {
308
+ try {
309
+ const mix = await readFile(join(root, "mix.exs"), "utf-8");
310
+ if (mix.includes("ecto"))
311
+ orms.push("ecto");
312
+ }
313
+ catch { }
314
+ }
210
315
  return orms;
211
316
  }
212
317
  function detectComponentFramework(deps) {
@@ -223,6 +328,12 @@ async function detectLanguage(root, deps) {
223
328
  const hasPyProject = await fileExists(join(root, "pyproject.toml")) || await fileExists(join(root, "backend/pyproject.toml"));
224
329
  const hasGoMod = await fileExists(join(root, "go.mod"));
225
330
  const hasRequirements = await fileExists(join(root, "requirements.txt")) || await fileExists(join(root, "backend/requirements.txt"));
331
+ const hasGemfile = await fileExists(join(root, "Gemfile"));
332
+ const hasMixExs = await fileExists(join(root, "mix.exs"));
333
+ const hasPomXml = await fileExists(join(root, "pom.xml"));
334
+ const hasBuildGradle = await fileExists(join(root, "build.gradle")) || await fileExists(join(root, "build.gradle.kts"));
335
+ const hasCargoToml = await fileExists(join(root, "Cargo.toml"));
336
+ const hasComposerJson = await fileExists(join(root, "composer.json"));
226
337
  const langs = [];
227
338
  if (hasTsConfig || deps["typescript"])
228
339
  langs.push("typescript");
@@ -230,14 +341,22 @@ async function detectLanguage(root, deps) {
230
341
  langs.push("python");
231
342
  if (hasGoMod)
232
343
  langs.push("go");
344
+ if (hasGemfile)
345
+ langs.push("ruby");
346
+ if (hasMixExs)
347
+ langs.push("elixir");
348
+ if (hasBuildGradle)
349
+ langs.push("kotlin");
350
+ else if (hasPomXml)
351
+ langs.push("java");
352
+ if (hasCargoToml)
353
+ langs.push("rust");
354
+ if (hasComposerJson)
355
+ langs.push("php");
233
356
  if (langs.length > 1)
234
357
  return "mixed";
235
- if (langs[0] === "typescript")
236
- return "typescript";
237
- if (langs[0] === "python")
238
- return "python";
239
- if (langs[0] === "go")
240
- return "go";
358
+ if (langs.length === 1)
359
+ return langs[0];
241
360
  return "javascript";
242
361
  }
243
362
  async function getWorkspacePatterns(root, pkg) {
package/dist/types.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- export type Framework = "next-app" | "next-pages" | "hono" | "express" | "fastify" | "koa" | "flask" | "fastapi" | "django" | "go-net-http" | "gin" | "fiber" | "unknown";
2
- export type ORM = "drizzle" | "prisma" | "typeorm" | "sqlalchemy" | "gorm" | "unknown";
1
+ export type Framework = "next-app" | "next-pages" | "hono" | "express" | "fastify" | "koa" | "nestjs" | "elysia" | "adonis" | "trpc" | "sveltekit" | "remix" | "nuxt" | "flask" | "fastapi" | "django" | "go-net-http" | "gin" | "fiber" | "echo" | "chi" | "rails" | "phoenix" | "spring" | "actix" | "axum" | "raw-http" | "unknown";
2
+ export type ORM = "drizzle" | "prisma" | "typeorm" | "sqlalchemy" | "gorm" | "mongoose" | "sequelize" | "activerecord" | "ecto" | "unknown";
3
3
  export type ComponentFramework = "react" | "vue" | "svelte" | "unknown";
4
4
  export interface ProjectInfo {
5
5
  root: string;
@@ -9,7 +9,7 @@ export interface ProjectInfo {
9
9
  componentFramework: ComponentFramework;
10
10
  isMonorepo: boolean;
11
11
  workspaces: WorkspaceInfo[];
12
- language: "typescript" | "javascript" | "python" | "go" | "mixed";
12
+ language: "typescript" | "javascript" | "python" | "go" | "ruby" | "elixir" | "java" | "kotlin" | "rust" | "php" | "mixed";
13
13
  }
14
14
  export interface WorkspaceInfo {
15
15
  name: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codesight",
3
- "version": "1.0.1",
3
+ "version": "1.1.1",
4
4
  "description": "See your codebase clearly. Universal AI context generator that maps routes, schema, components, dependencies, and more for Claude Code, Cursor, Copilot, Codex, and any AI coding tool.",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -10,6 +10,7 @@
10
10
  "scripts": {
11
11
  "build": "tsc",
12
12
  "dev": "tsx src/index.ts",
13
+ "test": "pnpm build && node --test tests/detectors.test.ts",
13
14
  "prepublishOnly": "pnpm build"
14
15
  },
15
16
  "keywords": [