codesight 1.3.2 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Configuration loader: reads codesight.config.(ts|js|json) from project root.
3
+ */
4
+ import type { CodesightConfig } from "./types.js";
5
+ /**
6
+ * Load config from project root. Returns empty config if no config file found.
7
+ */
8
+ export declare function loadConfig(root: string): Promise<CodesightConfig>;
9
+ /**
10
+ * Merges CLI args with config file values (CLI takes precedence).
11
+ */
12
+ export declare function mergeCliConfig(config: CodesightConfig, cli: {
13
+ maxDepth?: number;
14
+ outputDir?: string;
15
+ profile?: string;
16
+ }): CodesightConfig;
package/dist/config.js ADDED
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Configuration loader: reads codesight.config.(ts|js|json) from project root.
3
+ */
4
+ import { readFile, stat } from "node:fs/promises";
5
+ import { join } from "node:path";
6
+ import { pathToFileURL } from "node:url";
7
+ const CONFIG_FILES = [
8
+ "codesight.config.ts",
9
+ "codesight.config.js",
10
+ "codesight.config.mjs",
11
+ "codesight.config.json",
12
+ ];
13
+ async function fileExists(path) {
14
+ try {
15
+ await stat(path);
16
+ return true;
17
+ }
18
+ catch {
19
+ return false;
20
+ }
21
+ }
22
+ /**
23
+ * Load config from project root. Returns empty config if no config file found.
24
+ */
25
+ export async function loadConfig(root) {
26
+ for (const filename of CONFIG_FILES) {
27
+ const configPath = join(root, filename);
28
+ if (!(await fileExists(configPath)))
29
+ continue;
30
+ try {
31
+ if (filename.endsWith(".json")) {
32
+ const content = await readFile(configPath, "utf-8");
33
+ return JSON.parse(content);
34
+ }
35
+ if (filename.endsWith(".ts")) {
36
+ // Try loading with tsx or ts-node if available
37
+ return await loadTsConfig(configPath, root);
38
+ }
39
+ // JS/MJS — dynamic import
40
+ const module = await import(pathToFileURL(configPath).href);
41
+ return (module.default || module);
42
+ }
43
+ catch (err) {
44
+ console.warn(` Warning: failed to load ${filename}: ${err.message}`);
45
+ return {};
46
+ }
47
+ }
48
+ // Also check package.json "codesight" field
49
+ try {
50
+ const pkgPath = join(root, "package.json");
51
+ if (await fileExists(pkgPath)) {
52
+ const pkg = JSON.parse(await readFile(pkgPath, "utf-8"));
53
+ if (pkg.codesight && typeof pkg.codesight === "object") {
54
+ return pkg.codesight;
55
+ }
56
+ }
57
+ }
58
+ catch { }
59
+ return {};
60
+ }
61
+ async function loadTsConfig(configPath, _root) {
62
+ // Strategy 1: try tsx via dynamic import of the .ts file directly
63
+ // (works if tsx or ts-node is installed)
64
+ try {
65
+ const module = await import(pathToFileURL(configPath).href);
66
+ return (module.default || module);
67
+ }
68
+ catch { }
69
+ // Strategy 2: read as text and extract JSON-like config
70
+ // (fallback for when no TS loader is available)
71
+ const content = await readFile(configPath, "utf-8");
72
+ // Try to extract the config object from simple export default { ... }
73
+ const match = content.match(/export\s+default\s+({[\s\S]*})\s*;?\s*$/m);
74
+ if (match) {
75
+ try {
76
+ // Use Function constructor to evaluate the object literal
77
+ // Safe here since this is user's own config file in their project
78
+ const fn = new Function(`return (${match[1]})`);
79
+ return fn();
80
+ }
81
+ catch { }
82
+ }
83
+ console.warn(` Warning: cannot load codesight.config.ts (install tsx for TS config support)`);
84
+ return {};
85
+ }
86
+ /**
87
+ * Merges CLI args with config file values (CLI takes precedence).
88
+ */
89
+ export function mergeCliConfig(config, cli) {
90
+ return {
91
+ ...config,
92
+ maxDepth: cli.maxDepth ?? config.maxDepth,
93
+ outputDir: cli.outputDir ?? config.outputDir,
94
+ profile: cli.profile ?? config.profile,
95
+ };
96
+ }
@@ -257,7 +257,7 @@ async function detectSQLAlchemySchemas(files, project) {
257
257
  if (!content.includes("Base") && !content.includes("DeclarativeBase") && !content.includes("Model"))
258
258
  continue;
259
259
  // Match class definitions
260
- const classPattern = /class\s+(\w+)\s*\([^)]*(?:Base|Model|DeclarativeBase)[^)]*\)\s*:([\s\S]*?)(?=\nclass\s|\n[^\s]|\Z)/g;
260
+ const classPattern = /class\s+(\w+)\s*\([^)]*(?:Base|Model|DeclarativeBase)[^)]*\)\s*:([\s\S]*?)(?=\nclass\s|\n[^\s]|$)/g;
261
261
  let match;
262
262
  while ((match = classPattern.exec(content)) !== null) {
263
263
  const name = match[1];
package/dist/eval.d.ts ADDED
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Evaluation suite: runs codesight on fixture repos and measures
3
+ * precision, recall, and F1 against ground truth.
4
+ */
5
+ export declare function runEval(): Promise<void>;
package/dist/eval.js ADDED
@@ -0,0 +1,184 @@
1
+ /**
2
+ * Evaluation suite: runs codesight on fixture repos and measures
3
+ * precision, recall, and F1 against ground truth.
4
+ */
5
+ import { readFile, writeFile, mkdir, rm } from "node:fs/promises";
6
+ import { join, dirname } from "node:path";
7
+ import { fileURLToPath } from "node:url";
8
+ import { collectFiles, detectProject } from "./scanner.js";
9
+ import { detectRoutes } from "./detectors/routes.js";
10
+ import { detectSchemas } from "./detectors/schema.js";
11
+ import { detectComponents } from "./detectors/components.js";
12
+ import { detectConfig } from "./detectors/config.js";
13
+ import { detectMiddleware } from "./detectors/middleware.js";
14
+ function calcMetrics(detected, expected) {
15
+ let tp = 0;
16
+ let fp = 0;
17
+ let fn = 0;
18
+ for (const item of detected) {
19
+ if (expected.has(item))
20
+ tp++;
21
+ else
22
+ fp++;
23
+ }
24
+ for (const item of expected) {
25
+ if (!detected.has(item))
26
+ fn++;
27
+ }
28
+ const precision = tp + fp > 0 ? tp / (tp + fp) : 1;
29
+ const recall = tp + fn > 0 ? tp / (tp + fn) : 1;
30
+ const f1 = precision + recall > 0 ? (2 * precision * recall) / (precision + recall) : 0;
31
+ return {
32
+ precision: Math.round(precision * 1000) / 1000,
33
+ recall: Math.round(recall * 1000) / 1000,
34
+ f1: Math.round(f1 * 1000) / 1000,
35
+ truePositives: tp,
36
+ falsePositives: fp,
37
+ falseNegatives: fn,
38
+ };
39
+ }
40
+ async function createTempRepo(fixture) {
41
+ const tmpDir = join((await import("node:os")).tmpdir(), `codesight-eval-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`);
42
+ for (const [filePath, content] of Object.entries(fixture.files)) {
43
+ const fullPath = join(tmpDir, filePath);
44
+ await mkdir(dirname(fullPath), { recursive: true });
45
+ await writeFile(fullPath, content);
46
+ }
47
+ return tmpDir;
48
+ }
49
+ async function evalFixture(fixturePath) {
50
+ const repoJson = JSON.parse(await readFile(join(fixturePath, "repo.json"), "utf-8"));
51
+ const groundTruth = JSON.parse(await readFile(join(fixturePath, "ground-truth.json"), "utf-8"));
52
+ // Create temp repo from fixture
53
+ const tmpDir = await createTempRepo(repoJson);
54
+ const startTime = Date.now();
55
+ try {
56
+ // Run codesight detectors
57
+ const project = await detectProject(tmpDir);
58
+ const files = await collectFiles(tmpDir, 10);
59
+ const [routes, schemas, components, config, middleware] = await Promise.all([
60
+ detectRoutes(files, project),
61
+ detectSchemas(files, project),
62
+ detectComponents(files, project),
63
+ detectConfig(files, project),
64
+ detectMiddleware(files, project),
65
+ ]);
66
+ const runtime = Date.now() - startTime;
67
+ // Compare routes: method:path
68
+ const detectedRoutes = new Set(routes.map((r) => `${r.method}:${r.path}`));
69
+ const expectedRoutes = new Set((groundTruth.routes || []).map((r) => `${r.method}:${r.path}`));
70
+ // Compare models: name
71
+ const detectedModels = new Set(schemas.map((s) => s.name.toLowerCase()));
72
+ const expectedModels = new Set((groundTruth.models || []).map((m) => m.name.toLowerCase()));
73
+ // Compare env vars
74
+ const detectedEnvVars = new Set(config.envVars.map((e) => e.name));
75
+ const expectedEnvVars = new Set(groundTruth.envVars || []);
76
+ const result = {
77
+ name: repoJson.name,
78
+ routes: calcMetrics(detectedRoutes, expectedRoutes),
79
+ models: calcMetrics(detectedModels, expectedModels),
80
+ envVars: calcMetrics(detectedEnvVars, expectedEnvVars),
81
+ runtime,
82
+ };
83
+ // Components (if ground truth has them)
84
+ if (groundTruth.components && groundTruth.components.length > 0) {
85
+ const detectedComps = new Set(components.map((c) => c.name));
86
+ const expectedComps = new Set(groundTruth.components.map((c) => c.name));
87
+ result.components = calcMetrics(detectedComps, expectedComps);
88
+ }
89
+ // Middleware
90
+ if (groundTruth.middleware && groundTruth.middleware.length > 0) {
91
+ const detectedMw = new Set(middleware.map((m) => m.name));
92
+ const expectedMw = new Set(groundTruth.middleware);
93
+ result.middleware = calcMetrics(detectedMw, expectedMw);
94
+ }
95
+ return result;
96
+ }
97
+ finally {
98
+ // Cleanup temp dir
99
+ await rm(tmpDir, { recursive: true, force: true }).catch(() => { });
100
+ }
101
+ }
102
+ function formatPercent(n) {
103
+ return `${(n * 100).toFixed(1)}%`;
104
+ }
105
+ function printMetrics(label, m) {
106
+ console.log(` ${label.padEnd(14)} P: ${formatPercent(m.precision).padStart(6)} R: ${formatPercent(m.recall).padStart(6)} F1: ${formatPercent(m.f1).padStart(6)} (TP:${m.truePositives} FP:${m.falsePositives} FN:${m.falseNegatives})`);
107
+ }
108
+ export async function runEval() {
109
+ // Find eval fixtures
110
+ const __dirname = dirname(fileURLToPath(import.meta.url));
111
+ const evalDir = join(__dirname, "..", "eval", "fixtures");
112
+ let fixtureNames;
113
+ try {
114
+ const { readdir } = await import("node:fs/promises");
115
+ fixtureNames = await readdir(evalDir);
116
+ }
117
+ catch {
118
+ // Try from dist path
119
+ const altDir = join(__dirname, "..", "..", "eval", "fixtures");
120
+ const { readdir } = await import("node:fs/promises");
121
+ fixtureNames = await readdir(altDir);
122
+ // Override evalDir for the loop below
123
+ return runEvalFromDir(altDir, fixtureNames);
124
+ }
125
+ return runEvalFromDir(evalDir, fixtureNames);
126
+ }
127
+ async function runEvalFromDir(evalDir, fixtureNames) {
128
+ console.log(`\n codesight eval — precision/recall benchmarks\n`);
129
+ const results = [];
130
+ let totalPrecision = 0;
131
+ let totalRecall = 0;
132
+ let totalF1 = 0;
133
+ let metricCount = 0;
134
+ for (const name of fixtureNames) {
135
+ const fixturePath = join(evalDir, name);
136
+ // Check if it has repo.json
137
+ try {
138
+ await import("node:fs/promises").then((fs) => fs.stat(join(fixturePath, "repo.json")));
139
+ }
140
+ catch {
141
+ continue;
142
+ }
143
+ process.stdout.write(` ${name}...`);
144
+ const result = await evalFixture(fixturePath);
145
+ results.push(result);
146
+ console.log(` ${result.runtime}ms`);
147
+ printMetrics("Routes", result.routes);
148
+ printMetrics("Models", result.models);
149
+ printMetrics("Env vars", result.envVars);
150
+ if (result.components)
151
+ printMetrics("Components", result.components);
152
+ if (result.middleware)
153
+ printMetrics("Middleware", result.middleware);
154
+ console.log("");
155
+ // Accumulate for averages
156
+ const metrics = [result.routes, result.models, result.envVars];
157
+ if (result.components)
158
+ metrics.push(result.components);
159
+ if (result.middleware)
160
+ metrics.push(result.middleware);
161
+ for (const m of metrics) {
162
+ totalPrecision += m.precision;
163
+ totalRecall += m.recall;
164
+ totalF1 += m.f1;
165
+ metricCount++;
166
+ }
167
+ }
168
+ if (results.length === 0) {
169
+ console.log(" No fixtures found. Add fixtures to eval/fixtures/");
170
+ return;
171
+ }
172
+ // Summary
173
+ const avgP = totalPrecision / metricCount;
174
+ const avgR = totalRecall / metricCount;
175
+ const avgF1 = totalF1 / metricCount;
176
+ const totalRuntime = results.reduce((s, r) => s + r.runtime, 0);
177
+ console.log(" ──────────────────────────────────────────");
178
+ console.log(` Fixtures: ${results.length}`);
179
+ console.log(` Avg precision: ${formatPercent(avgP)}`);
180
+ console.log(` Avg recall: ${formatPercent(avgR)}`);
181
+ console.log(` Avg F1: ${formatPercent(avgF1)}`);
182
+ console.log(` Total runtime: ${totalRuntime}ms`);
183
+ console.log("");
184
+ }
package/dist/index.js CHANGED
@@ -14,7 +14,8 @@ 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.3.2";
17
+ import { loadConfig, mergeCliConfig } from "./config.js";
18
+ const VERSION = "1.4.0";
18
19
  const BRAND = "codesight";
19
20
  function printHelp() {
20
21
  console.log(`
@@ -35,9 +36,15 @@ function printHelp() {
35
36
  --benchmark Show detailed token savings breakdown
36
37
  --profile <tool> Generate optimized config (claude-code|cursor|codex|copilot|windsurf)
37
38
  --blast <file> Show blast radius for a file
39
+ --telemetry Run token telemetry (real before/after measurement)
40
+ --eval Run precision/recall benchmarks on eval fixtures
38
41
  -v, --version Show version
39
42
  -h, --help Show this help
40
43
 
44
+ Config:
45
+ Reads codesight.config.(ts|js|json) or package.json "codesight" field.
46
+ See docs for disableDetectors, customRoutePatterns, plugins, and more.
47
+
41
48
  Examples:
42
49
  npx ${BRAND} # Scan current directory
43
50
  npx ${BRAND} --init # Scan + generate AI config files
@@ -45,6 +52,8 @@ function printHelp() {
45
52
  npx ${BRAND} --watch # Watch mode, re-scan on changes
46
53
  npx ${BRAND} --mcp # Start MCP server
47
54
  npx ${BRAND} --hook # Install git pre-commit hook
55
+ npx ${BRAND} --telemetry # Measure real token savings
56
+ npx ${BRAND} --eval # Run accuracy benchmarks
48
57
  npx ${BRAND} ./my-project # Scan specific directory
49
58
  `);
50
59
  }
@@ -57,7 +66,7 @@ async function fileExists(path) {
57
66
  return false;
58
67
  }
59
68
  }
60
- async function scan(root, outputDirName, maxDepth) {
69
+ async function scan(root, outputDirName, maxDepth, userConfig = {}) {
61
70
  const outputDir = join(root, outputDirName);
62
71
  console.log(`\n ${BRAND} v${VERSION}`);
63
72
  console.log(` Scanning: ${root}\n`);
@@ -73,17 +82,39 @@ async function scan(root, outputDirName, maxDepth) {
73
82
  process.stdout.write(" Collecting files...");
74
83
  const files = await collectFiles(root, maxDepth);
75
84
  console.log(` ${files.length} files`);
76
- // Step 3: Run all detectors in parallel
85
+ // Step 3: Run all detectors in parallel (respecting disableDetectors config)
77
86
  process.stdout.write(" Analyzing...");
78
- const [rawRoutes, schemas, components, libs, config, middleware, graph] = await Promise.all([
79
- detectRoutes(files, project),
80
- detectSchemas(files, project),
81
- detectComponents(files, project),
82
- detectLibs(files, project),
83
- detectConfig(files, project),
84
- detectMiddleware(files, project),
85
- detectDependencyGraph(files, project),
87
+ const disabled = new Set(userConfig.disableDetectors || []);
88
+ const [rawRoutes, schemas, components, libs, configResult, middleware, graph] = await Promise.all([
89
+ disabled.has("routes") ? Promise.resolve([]) : detectRoutes(files, project),
90
+ disabled.has("schema") ? Promise.resolve([]) : detectSchemas(files, project),
91
+ disabled.has("components") ? Promise.resolve([]) : detectComponents(files, project),
92
+ disabled.has("libs") ? Promise.resolve([]) : detectLibs(files, project),
93
+ disabled.has("config") ? Promise.resolve({ envVars: [], configFiles: [], dependencies: {}, devDependencies: {} }) : detectConfig(files, project),
94
+ disabled.has("middleware") ? Promise.resolve([]) : detectMiddleware(files, project),
95
+ disabled.has("graph") ? Promise.resolve({ edges: [], hotFiles: [] }) : detectDependencyGraph(files, project),
86
96
  ]);
97
+ // Step 3b: Run plugin detectors
98
+ if (userConfig.plugins) {
99
+ for (const plugin of userConfig.plugins) {
100
+ if (plugin.detector) {
101
+ try {
102
+ const pluginResult = await plugin.detector(files, project);
103
+ if (pluginResult.routes)
104
+ rawRoutes.push(...pluginResult.routes);
105
+ if (pluginResult.schemas)
106
+ schemas.push(...pluginResult.schemas);
107
+ if (pluginResult.components)
108
+ components.push(...pluginResult.components);
109
+ if (pluginResult.middleware)
110
+ middleware.push(...pluginResult.middleware);
111
+ }
112
+ catch (err) {
113
+ console.warn(`\n Warning: plugin "${plugin.name}" failed: ${err.message}`);
114
+ }
115
+ }
116
+ }
117
+ }
87
118
  // Step 4: Enrich routes with contract info
88
119
  const routes = await enrichRouteContracts(rawRoutes, project);
89
120
  // Report AST vs regex detection
@@ -106,7 +137,7 @@ async function scan(root, outputDirName, maxDepth) {
106
137
  schemas,
107
138
  components,
108
139
  libs,
109
- config,
140
+ config: configResult,
110
141
  middleware,
111
142
  graph,
112
143
  tokenStats: { outputTokens: 0, estimatedExplorationTokens: 0, saved: 0, fileCount: files.length },
@@ -126,7 +157,7 @@ async function scan(root, outputDirName, maxDepth) {
126
157
  Models: ${schemas.length}
127
158
  Components: ${components.length}
128
159
  Libraries: ${libs.length}
129
- Env vars: ${config.envVars.length}
160
+ Env vars: ${configResult.envVars.length}
130
161
  Middleware: ${middleware.length}
131
162
  Import links: ${graph.edges.length}
132
163
  Hot files: ${graph.hotFiles.length}
@@ -234,6 +265,8 @@ async function main() {
234
265
  let doBenchmark = false;
235
266
  let doProfile = "";
236
267
  let doBlast = "";
268
+ let doTelemetry = false;
269
+ let doEval = false;
237
270
  for (let i = 0; i < args.length; i++) {
238
271
  const arg = args[i];
239
272
  if ((arg === "-o" || arg === "--output") && args[i + 1]) {
@@ -273,6 +306,12 @@ async function main() {
273
306
  else if (arg === "--blast" && args[i + 1]) {
274
307
  doBlast = args[++i];
275
308
  }
309
+ else if (arg === "--telemetry") {
310
+ doTelemetry = true;
311
+ }
312
+ else if (arg === "--eval") {
313
+ doEval = true;
314
+ }
276
315
  else if (!arg.startsWith("-")) {
277
316
  targetDir = resolve(arg);
278
317
  }
@@ -283,13 +322,58 @@ async function main() {
283
322
  await startMCPServer();
284
323
  return;
285
324
  }
325
+ // Eval mode (standalone, no scan needed)
326
+ if (doEval) {
327
+ const { runEval } = await import("./eval.js");
328
+ await runEval();
329
+ return;
330
+ }
286
331
  const root = resolve(targetDir);
332
+ // Load config file
333
+ const fileConfig = await loadConfig(root);
334
+ const config = mergeCliConfig(fileConfig, {
335
+ maxDepth: maxDepth !== 10 ? maxDepth : undefined,
336
+ outputDir: outputDirName !== ".codesight" ? outputDirName : undefined,
337
+ profile: doProfile || undefined,
338
+ });
339
+ // Apply config overrides
340
+ if (config.maxDepth)
341
+ maxDepth = config.maxDepth;
342
+ if (config.outputDir)
343
+ outputDirName = config.outputDir;
287
344
  // Install git hook
288
345
  if (doHook) {
289
346
  await installGitHook(root, outputDirName);
290
347
  }
291
- // Run scan
292
- const result = await scan(root, outputDirName, maxDepth);
348
+ // Run scan (passes config for disabled detectors + plugins)
349
+ let result = await scan(root, outputDirName, maxDepth, config);
350
+ // Run plugin post-processors
351
+ if (config.plugins) {
352
+ for (const plugin of config.plugins) {
353
+ if (plugin.postProcessor) {
354
+ try {
355
+ result = await plugin.postProcessor(result);
356
+ }
357
+ catch (err) {
358
+ console.warn(` Warning: plugin "${plugin.name}" post-processor failed: ${err.message}`);
359
+ }
360
+ }
361
+ }
362
+ }
363
+ // Token telemetry
364
+ if (doTelemetry) {
365
+ const { runTelemetry } = await import("./telemetry.js");
366
+ const outputDir = join(root, outputDirName);
367
+ process.stdout.write(" Running telemetry...");
368
+ const report = await runTelemetry(root, result, outputDir);
369
+ console.log(` ${outputDirName}/telemetry.md`);
370
+ console.log(`\n Telemetry Results:`);
371
+ for (const task of report.tasks) {
372
+ console.log(` ${task.name}: ${task.reduction}x reduction (${task.tokensWithout.toLocaleString()} → ${task.tokensWith.toLocaleString()} tokens)`);
373
+ }
374
+ console.log(` Average: ${report.summary.averageReduction}x | Tool calls saved: ${report.summary.totalToolCallsSaved}`);
375
+ console.log("");
376
+ }
293
377
  // JSON output
294
378
  if (jsonOutput) {
295
379
  console.log(JSON.stringify(result, null, 2));
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Token telemetry: measures real before/after token usage by simulating
3
+ * what an AI agent would do with and without codesight context.
4
+ *
5
+ * Approach: for each standard task (explain architecture, add route, review diff),
6
+ * measure the actual bytes of context that would be consumed.
7
+ *
8
+ * "Without codesight": count tokens from the files an AI would need to read
9
+ * to discover routes, schema, components, config, etc.
10
+ *
11
+ * "With codesight": count tokens from the CODESIGHT.md output.
12
+ */
13
+ import type { ScanResult } from "./types.js";
14
+ export interface TelemetryTask {
15
+ name: string;
16
+ description: string;
17
+ /** Files the AI would need to read without codesight */
18
+ filesRead: string[];
19
+ /** Tool calls the AI would make (glob, grep, read) */
20
+ toolCalls: number;
21
+ /** Tokens consumed reading those files */
22
+ tokensWithout: number;
23
+ /** Tokens consumed from codesight output */
24
+ tokensWith: number;
25
+ /** Reduction factor */
26
+ reduction: number;
27
+ }
28
+ export interface TelemetryReport {
29
+ project: string;
30
+ tasks: TelemetryTask[];
31
+ summary: {
32
+ totalTokensWithout: number;
33
+ totalTokensWith: number;
34
+ averageReduction: number;
35
+ totalToolCallsSaved: number;
36
+ };
37
+ }
38
+ export declare function runTelemetry(root: string, result: ScanResult, outputDir: string): Promise<TelemetryReport>;
@@ -0,0 +1,257 @@
1
+ /**
2
+ * Token telemetry: measures real before/after token usage by simulating
3
+ * what an AI agent would do with and without codesight context.
4
+ *
5
+ * Approach: for each standard task (explain architecture, add route, review diff),
6
+ * measure the actual bytes of context that would be consumed.
7
+ *
8
+ * "Without codesight": count tokens from the files an AI would need to read
9
+ * to discover routes, schema, components, config, etc.
10
+ *
11
+ * "With codesight": count tokens from the CODESIGHT.md output.
12
+ */
13
+ import { readFile } from "node:fs/promises";
14
+ import { join, relative } from "node:path";
15
+ function countTokens(text) {
16
+ return Math.ceil(text.length / 4);
17
+ }
18
+ async function readFileSafe(path) {
19
+ try {
20
+ return await readFile(path, "utf-8");
21
+ }
22
+ catch {
23
+ return "";
24
+ }
25
+ }
26
+ /**
27
+ * Task 1: "Explain the architecture"
28
+ * Without codesight: AI reads package.json, scans dirs, reads route files,
29
+ * schema files, config files, middleware files — typically 15-25 file reads.
30
+ */
31
+ async function measureExplainArchitecture(root, result, codesightTokens) {
32
+ const filesToRead = new Set();
33
+ // AI would read package.json first
34
+ filesToRead.add(join(root, "package.json"));
35
+ // Then scan for route files
36
+ for (const route of result.routes) {
37
+ filesToRead.add(join(root, route.file));
38
+ }
39
+ // Schema files
40
+ for (const _schema of result.schemas) {
41
+ // Find the file containing this schema from routes or libs
42
+ for (const lib of result.libs) {
43
+ if (lib.file.includes("schema") || lib.file.includes("model") || lib.file.includes("db")) {
44
+ filesToRead.add(join(root, lib.file));
45
+ }
46
+ }
47
+ }
48
+ // Config files
49
+ for (const cf of result.config.configFiles) {
50
+ filesToRead.add(join(root, cf));
51
+ }
52
+ // Middleware files
53
+ for (const mw of result.middleware) {
54
+ filesToRead.add(join(root, mw.file));
55
+ }
56
+ // Hot files (AI would discover these during exploration)
57
+ for (const hf of result.graph.hotFiles.slice(0, 10)) {
58
+ filesToRead.add(join(root, hf.file));
59
+ }
60
+ // Read all files and count tokens
61
+ let totalTokens = 0;
62
+ const readFiles = [];
63
+ for (const f of filesToRead) {
64
+ const content = await readFileSafe(f);
65
+ if (content) {
66
+ totalTokens += countTokens(content);
67
+ readFiles.push(relative(root, f));
68
+ }
69
+ }
70
+ // Add overhead for glob/grep tool calls (each costs ~50-100 tokens for command + results)
71
+ const toolCalls = Math.max(10, Math.ceil(filesToRead.size * 0.8));
72
+ totalTokens += toolCalls * 75; // average 75 tokens per tool call overhead
73
+ const reduction = totalTokens > 0 ? Math.round((totalTokens / codesightTokens) * 10) / 10 : 1;
74
+ return {
75
+ name: "Explain architecture",
76
+ description: "Understand project stack, routes, schema, and dependencies",
77
+ filesRead: readFiles,
78
+ toolCalls,
79
+ tokensWithout: totalTokens,
80
+ tokensWith: codesightTokens,
81
+ reduction,
82
+ };
83
+ }
84
+ /**
85
+ * Task 2: "Add a new API route"
86
+ * Without codesight: AI needs to find existing routes to match patterns,
87
+ * read schema for related models, check middleware, check config.
88
+ */
89
+ async function measureAddRoute(root, result, codesightTokens) {
90
+ const filesToRead = new Set();
91
+ // AI would grep for existing route patterns — reads 3-5 route files
92
+ const routeFiles = [...new Set(result.routes.map((r) => r.file))];
93
+ for (const f of routeFiles.slice(0, 5)) {
94
+ filesToRead.add(join(root, f));
95
+ }
96
+ // Read schema to understand models
97
+ for (const lib of result.libs) {
98
+ if (lib.file.includes("schema") || lib.file.includes("model") || lib.file.includes("db")) {
99
+ filesToRead.add(join(root, lib.file));
100
+ }
101
+ }
102
+ // Check middleware to know what to apply
103
+ for (const mw of result.middleware) {
104
+ filesToRead.add(join(root, mw.file));
105
+ }
106
+ let totalTokens = 0;
107
+ const readFiles = [];
108
+ for (const f of filesToRead) {
109
+ const content = await readFileSafe(f);
110
+ if (content) {
111
+ totalTokens += countTokens(content);
112
+ readFiles.push(relative(root, f));
113
+ }
114
+ }
115
+ const toolCalls = Math.max(6, Math.ceil(filesToRead.size * 0.7));
116
+ totalTokens += toolCalls * 75;
117
+ // With codesight, AI only reads the routes + schema sections (~40% of output)
118
+ const withTokens = Math.ceil(codesightTokens * 0.4);
119
+ const reduction = totalTokens > 0 ? Math.round((totalTokens / withTokens) * 10) / 10 : 1;
120
+ return {
121
+ name: "Add new API route",
122
+ description: "Find route patterns, check schema, apply middleware",
123
+ filesRead: readFiles,
124
+ toolCalls,
125
+ tokensWithout: totalTokens,
126
+ tokensWith: withTokens,
127
+ reduction,
128
+ };
129
+ }
130
+ /**
131
+ * Task 3: "Review a diff / understand blast radius"
132
+ * Without codesight: AI needs to trace imports, find dependents, check what routes
133
+ * and models are affected by a file change.
134
+ */
135
+ async function measureReviewDiff(root, result, codesightTokens) {
136
+ const filesToRead = new Set();
137
+ // AI would read the changed file + all its importers
138
+ // Simulate: pick the hottest file and trace its dependents
139
+ if (result.graph.hotFiles.length > 0) {
140
+ const hotFile = result.graph.hotFiles[0];
141
+ filesToRead.add(join(root, hotFile.file));
142
+ // Read files that import it
143
+ for (const edge of result.graph.edges) {
144
+ if (edge.to === hotFile.file) {
145
+ filesToRead.add(join(root, edge.from));
146
+ }
147
+ }
148
+ }
149
+ // Also read some route files to check impact
150
+ const routeFiles = [...new Set(result.routes.map((r) => r.file))];
151
+ for (const f of routeFiles.slice(0, 3)) {
152
+ filesToRead.add(join(root, f));
153
+ }
154
+ let totalTokens = 0;
155
+ const readFiles = [];
156
+ for (const f of filesToRead) {
157
+ const content = await readFileSafe(f);
158
+ if (content) {
159
+ totalTokens += countTokens(content);
160
+ readFiles.push(relative(root, f));
161
+ }
162
+ }
163
+ const toolCalls = Math.max(8, Math.ceil(filesToRead.size * 0.6));
164
+ totalTokens += toolCalls * 75;
165
+ // With codesight, AI reads graph section + routes (~50% of output)
166
+ const withTokens = Math.ceil(codesightTokens * 0.5);
167
+ const reduction = totalTokens > 0 ? Math.round((totalTokens / withTokens) * 10) / 10 : 1;
168
+ return {
169
+ name: "Review diff / blast radius",
170
+ description: "Trace imports, find affected routes and models",
171
+ filesRead: readFiles,
172
+ toolCalls,
173
+ tokensWithout: totalTokens,
174
+ tokensWith: withTokens,
175
+ reduction,
176
+ };
177
+ }
178
+ export async function runTelemetry(root, result, outputDir) {
179
+ // Read the codesight output to get real token count
180
+ const codesightContent = await readFileSafe(join(outputDir, "CODESIGHT.md"));
181
+ const codesightTokens = countTokens(codesightContent);
182
+ const tasks = await Promise.all([
183
+ measureExplainArchitecture(root, result, codesightTokens),
184
+ measureAddRoute(root, result, codesightTokens),
185
+ measureReviewDiff(root, result, codesightTokens),
186
+ ]);
187
+ const totalWithout = tasks.reduce((s, t) => s + t.tokensWithout, 0);
188
+ const totalWith = tasks.reduce((s, t) => s + t.tokensWith, 0);
189
+ const totalToolCalls = tasks.reduce((s, t) => s + t.toolCalls, 0);
190
+ const report = {
191
+ project: result.project.name,
192
+ tasks,
193
+ summary: {
194
+ totalTokensWithout: totalWithout,
195
+ totalTokensWith: totalWith,
196
+ averageReduction: totalWith > 0 ? Math.round((totalWithout / totalWith) * 10) / 10 : 1,
197
+ totalToolCallsSaved: totalToolCalls,
198
+ },
199
+ };
200
+ // Write telemetry report
201
+ const reportLines = [
202
+ `# Token Telemetry: ${result.project.name}`,
203
+ "",
204
+ `> Measured by reading the actual files an AI agent would need for each task,`,
205
+ `> then comparing against the codesight output (~${codesightTokens.toLocaleString()} tokens).`,
206
+ "",
207
+ "## Tasks",
208
+ "",
209
+ ];
210
+ for (const task of tasks) {
211
+ reportLines.push(`### ${task.name}`);
212
+ reportLines.push(`_${task.description}_`);
213
+ reportLines.push("");
214
+ reportLines.push(`| Metric | Value |`);
215
+ reportLines.push(`|---|---|`);
216
+ reportLines.push(`| Files AI would read | ${task.filesRead.length} |`);
217
+ reportLines.push(`| Tool calls (glob/grep/read) | ${task.toolCalls} |`);
218
+ reportLines.push(`| Tokens without codesight | ~${task.tokensWithout.toLocaleString()} |`);
219
+ reportLines.push(`| Tokens with codesight | ~${task.tokensWith.toLocaleString()} |`);
220
+ reportLines.push(`| **Reduction** | **${task.reduction}x** |`);
221
+ reportLines.push("");
222
+ if (task.filesRead.length > 0) {
223
+ reportLines.push("<details>");
224
+ reportLines.push(`<summary>Files read (${task.filesRead.length})</summary>`);
225
+ reportLines.push("");
226
+ for (const f of task.filesRead) {
227
+ reportLines.push(`- \`${f}\``);
228
+ }
229
+ reportLines.push("");
230
+ reportLines.push("</details>");
231
+ reportLines.push("");
232
+ }
233
+ }
234
+ reportLines.push("## Summary");
235
+ reportLines.push("");
236
+ reportLines.push(`| Metric | Value |`);
237
+ reportLines.push(`|---|---|`);
238
+ reportLines.push(`| Total tokens without codesight | ~${report.summary.totalTokensWithout.toLocaleString()} |`);
239
+ reportLines.push(`| Total tokens with codesight | ~${report.summary.totalTokensWith.toLocaleString()} |`);
240
+ reportLines.push(`| **Average reduction** | **${report.summary.averageReduction}x** |`);
241
+ reportLines.push(`| Tool calls saved | ${report.summary.totalToolCallsSaved} |`);
242
+ reportLines.push("");
243
+ reportLines.push("## Methodology");
244
+ reportLines.push("");
245
+ reportLines.push("Token counts are calculated by reading the actual source files an AI agent would");
246
+ reportLines.push("need to explore for each task, using the ~4 chars/token heuristic (standard for");
247
+ reportLines.push("GPT/Claude tokenizers). Tool call overhead is estimated at ~75 tokens per call");
248
+ reportLines.push("(command text + result formatting). The \"with codesight\" count uses the real");
249
+ reportLines.push("CODESIGHT.md output size, proportioned to the sections relevant to each task.");
250
+ reportLines.push("");
251
+ reportLines.push(`_Generated by codesight --telemetry_`);
252
+ const { writeFile: wf } = await import("node:fs/promises");
253
+ const { mkdir } = await import("node:fs/promises");
254
+ await mkdir(outputDir, { recursive: true });
255
+ await wf(join(outputDir, "telemetry.md"), reportLines.join("\n"));
256
+ return report;
257
+ }
package/dist/types.d.ts CHANGED
@@ -95,12 +95,47 @@ export interface BlastRadiusResult {
95
95
  depth: number;
96
96
  }
97
97
  export interface CodesightConfig {
98
+ /** Disable specific detectors: "routes", "schema", "components", "libs", "config", "middleware", "graph" */
98
99
  disableDetectors?: string[];
100
+ /** Custom route tags: { "billing": ["stripe", "payment"] } */
99
101
  customTags?: Record<string, string[]>;
102
+ /** Max directory depth (default: 10) */
100
103
  maxDepth?: number;
104
+ /** Output directory name (default: ".codesight") */
101
105
  outputDir?: string;
106
+ /** AI tool profile */
102
107
  profile?: "claude-code" | "cursor" | "codex" | "copilot" | "windsurf" | "generic";
108
+ /** Additional ignore patterns (glob-style) */
103
109
  ignorePatterns?: string[];
110
+ /** Custom route patterns: [{ pattern: "router\\.handle\\(", method: "ALL" }] */
111
+ customRoutePatterns?: {
112
+ pattern: string;
113
+ method?: string;
114
+ }[];
115
+ /** Blast radius max BFS depth (default: 5) */
116
+ blastRadiusDepth?: number;
117
+ /** Hot file threshold: min imports to be "hot" (default: 3) */
118
+ hotFileThreshold?: number;
119
+ /** Plugin hooks */
120
+ plugins?: CodesightPlugin[];
121
+ }
122
+ export interface CodesightPlugin {
123
+ /** Plugin name for identification */
124
+ name: string;
125
+ /** Custom detector: runs after built-in detectors */
126
+ detector?: (files: string[], project: ProjectInfo) => Promise<PluginDetectorResult>;
127
+ /** Post-processor: transforms the final ScanResult */
128
+ postProcessor?: (result: ScanResult) => Promise<ScanResult>;
129
+ }
130
+ export interface PluginDetectorResult {
131
+ /** Additional routes to merge */
132
+ routes?: RouteInfo[];
133
+ /** Additional schema models to merge */
134
+ schemas?: SchemaModel[];
135
+ /** Additional components to merge */
136
+ components?: ComponentInfo[];
137
+ /** Additional middleware to merge */
138
+ middleware?: MiddlewareInfo[];
104
139
  }
105
140
  export interface ScanResult {
106
141
  project: ProjectInfo;
package/eval/README.md ADDED
@@ -0,0 +1,36 @@
1
+ # codesight Evaluation Suite
2
+
3
+ Reproducible accuracy benchmarks for codesight detectors.
4
+
5
+ ## How It Works
6
+
7
+ Each fixture in `fixtures/` contains:
8
+ - `repo.json` — describes the repo structure (files with inline content)
9
+ - `ground-truth.json` — expected detection results (routes, models, env vars, blast radius)
10
+
11
+ Running `npx codesight --eval` will:
12
+ 1. Create temporary directories from each fixture
13
+ 2. Run codesight detectors on them
14
+ 3. Compare results against ground truth
15
+ 4. Print precision, recall, F1 score, and runtime per fixture
16
+
17
+ ## Fixtures
18
+
19
+ | Fixture | Stack | What it tests |
20
+ |---|---|---|
21
+ | `nextjs-drizzle` | Next.js App Router + Drizzle ORM | Routes, schema, components, env vars |
22
+ | `express-prisma` | Express + Prisma | Route detection, schema parsing, middleware |
23
+ | `fastapi-sqlalchemy` | FastAPI + SQLAlchemy | Python routes, Python ORM, config |
24
+ | `hono-monorepo` | Hono + Drizzle (pnpm monorepo) | Monorepo detection, workspace routes, schema |
25
+
26
+ ## Adding a Fixture
27
+
28
+ 1. Create a folder in `fixtures/` with `repo.json` and `ground-truth.json`
29
+ 2. Follow the JSON schema used by existing fixtures
30
+ 3. Run `npx codesight --eval` to verify
31
+
32
+ ## Metrics
33
+
34
+ - **Precision**: of all items codesight detected, how many are correct?
35
+ - **Recall**: of all items that exist, how many did codesight find?
36
+ - **F1**: harmonic mean of precision and recall
@@ -0,0 +1,31 @@
1
+ {
2
+ "routes": [
3
+ { "method": "POST", "path": "/login" },
4
+ { "method": "POST", "path": "/register" },
5
+ { "method": "GET", "path": "/" },
6
+ { "method": "GET", "path": "/:id" },
7
+ { "method": "PUT", "path": "/:id" },
8
+ { "method": "DELETE", "path": "/:id" },
9
+ { "method": "POST", "path": "/" }
10
+ ],
11
+ "models": [
12
+ {
13
+ "name": "User",
14
+ "fields": ["id", "email", "password", "name", "role", "posts", "createdAt"]
15
+ },
16
+ {
17
+ "name": "Post",
18
+ "fields": ["id", "title", "content", "published", "author", "authorId", "tags", "createdAt"]
19
+ },
20
+ {
21
+ "name": "Tag",
22
+ "fields": ["id", "name", "posts"]
23
+ },
24
+ {
25
+ "name": "enum:Role",
26
+ "fields": ["USER", "ADMIN"]
27
+ }
28
+ ],
29
+ "envVars": ["DATABASE_URL", "JWT_SECRET", "PORT", "REDIS_URL", "CORS_ORIGIN"],
30
+ "middleware": ["auth", "error", "rate-limit", "cors"]
31
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "express-prisma-api",
3
+ "description": "Express.js REST API with Prisma ORM and middleware",
4
+ "files": {
5
+ "package.json": "{\"name\":\"api-server\",\"dependencies\":{\"express\":\"4.18.0\",\"@prisma/client\":\"5.0.0\",\"cors\":\"2.8.5\",\"helmet\":\"7.0.0\",\"express-rate-limit\":\"7.0.0\",\"jsonwebtoken\":\"9.0.0\"},\"devDependencies\":{\"typescript\":\"5.3.0\",\"@types/express\":\"4.17.0\",\"@types/node\":\"20.0.0\",\"prisma\":\"5.0.0\"}}",
6
+ "tsconfig.json": "{\"compilerOptions\":{\"target\":\"es2017\",\"module\":\"commonjs\",\"outDir\":\"dist\"}}",
7
+ ".env.example": "DATABASE_URL=postgresql://localhost:5432/api\nJWT_SECRET=changeme\nPORT=3000\nREDIS_URL=redis://localhost:6379\nCORS_ORIGIN=http://localhost:3001",
8
+ "prisma/schema.prisma": "datasource db {\n provider = \"postgresql\"\n url = env(\"DATABASE_URL\")\n}\n\ngenerator client {\n provider = \"prisma-client-js\"\n}\n\nmodel User {\n id Int @id @default(autoincrement())\n email String @unique\n password String\n name String?\n role Role @default(USER)\n posts Post[]\n createdAt DateTime @default(now())\n}\n\nmodel Post {\n id Int @id @default(autoincrement())\n title String\n content String?\n published Boolean @default(false)\n author User @relation(fields: [authorId], references: [id])\n authorId Int\n tags Tag[]\n createdAt DateTime @default(now())\n}\n\nmodel Tag {\n id Int @id @default(autoincrement())\n name String @unique\n posts Post[]\n}\n\nenum Role {\n USER\n ADMIN\n}",
9
+ "src/index.ts": "import express from 'express';\nimport cors from 'cors';\nimport helmet from 'helmet';\nimport { authRouter } from './routes/auth';\nimport { usersRouter } from './routes/users';\nimport { postsRouter } from './routes/posts';\nimport { errorHandler } from './middleware/error';\nimport { rateLimiter } from './middleware/rate-limit';\n\nconst app = express();\n\napp.use(cors());\napp.use(helmet());\napp.use(express.json());\napp.use(rateLimiter);\n\napp.use('/api/auth', authRouter);\napp.use('/api/users', usersRouter);\napp.use('/api/posts', postsRouter);\n\napp.use(errorHandler);\n\napp.listen(process.env.PORT || 3000);",
10
+ "src/routes/auth.ts": "import { Router } from 'express';\nimport { prisma } from '../lib/prisma';\nimport { generateToken } from '../lib/jwt';\n\nexport const authRouter = Router();\n\nauthRouter.post('/login', async (req, res) => {\n const { email, password } = req.body;\n const user = await prisma.user.findUnique({ where: { email } });\n if (!user) return res.status(401).json({ error: 'Invalid credentials' });\n const token = generateToken(user.id);\n res.json({ token });\n});\n\nauthRouter.post('/register', async (req, res) => {\n const { email, password, name } = req.body;\n const user = await prisma.user.create({ data: { email, password, name } });\n const token = generateToken(user.id);\n res.status(201).json({ token });\n});",
11
+ "src/routes/users.ts": "import { Router } from 'express';\nimport { prisma } from '../lib/prisma';\nimport { authenticate } from '../middleware/auth';\n\nexport const usersRouter = Router();\n\nusersRouter.get('/', authenticate, async (req, res) => {\n const users = await prisma.user.findMany();\n res.json(users);\n});\n\nusersRouter.get('/:id', authenticate, async (req, res) => {\n const user = await prisma.user.findUnique({ where: { id: parseInt(req.params.id) } });\n res.json(user);\n});\n\nusersRouter.put('/:id', authenticate, async (req, res) => {\n const user = await prisma.user.update({ where: { id: parseInt(req.params.id) }, data: req.body });\n res.json(user);\n});\n\nusersRouter.delete('/:id', authenticate, async (req, res) => {\n await prisma.user.delete({ where: { id: parseInt(req.params.id) } });\n res.json({ deleted: true });\n});",
12
+ "src/routes/posts.ts": "import { Router } from 'express';\nimport { prisma } from '../lib/prisma';\nimport { authenticate } from '../middleware/auth';\n\nexport const postsRouter = Router();\n\npostsRouter.get('/', async (req, res) => {\n const posts = await prisma.post.findMany({ include: { author: true, tags: true } });\n res.json(posts);\n});\n\npostsRouter.get('/:id', async (req, res) => {\n const post = await prisma.post.findUnique({ where: { id: parseInt(req.params.id) }, include: { author: true, tags: true } });\n res.json(post);\n});\n\npostsRouter.post('/', authenticate, async (req, res) => {\n const post = await prisma.post.create({ data: { ...req.body, authorId: req.userId } });\n res.status(201).json(post);\n});\n\npostsRouter.put('/:id', authenticate, async (req, res) => {\n const post = await prisma.post.update({ where: { id: parseInt(req.params.id) }, data: req.body });\n res.json(post);\n});\n\npostsRouter.delete('/:id', authenticate, async (req, res) => {\n await prisma.post.delete({ where: { id: parseInt(req.params.id) } });\n res.json({ deleted: true });\n});",
13
+ "src/middleware/auth.ts": "import { Request, Response, NextFunction } from 'express';\nimport { verifyToken } from '../lib/jwt';\n\nexport function authenticate(req: Request, res: Response, next: NextFunction) {\n const token = req.headers.authorization?.replace('Bearer ', '');\n if (!token) return res.status(401).json({ error: 'No token' });\n try {\n const payload = verifyToken(token);\n (req as any).userId = payload.userId;\n next();\n } catch {\n res.status(401).json({ error: 'Invalid token' });\n }\n}",
14
+ "src/middleware/error.ts": "import { Request, Response, NextFunction } from 'express';\n\nexport function errorHandler(err: Error, req: Request, res: Response, next: NextFunction) {\n console.error(err.stack);\n res.status(500).json({ error: 'Internal server error' });\n}",
15
+ "src/middleware/rate-limit.ts": "import rateLimit from 'express-rate-limit';\n\nexport const rateLimiter = rateLimit({\n windowMs: 15 * 60 * 1000,\n max: 100,\n message: { error: 'Too many requests' }\n});",
16
+ "src/lib/prisma.ts": "import { PrismaClient } from '@prisma/client';\n\nexport const prisma = new PrismaClient();",
17
+ "src/lib/jwt.ts": "import jwt from 'jsonwebtoken';\n\nconst SECRET = process.env.JWT_SECRET || 'dev-secret';\n\nexport function generateToken(userId: number): string {\n return jwt.sign({ userId }, SECRET, { expiresIn: '24h' });\n}\n\nexport function verifyToken(token: string): { userId: number } {\n return jwt.verify(token, SECRET) as { userId: number };\n}"
18
+ }
19
+ }
@@ -0,0 +1,29 @@
1
+ {
2
+ "routes": [
3
+ { "method": "GET", "path": "/health" },
4
+ { "method": "GET", "path": "/" },
5
+ { "method": "GET", "path": "/{user_id}" },
6
+ { "method": "POST", "path": "/" },
7
+ { "method": "DELETE", "path": "/{user_id}" },
8
+ { "method": "GET", "path": "/{item_id}" },
9
+ { "method": "POST", "path": "/" },
10
+ { "method": "PUT", "path": "/{item_id}" },
11
+ { "method": "POST", "path": "/login" },
12
+ { "method": "POST", "path": "/register" }
13
+ ],
14
+ "models": [
15
+ {
16
+ "name": "User",
17
+ "fields": ["id", "email", "password", "name", "is_active", "items", "created_at"]
18
+ },
19
+ {
20
+ "name": "Item",
21
+ "fields": ["id", "title", "description", "price", "owner_id", "owner", "created_at"]
22
+ },
23
+ {
24
+ "name": "Category",
25
+ "fields": ["id", "name", "description"]
26
+ }
27
+ ],
28
+ "envVars": ["DATABASE_URL", "SECRET_KEY", "DEBUG", "ALLOWED_ORIGINS"]
29
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "fastapi-sqlalchemy-api",
3
+ "description": "FastAPI with SQLAlchemy models",
4
+ "files": {
5
+ "requirements.txt": "fastapi==0.104.0\nuvicorn==0.24.0\nsqlalchemy==2.0.23\nalembic==1.12.0\npydantic==2.5.0\npython-dotenv==1.0.0",
6
+ ".env.example": "DATABASE_URL=postgresql://localhost:5432/fastapi_db\nSECRET_KEY=changeme\nDEBUG=true\nALLOWED_ORIGINS=http://localhost:3000",
7
+ "app/__init__.py": "",
8
+ "app/main.py": "from fastapi import FastAPI\nfrom fastapi.middleware.cors import CORSMiddleware\nfrom app.routes import users, items, auth\n\napp = FastAPI(title=\"My API\")\n\napp.add_middleware(CORSMiddleware, allow_origins=[\"*\"])\n\napp.include_router(users.router, prefix=\"/api/users\", tags=[\"users\"])\napp.include_router(items.router, prefix=\"/api/items\", tags=[\"items\"])\napp.include_router(auth.router, prefix=\"/api/auth\", tags=[\"auth\"])\n\n@app.get(\"/health\")\ndef health():\n return {\"status\": \"ok\"}",
9
+ "app/routes/__init__.py": "",
10
+ "app/routes/users.py": "from fastapi import APIRouter, Depends, HTTPException\nfrom sqlalchemy.orm import Session\nfrom app.db import get_db\nfrom app.models import User\n\nrouter = APIRouter()\n\n@router.get(\"/\")\ndef list_users(db: Session = Depends(get_db)):\n return db.query(User).all()\n\n@router.get(\"/{user_id}\")\ndef get_user(user_id: int, db: Session = Depends(get_db)):\n user = db.query(User).filter(User.id == user_id).first()\n if not user:\n raise HTTPException(status_code=404)\n return user\n\n@router.post(\"/\")\ndef create_user(data: dict, db: Session = Depends(get_db)):\n user = User(**data)\n db.add(user)\n db.commit()\n return user\n\n@router.delete(\"/{user_id}\")\ndef delete_user(user_id: int, db: Session = Depends(get_db)):\n db.query(User).filter(User.id == user_id).delete()\n db.commit()\n return {\"deleted\": True}",
11
+ "app/routes/items.py": "from fastapi import APIRouter, Depends\nfrom sqlalchemy.orm import Session\nfrom app.db import get_db\nfrom app.models import Item\n\nrouter = APIRouter()\n\n@router.get(\"/\")\ndef list_items(db: Session = Depends(get_db)):\n return db.query(Item).all()\n\n@router.get(\"/{item_id}\")\ndef get_item(item_id: int, db: Session = Depends(get_db)):\n return db.query(Item).filter(Item.id == item_id).first()\n\n@router.post(\"/\")\ndef create_item(data: dict, db: Session = Depends(get_db)):\n item = Item(**data)\n db.add(item)\n db.commit()\n return item\n\n@router.put(\"/{item_id}\")\ndef update_item(item_id: int, data: dict, db: Session = Depends(get_db)):\n item = db.query(Item).filter(Item.id == item_id).first()\n for k, v in data.items():\n setattr(item, k, v)\n db.commit()\n return item",
12
+ "app/routes/auth.py": "from fastapi import APIRouter\n\nrouter = APIRouter()\n\n@router.post(\"/login\")\ndef login(data: dict):\n return {\"token\": \"fake-token\"}\n\n@router.post(\"/register\")\ndef register(data: dict):\n return {\"user\": data}",
13
+ "app/models.py": "from sqlalchemy import Column, Integer, String, Boolean, ForeignKey, DateTime, Float\nfrom sqlalchemy.orm import relationship\nfrom datetime import datetime\nfrom app.db import Base\n\nclass User(Base):\n __tablename__ = 'users'\n id = Column(Integer, primary_key=True)\n email = Column(String, unique=True, nullable=False)\n password = Column(String, nullable=False)\n name = Column(String)\n is_active = Column(Boolean, default=True)\n items = relationship('Item', back_populates='owner')\n created_at = Column(DateTime, default=datetime.utcnow)\n\nclass Item(Base):\n __tablename__ = 'items'\n id = Column(Integer, primary_key=True)\n title = Column(String, nullable=False)\n description = Column(String)\n price = Column(Float)\n owner_id = Column(Integer, ForeignKey('users.id'))\n owner = relationship('User', back_populates='items')\n created_at = Column(DateTime, default=datetime.utcnow)\n\nclass Category(Base):\n __tablename__ = 'categories'\n id = Column(Integer, primary_key=True)\n name = Column(String, unique=True, nullable=False)\n description = Column(String)",
14
+ "app/db.py": "from sqlalchemy import create_engine\nfrom sqlalchemy.orm import sessionmaker, declarative_base\nimport os\n\nDATABASE_URL = os.getenv('DATABASE_URL', 'sqlite:///./test.db')\nengine = create_engine(DATABASE_URL)\nSessionLocal = sessionmaker(bind=engine)\nBase = declarative_base()\n\ndef get_db():\n db = SessionLocal()\n try:\n yield db\n finally:\n db.close()"
15
+ }
16
+ }
@@ -0,0 +1,33 @@
1
+ {
2
+ "routes": [
3
+ { "method": "GET", "path": "/health" },
4
+ { "method": "POST", "path": "/login" },
5
+ { "method": "POST", "path": "/register" },
6
+ { "method": "POST", "path": "/refresh" },
7
+ { "method": "GET", "path": "/" },
8
+ { "method": "GET", "path": "/:id" },
9
+ { "method": "PUT", "path": "/:id" },
10
+ { "method": "DELETE", "path": "/:id" },
11
+ { "method": "GET", "path": "/" },
12
+ { "method": "GET", "path": "/:id" },
13
+ { "method": "POST", "path": "/" }
14
+ ],
15
+ "models": [
16
+ {
17
+ "name": "users",
18
+ "fields": ["id", "email", "name", "role", "createdAt"],
19
+ "relations": ["projects"]
20
+ },
21
+ {
22
+ "name": "projects",
23
+ "fields": ["id", "name", "description", "ownerId", "isPublic", "createdAt"],
24
+ "relations": ["owner"]
25
+ }
26
+ ],
27
+ "components": [
28
+ { "name": "ProjectCard", "props": ["name", "description", "isPublic"] },
29
+ { "name": "UserAvatar", "props": ["name", "size"] }
30
+ ],
31
+ "envVars": ["DATABASE_URL", "JWT_SECRET", "PORT"],
32
+ "middleware": ["auth"]
33
+ }
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "hono-monorepo",
3
+ "description": "Hono API + React frontend in pnpm monorepo with Drizzle",
4
+ "files": {
5
+ "package.json": "{\"name\":\"my-monorepo\",\"private\":true}",
6
+ "pnpm-workspace.yaml": "packages:\n - 'apps/*'\n - 'packages/*'",
7
+ "apps/api/package.json": "{\"name\":\"@mono/api\",\"dependencies\":{\"hono\":\"4.0.0\",\"drizzle-orm\":\"0.30.0\",\"pg\":\"8.11.0\",\"zod\":\"3.22.0\"},\"devDependencies\":{\"typescript\":\"5.3.0\"}}",
8
+ "apps/api/.env.example": "DATABASE_URL=postgres://localhost:5432/mono\nJWT_SECRET=secret\nPORT=4000",
9
+ "apps/api/src/index.ts": "import { Hono } from 'hono';\nimport { cors } from 'hono/cors';\nimport { logger } from 'hono/logger';\nimport { authRoutes } from './routes/auth';\nimport { userRoutes } from './routes/users';\nimport { projectRoutes } from './routes/projects';\n\nconst app = new Hono();\n\napp.use('*', cors());\napp.use('*', logger());\n\napp.get('/health', (c) => c.json({ ok: true }));\n\napp.route('/api/auth', authRoutes);\napp.route('/api/users', userRoutes);\napp.route('/api/projects', projectRoutes);\n\nexport default app;",
10
+ "apps/api/src/routes/auth.ts": "import { Hono } from 'hono';\nimport { zValidator } from '@hono/zod-validator';\nimport { z } from 'zod';\n\nexport const authRoutes = new Hono();\n\nconst loginSchema = z.object({ email: z.string().email(), password: z.string() });\n\nauthRoutes.post('/login', zValidator('json', loginSchema), async (c) => {\n const { email, password } = c.req.valid('json');\n return c.json({ token: 'jwt-token' });\n});\n\nauthRoutes.post('/register', async (c) => {\n const body = await c.req.json();\n return c.json({ user: body }, 201);\n});\n\nauthRoutes.post('/refresh', async (c) => {\n return c.json({ token: 'new-token' });\n});",
11
+ "apps/api/src/routes/users.ts": "import { Hono } from 'hono';\nimport { db } from '../db';\nimport { users } from '../db/schema';\nimport { eq } from 'drizzle-orm';\n\nexport const userRoutes = new Hono();\n\nuserRoutes.get('/', async (c) => {\n const all = await db.select().from(users);\n return c.json(all);\n});\n\nuserRoutes.get('/:id', async (c) => {\n const id = parseInt(c.req.param('id'));\n const user = await db.select().from(users).where(eq(users.id, id));\n return c.json(user[0]);\n});\n\nuserRoutes.put('/:id', async (c) => {\n const id = parseInt(c.req.param('id'));\n const body = await c.req.json();\n const updated = await db.update(users).set(body).where(eq(users.id, id)).returning();\n return c.json(updated[0]);\n});\n\nuserRoutes.delete('/:id', async (c) => {\n const id = parseInt(c.req.param('id'));\n await db.delete(users).where(eq(users.id, id));\n return c.json({ deleted: true });\n});",
12
+ "apps/api/src/routes/projects.ts": "import { Hono } from 'hono';\nimport { db } from '../db';\nimport { projects } from '../db/schema';\nimport { eq } from 'drizzle-orm';\n\nexport const projectRoutes = new Hono();\n\nprojectRoutes.get('/', async (c) => {\n const all = await db.select().from(projects);\n return c.json(all);\n});\n\nprojectRoutes.get('/:id', async (c) => {\n const id = parseInt(c.req.param('id'));\n const project = await db.select().from(projects).where(eq(projects.id, id));\n return c.json(project[0]);\n});\n\nprojectRoutes.post('/', async (c) => {\n const body = await c.req.json();\n const created = await db.insert(projects).values(body).returning();\n return c.json(created[0], 201);\n});",
13
+ "apps/api/src/db/index.ts": "import { drizzle } from 'drizzle-orm/node-postgres';\nimport { Pool } from 'pg';\n\nconst pool = new Pool({ connectionString: process.env.DATABASE_URL });\nexport const db = drizzle(pool);",
14
+ "apps/api/src/db/schema.ts": "import { pgTable, serial, text, timestamp, integer, boolean } from 'drizzle-orm/pg-core';\nimport { relations } from 'drizzle-orm';\n\nexport const users = pgTable('users', {\n id: serial('id').primaryKey(),\n email: text('email').notNull().unique(),\n name: text('name'),\n role: text('role').default('user'),\n createdAt: timestamp('created_at').defaultNow()\n});\n\nexport const projects = pgTable('projects', {\n id: serial('id').primaryKey(),\n name: text('name').notNull(),\n description: text('description'),\n ownerId: integer('owner_id').notNull().references(() => users.id),\n isPublic: boolean('is_public').default(true),\n createdAt: timestamp('created_at').defaultNow()\n});\n\nexport const usersRelations = relations(users, ({ many }) => ({\n projects: many(projects)\n}));\n\nexport const projectsRelations = relations(projects, ({ one }) => ({\n owner: one(users, { fields: [projects.ownerId], references: [users.id] })\n}));",
15
+ "apps/api/src/middleware/auth.ts": "import { Context, Next } from 'hono';\n\nexport async function authMiddleware(c: Context, next: Next) {\n const token = c.req.header('Authorization');\n if (!token) return c.json({ error: 'Unauthorized' }, 401);\n await next();\n}",
16
+ "apps/web/package.json": "{\"name\":\"@mono/web\",\"dependencies\":{\"react\":\"18.0.0\",\"react-dom\":\"18.0.0\"},\"devDependencies\":{\"typescript\":\"5.3.0\",\"vite\":\"5.0.0\"}}",
17
+ "apps/web/src/components/ProjectCard.tsx": "import React from 'react';\n\ninterface ProjectCardProps {\n name: string;\n description: string;\n isPublic: boolean;\n}\n\nexport function ProjectCard({ name, description, isPublic }: ProjectCardProps) {\n return <div><h3>{name}</h3><p>{description}</p>{isPublic && <span>Public</span>}</div>;\n}",
18
+ "apps/web/src/components/UserAvatar.tsx": "'use client';\nimport React from 'react';\n\ninterface UserAvatarProps {\n name: string;\n size?: number;\n}\n\nexport function UserAvatar({ name, size = 40 }: UserAvatarProps) {\n return <div style={{ width: size, height: size }}>{name[0]}</div>;\n}"
19
+ }
20
+ }
@@ -0,0 +1,37 @@
1
+ {
2
+ "routes": [
3
+ { "method": "GET", "path": "/api/users" },
4
+ { "method": "POST", "path": "/api/users" },
5
+ { "method": "GET", "path": "/api/users/[id]" },
6
+ { "method": "PUT", "path": "/api/users/[id]" },
7
+ { "method": "DELETE", "path": "/api/users/[id]" },
8
+ { "method": "GET", "path": "/api/posts" },
9
+ { "method": "POST", "path": "/api/posts" },
10
+ { "method": "GET", "path": "/api/posts/[id]/comments" },
11
+ { "method": "POST", "path": "/api/posts/[id]/comments" }
12
+ ],
13
+ "models": [
14
+ {
15
+ "name": "users",
16
+ "fields": ["id", "email", "name", "createdAt"],
17
+ "relations": ["posts", "comments"]
18
+ },
19
+ {
20
+ "name": "posts",
21
+ "fields": ["id", "title", "content", "published", "authorId", "createdAt"],
22
+ "relations": ["author", "comments"]
23
+ },
24
+ {
25
+ "name": "comments",
26
+ "fields": ["id", "body", "postId", "authorId", "createdAt"],
27
+ "relations": []
28
+ }
29
+ ],
30
+ "components": [
31
+ { "name": "UserCard", "props": ["name", "email", "avatar"] },
32
+ { "name": "PostList", "props": ["posts", "onSelect"] },
33
+ { "name": "CommentForm", "props": ["postId", "onSubmit"] }
34
+ ],
35
+ "envVars": ["DATABASE_URL", "NEXTAUTH_SECRET", "NEXT_PUBLIC_API_URL"],
36
+ "middleware": ["middleware", "auth"]
37
+ }
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "nextjs-drizzle-app",
3
+ "description": "Next.js App Router with Drizzle ORM and React components",
4
+ "files": {
5
+ "package.json": "{\"name\":\"my-app\",\"dependencies\":{\"next\":\"14.0.0\",\"react\":\"18.0.0\",\"drizzle-orm\":\"0.30.0\",\"pg\":\"8.11.0\",\"zod\":\"3.22.0\"},\"devDependencies\":{\"typescript\":\"5.3.0\",\"@types/node\":\"20.0.0\"}}",
6
+ "tsconfig.json": "{\"compilerOptions\":{\"target\":\"es2017\",\"module\":\"esnext\",\"jsx\":\"react-jsx\"}}",
7
+ ".env.example": "DATABASE_URL=postgres://localhost:5432/mydb\nNEXTAUTH_SECRET=\nNEXT_PUBLIC_API_URL=http://localhost:3000",
8
+ "drizzle.config.ts": "import { defineConfig } from 'drizzle-kit';\nexport default defineConfig({ schema: './src/db/schema.ts', driver: 'pg' });",
9
+ "src/db/schema.ts": "import { pgTable, serial, text, timestamp, integer, boolean } from 'drizzle-orm/pg-core';\nimport { relations } from 'drizzle-orm';\n\nexport const users = pgTable('users', {\n id: serial('id').primaryKey(),\n email: text('email').notNull().unique(),\n name: text('name'),\n createdAt: timestamp('created_at').defaultNow()\n});\n\nexport const posts = pgTable('posts', {\n id: serial('id').primaryKey(),\n title: text('title').notNull(),\n content: text('content'),\n published: boolean('published').default(false),\n authorId: integer('author_id').notNull().references(() => users.id),\n createdAt: timestamp('created_at').defaultNow()\n});\n\nexport const comments = pgTable('comments', {\n id: serial('id').primaryKey(),\n body: text('body').notNull(),\n postId: integer('post_id').notNull().references(() => posts.id),\n authorId: integer('author_id').notNull().references(() => users.id),\n createdAt: timestamp('created_at').defaultNow()\n});\n\nexport const usersRelations = relations(users, ({ many }) => ({\n posts: many(posts),\n comments: many(comments)\n}));\n\nexport const postsRelations = relations(posts, ({ one, many }) => ({\n author: one(users, { fields: [posts.authorId], references: [users.id] }),\n comments: many(comments)\n}));",
10
+ "src/app/api/users/route.ts": "import { NextResponse } from 'next/server';\nimport { db } from '@/db';\nimport { users } from '@/db/schema';\n\nexport async function GET() {\n const all = await db.select().from(users);\n return NextResponse.json(all);\n}\n\nexport async function POST(req: Request) {\n const body = await req.json();\n const user = await db.insert(users).values(body).returning();\n return NextResponse.json(user, { status: 201 });\n}",
11
+ "src/app/api/users/[id]/route.ts": "import { NextResponse } from 'next/server';\nimport { db } from '@/db';\nimport { users } from '@/db/schema';\nimport { eq } from 'drizzle-orm';\n\nexport async function GET(req: Request, { params }: { params: { id: string } }) {\n const user = await db.select().from(users).where(eq(users.id, parseInt(params.id)));\n return NextResponse.json(user[0]);\n}\n\nexport async function PUT(req: Request, { params }: { params: { id: string } }) {\n const body = await req.json();\n const updated = await db.update(users).set(body).where(eq(users.id, parseInt(params.id))).returning();\n return NextResponse.json(updated[0]);\n}\n\nexport async function DELETE(req: Request, { params }: { params: { id: string } }) {\n await db.delete(users).where(eq(users.id, parseInt(params.id)));\n return NextResponse.json({ deleted: true });\n}",
12
+ "src/app/api/posts/route.ts": "import { NextResponse } from 'next/server';\nimport { db } from '@/db';\nimport { posts } from '@/db/schema';\n\nexport async function GET() {\n const all = await db.select().from(posts);\n return NextResponse.json(all);\n}\n\nexport async function POST(req: Request) {\n const body = await req.json();\n const post = await db.insert(posts).values(body).returning();\n return NextResponse.json(post, { status: 201 });\n}",
13
+ "src/app/api/posts/[id]/comments/route.ts": "import { NextResponse } from 'next/server';\nimport { db } from '@/db';\nimport { comments } from '@/db/schema';\nimport { eq } from 'drizzle-orm';\n\nexport async function GET(req: Request, { params }: { params: { id: string } }) {\n const all = await db.select().from(comments).where(eq(comments.postId, parseInt(params.id)));\n return NextResponse.json(all);\n}\n\nexport async function POST(req: Request, { params }: { params: { id: string } }) {\n const body = await req.json();\n const comment = await db.insert(comments).values({ ...body, postId: parseInt(params.id) }).returning();\n return NextResponse.json(comment, { status: 201 });\n}",
14
+ "src/components/UserCard.tsx": "'use client';\nimport React from 'react';\n\ninterface UserCardProps {\n name: string;\n email: string;\n avatar?: string;\n}\n\nexport function UserCard({ name, email, avatar }: UserCardProps) {\n return <div className=\"card\"><h3>{name}</h3><p>{email}</p></div>;\n}",
15
+ "src/components/PostList.tsx": "import React from 'react';\n\ninterface PostListProps {\n posts: Array<{ id: number; title: string; published: boolean }>;\n onSelect: (id: number) => void;\n}\n\nexport function PostList({ posts, onSelect }: PostListProps) {\n return <ul>{posts.map(p => <li key={p.id} onClick={() => onSelect(p.id)}>{p.title}</li>)}</ul>;\n}",
16
+ "src/components/CommentForm.tsx": "'use client';\nimport React, { useState } from 'react';\n\ninterface CommentFormProps {\n postId: number;\n onSubmit: (body: string) => void;\n}\n\nexport function CommentForm({ postId, onSubmit }: CommentFormProps) {\n const [body, setBody] = useState('');\n return <form onSubmit={() => onSubmit(body)}><textarea value={body} onChange={e => setBody(e.target.value)} /><button type=\"submit\">Submit</button></form>;\n}",
17
+ "src/middleware.ts": "import { NextResponse } from 'next/server';\nimport type { NextRequest } from 'next/server';\n\nexport function middleware(request: NextRequest) {\n const token = request.headers.get('authorization');\n if (!token && request.nextUrl.pathname.startsWith('/api')) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });\n }\n return NextResponse.next();\n}\n\nexport const config = { matcher: '/api/:path*' };",
18
+ "src/lib/auth.ts": "export function verifyToken(token: string): boolean {\n return token.startsWith('Bearer ');\n}\n\nexport function createToken(userId: number): string {\n return `Bearer ${userId}-${Date.now()}`;\n}",
19
+ "src/lib/validate.ts": "import { z } from 'zod';\n\nexport const createUserSchema = z.object({\n email: z.string().email(),\n name: z.string().min(1)\n});\n\nexport const createPostSchema = z.object({\n title: z.string().min(1),\n content: z.string().optional(),\n published: z.boolean().optional()\n});"
20
+ }
21
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codesight",
3
- "version": "1.3.2",
3
+ "version": "1.4.0",
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": {
@@ -50,6 +50,7 @@
50
50
  "node": ">=18.0.0"
51
51
  },
52
52
  "files": [
53
- "dist"
53
+ "dist",
54
+ "eval"
54
55
  ]
55
56
  }