@eloquence98/ctx 0.2.0 → 0.3.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.
package/README.md CHANGED
@@ -1,50 +1,88 @@
1
1
  # ctx
2
2
 
3
- Scan your codebase. Get a clean summary. Paste it to AI.
3
+ Dump a truthful structural index of a codebase.
4
4
 
5
- ## Usage
5
+ No analysis. No opinions. No guessing.
6
6
 
7
- ```bash
8
- # Scan current directory
9
- ctx .
10
- ```
7
+ ctx scans a directory and prints a map of folders, files, and statically detectable exported symbols. It tells you exactly what exists—nothing more, nothing less.
8
+
9
+ ## ⚡️ Quick Start
10
+
11
+ No installation required. Run it directly with npx:
11
12
 
12
13
  ```bash
13
- # Scan specific folder
14
- ctx ./src
14
+ npx @eloquence98/ctx ./path-to-project
15
15
  ```
16
16
 
17
+ ## 📖 What it does
18
+
19
+ ctx provides a high-level map of a project. It identifies:
20
+
21
+ - 📂 Folders
22
+ - 📄 Files
23
+ - ➡️ Exported Symbols (when statically detectable)
24
+
25
+ ## Example Output
26
+
17
27
  ```bash
18
- # Human-readable output
19
- ctx ./src --human
28
+ src/
29
+ ├─ app.tsx → App
30
+ ├─ utils.ts → formatDate, parseCurrency
31
+ └─ components/
32
+ ├─ button.tsx → Button
33
+ ├─ modal.tsx → Modal, ModalProps
34
+ └─ styles.css
20
35
  ```
21
36
 
22
- ## Output Example
37
+ If exports cannot be determined (e.g., non-code files or complex dynamic exports), the file is listed without symbols.
23
38
 
24
- ```bash
25
- # Codebase Context
39
+ ## 🧠 Why this exists
26
40
 
27
- ## Routes
28
- - /admin
29
- - /admin/orders
30
- - /client/[slug]/orders
41
+ When working with LLMs (ChatGPT, Claude, etc.), new contributors, or legacy codebases, you don't always need the content of the files immediately; you need to understand the topology of the project first.
31
42
 
32
- ## Components
33
- - navbar.tsx: Navbar
34
- - sidebar.tsx: Sidebar
43
+ ctx gives you that map.
35
44
 
36
- ## Hooks
37
- - useAuth
38
- - useFetch
45
+ 1. Copy the output.
46
+ 2. Paste it into an LLM context window.
47
+ 3. Ask informed questions about the architecture before dumping raw code.
39
48
 
40
- ## Utils
41
- - utils.ts: cn, formatDate
42
- ```
49
+ ## 🚫 What it does NOT do
50
+
51
+ ctx is intentionally dumb. That is why it is reliable.
43
52
 
44
- ## That's It
53
+ It does not:
45
54
 
46
- Copy. Paste to ChatGPT/Claude. Done.
55
+ - Interpret architecture or infer domains.
56
+ - ❌ Explain code intent.
57
+ - ❌ Refactor or execute code.
58
+ - ❌ Read node_modules or .git folders.
59
+ - ❌ Read environment variables.
60
+
61
+ It is not a framework detector, a dependency graph tool, or a documentation generator.
62
+
63
+ ## ⚙️ Configuration
64
+
65
+ No configuration required.
66
+
67
+ ctx automatically ignores:
68
+
69
+ - node_modules
70
+ - .git
71
+ - Build outputs (dist, build, etc.)
72
+ - Environment files (.env)
73
+ - Test files (.test., .spec.)
74
+
75
+ ## 💡 Philosophy
76
+
77
+ Don't explain the code. Show the codebase as it exists.
78
+
79
+ ## ⚡️ Install (optional)
80
+
81
+ ```bash
82
+ npm install -g @eloquence98/ctx
83
+ ctx ./src
84
+ ```
47
85
 
48
86
  ## License
49
87
 
50
- MIT
88
+ [MIT](https://choosealicense.com/licenses/mit/)
package/dist/cli.js CHANGED
@@ -2,26 +2,20 @@
2
2
  import path from "path";
3
3
  import { scan } from "./scanner.js";
4
4
  import { parse } from "./parser.js";
5
- import { organize } from "./organizer.js";
6
- import { formatAI } from "./formatters/ai.js";
7
- import { formatHuman } from "./formatters/human.js";
5
+ import { format } from "./formatter.js";
8
6
  const args = process.argv.slice(2);
9
- const targetPath = args.find((a) => !a.startsWith("-")) || ".";
10
- const humanMode = args.includes("--human");
7
+ const targetPath = args[0] || ".";
11
8
  async function main() {
12
9
  const dir = path.resolve(process.cwd(), targetPath);
13
- // 1. Scan
10
+ // Scan
14
11
  const files = await scan(dir);
15
12
  if (files.length === 0) {
16
13
  console.log("No files found.");
17
14
  process.exit(1);
18
15
  }
19
- // 2. Parse
16
+ // Parse
20
17
  const parsed = await Promise.all(files.map(parse));
21
- // 3. Organize
22
- const context = await organize(parsed, dir);
23
- // 4. Format
24
- const output = humanMode ? formatHuman(context) : formatAI(context);
25
- console.log(output);
18
+ // Format and print
19
+ console.log(format(parsed, dir));
26
20
  }
27
21
  main().catch(console.error);
@@ -0,0 +1,2 @@
1
+ import type { ParsedFile } from "./types.js";
2
+ export declare function format(files: ParsedFile[], baseDir: string): string;
@@ -0,0 +1,49 @@
1
+ import path from "path";
2
+ export function format(files, baseDir) {
3
+ // Build tree structure
4
+ const root = { name: path.basename(baseDir), children: new Map() };
5
+ for (const file of files) {
6
+ const rel = path.relative(baseDir, file.path);
7
+ const parts = rel.split(path.sep);
8
+ let current = root;
9
+ for (let i = 0; i < parts.length; i++) {
10
+ const part = parts[i];
11
+ const isFile = i === parts.length - 1;
12
+ if (!current.children.has(part)) {
13
+ current.children.set(part, {
14
+ name: part,
15
+ children: new Map(),
16
+ exports: isFile ? file.exports : undefined,
17
+ });
18
+ }
19
+ else if (isFile) {
20
+ current.children.get(part).exports = file.exports;
21
+ }
22
+ current = current.children.get(part);
23
+ }
24
+ }
25
+ // Render tree
26
+ const lines = [];
27
+ lines.push(root.name + "/");
28
+ renderTree(root, "", lines);
29
+ return lines.join("\n");
30
+ }
31
+ function renderTree(node, prefix, lines) {
32
+ const children = [...node.children.values()];
33
+ for (let i = 0; i < children.length; i++) {
34
+ const child = children[i];
35
+ const isLast = i === children.length - 1;
36
+ const connector = isLast ? "└─ " : "├─ ";
37
+ const extension = isLast ? " " : "│ ";
38
+ // Is it a file (has exports defined) or folder?
39
+ const isFile = child.exports !== undefined;
40
+ if (isFile) {
41
+ const exportsStr = child.exports.length > 0 ? ` → ${child.exports.join(", ")}` : "";
42
+ lines.push(prefix + connector + child.name + exportsStr);
43
+ }
44
+ else {
45
+ lines.push(prefix + connector + child.name + "/");
46
+ renderTree(child, prefix + extension, lines);
47
+ }
48
+ }
49
+ }
package/dist/parser.js CHANGED
@@ -3,38 +3,26 @@ import path from "path";
3
3
  export async function parse(filePath) {
4
4
  const content = await fs.readFile(filePath, "utf-8");
5
5
  const name = path.basename(filePath);
6
- const functions = [];
7
- const constants = [];
8
- const types = [];
9
- const components = [];
10
- // Export function
6
+ const exports = [];
7
+ // export function Name
11
8
  for (const match of content.matchAll(/export\s+(?:async\s+)?function\s+(\w+)/g)) {
12
- const fn = match[1];
13
- isPascalCase(fn) ? components.push(fn) : functions.push(fn);
9
+ exports.push(match[1]);
14
10
  }
15
- // Export const
16
- for (const match of content.matchAll(/export\s+const\s+(\w+)\s*=/g)) {
17
- const n = match[1];
18
- if (isPascalCase(n)) {
19
- components.push(n);
20
- }
21
- else if (n.startsWith("use")) {
22
- functions.push(n);
23
- }
24
- else {
25
- constants.push(n);
26
- }
11
+ // export const Name
12
+ for (const match of content.matchAll(/export\s+const\s+(\w+)/g)) {
13
+ exports.push(match[1]);
27
14
  }
28
- // Types & interfaces
15
+ // export type/interface Name
29
16
  for (const match of content.matchAll(/export\s+(?:type|interface)\s+(\w+)/g)) {
30
- types.push(match[1]);
17
+ exports.push(match[1]);
31
18
  }
32
- return {
33
- path: filePath,
34
- name,
35
- exports: { functions, constants, types, components },
36
- };
37
- }
38
- function isPascalCase(str) {
39
- return /^[A-Z][a-zA-Z0-9]*$/.test(str);
19
+ // export class Name
20
+ for (const match of content.matchAll(/export\s+class\s+(\w+)/g)) {
21
+ exports.push(match[1]);
22
+ }
23
+ // export default function Name / class Name
24
+ for (const match of content.matchAll(/export\s+default\s+(?:function|class)\s+(\w+)/g)) {
25
+ exports.push(match[1]);
26
+ }
27
+ return { path: filePath, name, exports };
40
28
  }
package/dist/scanner.js CHANGED
@@ -1,6 +1,19 @@
1
1
  import fs from "fs/promises";
2
2
  import path from "path";
3
- const IGNORE = ["node_modules", ".git", "dist", ".next", "build", "__tests__"];
3
+ const IGNORE = [
4
+ "node_modules",
5
+ ".git",
6
+ ".next",
7
+ "dist",
8
+ "build",
9
+ ".env",
10
+ ".env.local",
11
+ ".env.production",
12
+ "__pycache__",
13
+ ".vscode",
14
+ ".idea",
15
+ "coverage",
16
+ ];
4
17
  const EXTENSIONS = [".ts", ".tsx", ".js", ".jsx"];
5
18
  export async function scan(dir) {
6
19
  const files = [];
@@ -13,18 +26,21 @@ export async function scan(dir) {
13
26
  return;
14
27
  }
15
28
  for (const entry of entries) {
29
+ // Skip ignored
16
30
  if (IGNORE.includes(entry.name))
17
31
  continue;
18
32
  if (entry.name.startsWith("."))
19
33
  continue;
34
+ if (entry.name.includes(".test."))
35
+ continue;
36
+ if (entry.name.includes(".spec."))
37
+ continue;
20
38
  const full = path.join(current, entry.name);
21
39
  if (entry.isDirectory()) {
22
40
  await walk(full);
23
41
  }
24
42
  else if (EXTENSIONS.includes(path.extname(entry.name))) {
25
- if (!entry.name.includes(".test.") && !entry.name.includes(".spec.")) {
26
- files.push(full);
27
- }
43
+ files.push(full);
28
44
  }
29
45
  }
30
46
  }
package/dist/types.d.ts CHANGED
@@ -1,17 +1,9 @@
1
1
  export interface ParsedFile {
2
2
  path: string;
3
3
  name: string;
4
- exports: {
5
- functions: string[];
6
- constants: string[];
7
- types: string[];
8
- components: string[];
9
- };
4
+ exports: string[];
10
5
  }
11
- export interface OrganizedContext {
12
- routes: string[];
13
- components: ParsedFile[];
14
- hooks: ParsedFile[];
15
- utils: ParsedFile[];
16
- other: ParsedFile[];
6
+ export interface FolderContent {
7
+ folder: string;
8
+ files: ParsedFile[];
17
9
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eloquence98/ctx",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Scan your codebase. Get a clean summary. Paste it to AI.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -36,4 +36,4 @@
36
36
  "tsx": "^4.0.0",
37
37
  "typescript": "^5.0.0"
38
38
  }
39
- }
39
+ }
@@ -1,2 +0,0 @@
1
- import type { Adapter } from "./types.js";
2
- export declare const expressAdapter: Adapter;
@@ -1,58 +0,0 @@
1
- import fs from "fs/promises";
2
- import path from "path";
3
- export const expressAdapter = {
4
- name: "express",
5
- async detect(dir) {
6
- try {
7
- const pkg = await fs.readFile(path.join(dir, "package.json"), "utf-8");
8
- const parsed = JSON.parse(pkg);
9
- const deps = { ...parsed.dependencies, ...parsed.devDependencies };
10
- return ("express" in deps ||
11
- "fastify" in deps ||
12
- "koa" in deps ||
13
- "hono" in deps);
14
- }
15
- catch {
16
- return false;
17
- }
18
- },
19
- async analyze(dir, files) {
20
- const sections = new Map();
21
- const folderMappings = {
22
- Routes: ["routes", "routers", "api"],
23
- Controllers: ["controllers", "handlers"],
24
- Services: ["services"],
25
- Models: ["models", "entities", "schemas"],
26
- Middleware: ["middleware", "middlewares"],
27
- Utils: ["utils", "helpers", "lib"],
28
- Config: ["config", "configs"],
29
- };
30
- for (const file of files) {
31
- const relativePath = path.relative(dir, file.filePath);
32
- const parts = relativePath.split(path.sep);
33
- let matched = false;
34
- for (const [sectionName, folders] of Object.entries(folderMappings)) {
35
- const folderIndex = parts.findIndex((p) => folders.includes(p.toLowerCase()));
36
- if (folderIndex !== -1) {
37
- if (!sections.has(sectionName)) {
38
- sections.set(sectionName, []);
39
- }
40
- sections.get(sectionName).push(file);
41
- matched = true;
42
- break;
43
- }
44
- }
45
- if (!matched) {
46
- const folder = parts.length > 1 ? parts[0] : "_root";
47
- if (!sections.has(folder)) {
48
- sections.set(folder, []);
49
- }
50
- sections.get(folder).push(file);
51
- }
52
- }
53
- return {
54
- projectType: "Express/Fastify",
55
- sections,
56
- };
57
- },
58
- };
@@ -1,4 +0,0 @@
1
- import type { ProjectType } from "../detectors/index.js";
2
- import type { Adapter } from "./types.js";
3
- export declare function getAdapter(projectType: ProjectType): Adapter;
4
- export type { Adapter, AdapterContext } from "./types.js";
@@ -1,16 +0,0 @@
1
- import { expressAdapter } from "./express.js";
2
- import { nextjsAdapter } from "./nextjs.js";
3
- import { vanillaAdapter } from "./vanilla.js";
4
- const adapters = {
5
- nextjs: nextjsAdapter,
6
- react: vanillaAdapter,
7
- express: expressAdapter,
8
- nestjs: expressAdapter, // Similar structure
9
- vue: vanillaAdapter,
10
- sveltekit: vanillaAdapter,
11
- node: vanillaAdapter,
12
- vanilla: vanillaAdapter,
13
- };
14
- export function getAdapter(projectType) {
15
- return adapters[projectType] || vanillaAdapter;
16
- }
@@ -1,2 +0,0 @@
1
- import type { Adapter } from "./types.js";
2
- export declare const nextjsAdapter: Adapter;
@@ -1,102 +0,0 @@
1
- import fs from "fs/promises";
2
- import path from "path";
3
- import { getRoutes } from "../scanner.js";
4
- export const nextjsAdapter = {
5
- name: "nextjs",
6
- async detect(dir) {
7
- const configFiles = ["next.config.js", "next.config.mjs", "next.config.ts"];
8
- for (const file of configFiles) {
9
- try {
10
- await fs.access(path.join(dir, file));
11
- return true;
12
- }
13
- catch {
14
- continue;
15
- }
16
- }
17
- // Check package.json for next dependency
18
- try {
19
- const pkg = await fs.readFile(path.join(dir, "package.json"), "utf-8");
20
- const parsed = JSON.parse(pkg);
21
- const deps = { ...parsed.dependencies, ...parsed.devDependencies };
22
- return "next" in deps;
23
- }
24
- catch {
25
- return false;
26
- }
27
- },
28
- async analyze(dir, files) {
29
- const sections = new Map();
30
- // Get routes
31
- let routes = [];
32
- const appDir = await findAppDirectory(dir);
33
- if (appDir) {
34
- routes = await getRoutes(appDir);
35
- }
36
- // Group by known Next.js folders
37
- const folderMappings = {
38
- Features: ["features", "modules", "domains"],
39
- Components: ["components"],
40
- Hooks: ["hooks"],
41
- Lib: ["lib", "utils", "helpers", "services"],
42
- API: ["api"],
43
- };
44
- for (const file of files) {
45
- const relativePath = path.relative(dir, file.filePath);
46
- const parts = relativePath.split(path.sep);
47
- let matched = false;
48
- for (const [sectionName, folders] of Object.entries(folderMappings)) {
49
- const folderIndex = parts.findIndex((p) => folders.includes(p));
50
- if (folderIndex !== -1) {
51
- const subParts = parts.slice(folderIndex + 1);
52
- if (subParts.length >= 1) {
53
- // Use subfolder name if exists, otherwise use section name
54
- const subFolder = subParts.length > 1 ? subParts[0] : "_direct";
55
- const key = subFolder === "_direct"
56
- ? sectionName
57
- : `${sectionName}/${subParts[0]}`;
58
- if (!sections.has(key)) {
59
- sections.set(key, []);
60
- }
61
- sections.get(key).push(file);
62
- matched = true;
63
- break;
64
- }
65
- }
66
- }
67
- // If no match, group by top folder
68
- if (!matched) {
69
- const folder = parts.length > 1 ? parts[0] : "_root";
70
- const key = `Other/${folder}`;
71
- if (!sections.has(key)) {
72
- sections.set(key, []);
73
- }
74
- sections.get(key).push(file);
75
- }
76
- }
77
- return {
78
- projectType: "Next.js",
79
- sections,
80
- routes,
81
- };
82
- },
83
- };
84
- async function findAppDirectory(basePath) {
85
- const possiblePaths = [
86
- path.join(basePath, "app"),
87
- path.join(basePath, "src", "app"),
88
- path.join(basePath, "src/app"),
89
- ];
90
- for (const p of possiblePaths) {
91
- try {
92
- const stat = await fs.stat(p);
93
- if (stat.isDirectory()) {
94
- return p;
95
- }
96
- }
97
- catch {
98
- continue;
99
- }
100
- }
101
- return null;
102
- }
@@ -1,11 +0,0 @@
1
- import type { FileExports } from "../types.js";
2
- export interface AdapterContext {
3
- projectType: string;
4
- sections: Map<string, FileExports[]>;
5
- routes?: string[];
6
- }
7
- export interface Adapter {
8
- name: string;
9
- detect: (dir: string) => Promise<boolean>;
10
- analyze: (dir: string, files: FileExports[]) => Promise<AdapterContext>;
11
- }
@@ -1 +0,0 @@
1
- export {};
@@ -1,2 +0,0 @@
1
- import type { Adapter } from "./types.js";
2
- export declare const vanillaAdapter: Adapter;
@@ -1,24 +0,0 @@
1
- import path from "path";
2
- export const vanillaAdapter = {
3
- name: "vanilla",
4
- async detect() {
5
- // Always returns true - this is the fallback
6
- return true;
7
- },
8
- async analyze(dir, files) {
9
- const sections = new Map();
10
- for (const file of files) {
11
- const relativePath = path.relative(dir, file.filePath);
12
- const parts = relativePath.split(path.sep);
13
- const folder = parts.length > 1 ? parts[0] : "_root";
14
- if (!sections.has(folder)) {
15
- sections.set(folder, []);
16
- }
17
- sections.get(folder).push(file);
18
- }
19
- return {
20
- projectType: "JavaScript/TypeScript",
21
- sections,
22
- };
23
- },
24
- };
package/dist/config.d.ts DELETED
@@ -1,9 +0,0 @@
1
- import type { ScanOptions } from "./types.js";
2
- export declare const defaultConfig: ScanOptions;
3
- export declare const folderCategories: {
4
- routes: string[];
5
- features: string[];
6
- hooks: string[];
7
- lib: string[];
8
- components: string[];
9
- };
package/dist/config.js DELETED
@@ -1,25 +0,0 @@
1
- export const defaultConfig = {
2
- entry: "./src",
3
- extensions: [".ts", ".tsx", ".js", ".jsx"],
4
- ignore: [
5
- "node_modules",
6
- ".git",
7
- ".directory",
8
- "dist",
9
- "build",
10
- ".next",
11
- "*.test.*",
12
- "*.spec.*",
13
- "__tests__",
14
- "*.d.ts",
15
- ".DS_Store",
16
- ],
17
- maxDepth: 10,
18
- };
19
- export const folderCategories = {
20
- routes: ["app", "pages"],
21
- features: ["features", "modules", "domains"],
22
- hooks: ["hooks"],
23
- lib: ["lib", "utils", "helpers", "services"],
24
- components: ["components", "ui"],
25
- };
@@ -1,3 +0,0 @@
1
- export type ProjectType = "nextjs" | "react" | "express" | "nestjs" | "vue" | "sveltekit" | "node" | "vanilla";
2
- export declare function detectProject(dir: string): Promise<ProjectType>;
3
- export declare function getProjectLabel(type: ProjectType): string;
@@ -1,73 +0,0 @@
1
- import fs from "fs/promises";
2
- import path from "path";
3
- async function readPackageJson(dir) {
4
- try {
5
- const content = await fs.readFile(path.join(dir, "package.json"), "utf-8");
6
- return JSON.parse(content);
7
- }
8
- catch {
9
- return null;
10
- }
11
- }
12
- async function fileExists(filePath) {
13
- try {
14
- await fs.access(filePath);
15
- return true;
16
- }
17
- catch {
18
- return false;
19
- }
20
- }
21
- export async function detectProject(dir) {
22
- const pkg = await readPackageJson(dir);
23
- const deps = {
24
- ...pkg?.dependencies,
25
- ...pkg?.devDependencies,
26
- };
27
- // Check config files first (more specific)
28
- const hasNextConfig = (await fileExists(path.join(dir, "next.config.js"))) ||
29
- (await fileExists(path.join(dir, "next.config.mjs"))) ||
30
- (await fileExists(path.join(dir, "next.config.ts")));
31
- if (hasNextConfig || deps["next"]) {
32
- return "nextjs";
33
- }
34
- // NestJS
35
- if (deps["@nestjs/core"]) {
36
- return "nestjs";
37
- }
38
- // SvelteKit
39
- if (deps["@sveltejs/kit"]) {
40
- return "sveltekit";
41
- }
42
- // Vue
43
- if (deps["vue"] || deps["nuxt"]) {
44
- return "vue";
45
- }
46
- // Express
47
- if (deps["express"] || deps["fastify"] || deps["koa"] || deps["hono"]) {
48
- return "express";
49
- }
50
- // React (without Next.js)
51
- if (deps["react"] && !deps["next"]) {
52
- return "react";
53
- }
54
- // Node.js project (has package.json but no framework)
55
- if (pkg) {
56
- return "node";
57
- }
58
- // No package.json
59
- return "vanilla";
60
- }
61
- export function getProjectLabel(type) {
62
- const labels = {
63
- nextjs: "Next.js",
64
- react: "React",
65
- express: "Express/Fastify",
66
- nestjs: "NestJS",
67
- vue: "Vue/Nuxt",
68
- sveltekit: "SvelteKit",
69
- node: "Node.js",
70
- vanilla: "JavaScript/TypeScript",
71
- };
72
- return labels[type];
73
- }
@@ -1,2 +0,0 @@
1
- import type { AdapterContext } from "../adapters/types.js";
2
- export declare function formatAIOptimized(data: AdapterContext): string;
@@ -1,215 +0,0 @@
1
- export function formatAIOptimized(data) {
2
- const lines = [];
3
- lines.push(`# Project Context (${data.projectType})`);
4
- lines.push("");
5
- // 1. Routes
6
- if (data.routes && data.routes.length > 0) {
7
- const routeGroups = parseRouteGroups(data.routes);
8
- // Filter out empty shell routes
9
- const filteredGroups = new Map();
10
- for (const [key, value] of routeGroups) {
11
- if (value.length > 0) {
12
- filteredGroups.set(key, value);
13
- }
14
- }
15
- lines.push(`## Routes (${filteredGroups.size})`);
16
- for (const [group, paths] of filteredGroups) {
17
- // Max 4 routes, no "+N more"
18
- const displayPaths = paths.slice(0, 4);
19
- lines.push(`/${group} → ${displayPaths.join(", ")}`);
20
- }
21
- lines.push("");
22
- }
23
- // 2. Core Domains
24
- const features = getSectionsByPattern(data.sections, "features");
25
- if (features.size > 0) {
26
- lines.push(`## Core Domains (${features.size})`);
27
- for (const [name, files] of features) {
28
- lines.push(formatDomainLine(name, files));
29
- }
30
- lines.push("");
31
- }
32
- // 3. Auth & Session
33
- const authFiles = getAuthFiles(data.sections);
34
- if (authFiles.length > 0) {
35
- lines.push("## Auth & Session");
36
- lines.push("sign-in, session handling, token management");
37
- lines.push("");
38
- }
39
- // 4. Shared Lib
40
- const lib = getSectionsByPattern(data.sections, "lib");
41
- if (lib.size > 0 || data.sections.has("LIB") || data.sections.has("Lib")) {
42
- lines.push("## Shared Lib");
43
- lines.push("utils — formatting, helpers");
44
- const config = getSectionsByPattern(data.sections, "config");
45
- if (config.size > 0) {
46
- lines.push("config — api, uploads, pricing");
47
- }
48
- lines.push("");
49
- }
50
- // 5. Hooks
51
- const hooks = data.sections.get("HOOKS") || data.sections.get("Hooks");
52
- if (hooks && hooks.length > 0) {
53
- const hookNames = hooks
54
- .flatMap((f) => f.functions)
55
- .filter((n) => n.startsWith("use"));
56
- lines.push(`## Hooks (${hookNames.length})`);
57
- lines.push(hookNames.join(", "));
58
- lines.push("");
59
- }
60
- // 6. UI Layer
61
- const components = getSectionsByPattern(data.sections, "components");
62
- if (components.size > 0) {
63
- const totalComponents = countTotalFiles(components);
64
- lines.push("## UI Layer");
65
- const folders = [...components.keys()]
66
- .map((k) => k.split("/").pop()?.toLowerCase())
67
- .filter(Boolean);
68
- const uniqueFolders = [...new Set(folders)];
69
- lines.push(`~${totalComponents} components (${uniqueFolders.join(", ")})`);
70
- lines.push("");
71
- }
72
- return lines.join("\n");
73
- }
74
- // === Route Parsing ===
75
- function parseRouteGroups(routes) {
76
- const groups = new Map();
77
- let currentTopLevel = "";
78
- let currentDynamic = "";
79
- for (const route of routes) {
80
- const trimmed = route.trim();
81
- // Skip group markers like (auth), (website)
82
- if (trimmed.startsWith("(") && trimmed.endsWith(")")) {
83
- continue;
84
- }
85
- // Calculate depth by counting leading spaces
86
- const depth = route.search(/\S/);
87
- // Top-level route
88
- if (depth === 0) {
89
- currentTopLevel = trimmed;
90
- currentDynamic = "";
91
- if (!groups.has(currentTopLevel)) {
92
- groups.set(currentTopLevel, []);
93
- }
94
- }
95
- // First-level dynamic segment like [slug]
96
- else if (depth === 2 && trimmed.startsWith("[") && trimmed.endsWith("]")) {
97
- currentDynamic = trimmed;
98
- const dynamicKey = `${currentTopLevel}/${currentDynamic}`;
99
- if (!groups.has(dynamicKey)) {
100
- groups.set(dynamicKey, []);
101
- }
102
- }
103
- // Child routes
104
- else if (depth >= 2) {
105
- // Skip dynamic segments and technical routes
106
- if (isSkippableRoute(trimmed)) {
107
- continue;
108
- }
109
- // Add to dynamic group if exists, otherwise to top level
110
- if (currentDynamic && depth > 2) {
111
- const dynamicKey = `${currentTopLevel}/${currentDynamic}`;
112
- const children = groups.get(dynamicKey);
113
- if (children && !children.includes(trimmed)) {
114
- children.push(trimmed);
115
- }
116
- }
117
- else if (currentTopLevel) {
118
- const children = groups.get(currentTopLevel);
119
- if (children && !children.includes(trimmed)) {
120
- children.push(trimmed);
121
- }
122
- }
123
- }
124
- }
125
- // Clean up empty groups and dynamic groups with no children
126
- const cleaned = new Map();
127
- for (const [key, value] of groups) {
128
- // Keep if has children OR is a standalone route
129
- if (value.length > 0 || !key.includes("/")) {
130
- cleaned.set(key, value);
131
- }
132
- }
133
- return cleaned;
134
- }
135
- function isSkippableRoute(route) {
136
- const skipPatterns = [
137
- "[",
138
- "]",
139
- "error",
140
- "sync",
141
- "verify-email",
142
- "success",
143
- "...nextauth",
144
- ];
145
- const lower = route.toLowerCase();
146
- return skipPatterns.some((p) => lower.includes(p));
147
- }
148
- // === Section Helpers ===
149
- function getSectionsByPattern(sections, pattern) {
150
- const result = new Map();
151
- for (const [key, value] of sections) {
152
- if (key.toLowerCase().includes(pattern.toLowerCase())) {
153
- result.set(key, value);
154
- }
155
- }
156
- return result;
157
- }
158
- function getAuthFiles(sections) {
159
- for (const [key, value] of sections) {
160
- if (key.toLowerCase().includes("auth")) {
161
- return value;
162
- }
163
- }
164
- return [];
165
- }
166
- function countTotalFiles(sections) {
167
- let count = 0;
168
- for (const files of sections.values()) {
169
- count += files.length;
170
- }
171
- return count;
172
- }
173
- // === Domain Formatting ===
174
- function formatDomainLine(name, files) {
175
- const cleanName = name
176
- .replace(/features\//i, "")
177
- .replace(/FEATURES\//i, "")
178
- .toLowerCase();
179
- const actionFiles = files.filter((f) => f.fileName.includes("action") || f.fileName.includes("actions"));
180
- const actionCount = actionFiles.reduce((sum, f) => sum + f.functions.length, 0);
181
- const intent = getIntent(cleanName, files);
182
- return `${cleanName} — ${intent} (${actionCount} actions)`;
183
- }
184
- function getIntent(domain, files) {
185
- const allFunctions = files
186
- .flatMap((f) => f.functions)
187
- .join(" ")
188
- .toLowerCase();
189
- // Domain-specific intent mapping
190
- if (domain === "users") {
191
- return "authenticate, edit profile, manage credentials";
192
- }
193
- if (domain === "orders") {
194
- return "create/edit/cancel";
195
- }
196
- if (domain === "estimates") {
197
- return "create/edit/convert";
198
- }
199
- if (domain === "files") {
200
- return "upload/download";
201
- }
202
- // Fallback: derive from function names
203
- const intents = [];
204
- if (allFunctions.includes("create"))
205
- intents.push("create");
206
- if (allFunctions.includes("edit") || allFunctions.includes("update")) {
207
- intents.push("edit");
208
- }
209
- if (allFunctions.includes("delete") || allFunctions.includes("cancel")) {
210
- intents.push("cancel");
211
- }
212
- if (intents.length === 0)
213
- return "manage";
214
- return intents.slice(0, 3).join("/");
215
- }
@@ -1,2 +0,0 @@
1
- import type { OrganizedContext } from "../types.js";
2
- export declare function formatAI(ctx: OrganizedContext): string;
@@ -1,51 +0,0 @@
1
- export function formatAI(ctx) {
2
- const lines = ["# Codebase Context", ""];
3
- if (ctx.routes.length) {
4
- lines.push("## Routes", ...ctx.routes.map((r) => `- ${r}`), "");
5
- }
6
- if (ctx.components.length) {
7
- lines.push("## Components");
8
- for (const f of ctx.components) {
9
- const exp = f.exports.components.join(", ");
10
- if (exp) {
11
- lines.push(`- ${f.name}: ${exp}`);
12
- }
13
- }
14
- lines.push("");
15
- }
16
- if (ctx.hooks.length) {
17
- lines.push("## Hooks");
18
- for (const f of ctx.hooks) {
19
- const hookFns = f.exports.functions.filter((fn) => fn.startsWith("use"));
20
- if (hookFns.length) {
21
- lines.push(`- ${hookFns.join(", ")}`);
22
- }
23
- }
24
- lines.push("");
25
- }
26
- if (ctx.utils.length) {
27
- lines.push("## Utils");
28
- for (const f of ctx.utils) {
29
- const exp = [...f.exports.functions, ...f.exports.constants].join(", ");
30
- if (exp) {
31
- lines.push(`- ${f.name}: ${exp}`);
32
- }
33
- }
34
- lines.push("");
35
- }
36
- if (ctx.other.length) {
37
- lines.push("## Other");
38
- for (const f of ctx.other) {
39
- const exp = [
40
- ...f.exports.functions,
41
- ...f.exports.constants,
42
- ...f.exports.components,
43
- ].join(", ");
44
- if (exp) {
45
- lines.push(`- ${f.name}: ${exp}`);
46
- }
47
- }
48
- lines.push("");
49
- }
50
- return lines.join("\n");
51
- }
@@ -1,2 +0,0 @@
1
- import type { OrganizedContext } from "../types.js";
2
- export declare function formatHuman(ctx: OrganizedContext): string;
@@ -1,53 +0,0 @@
1
- export function formatHuman(ctx) {
2
- const lines = [
3
- "╭─────────────────────────────────────╮",
4
- "│ CODEBASE OVERVIEW │",
5
- "╰─────────────────────────────────────╯",
6
- "",
7
- ];
8
- if (ctx.routes.length) {
9
- lines.push("📍 ROUTES", "");
10
- for (const r of ctx.routes) {
11
- lines.push(` ${r}`);
12
- }
13
- lines.push("");
14
- }
15
- if (ctx.components.length) {
16
- lines.push(`🧩 COMPONENTS (${ctx.components.length} files)`, "");
17
- for (const f of ctx.components.slice(0, 10)) {
18
- if (f.exports.components.length) {
19
- lines.push(` ${f.name}`);
20
- for (const c of f.exports.components.slice(0, 3)) {
21
- lines.push(` └─ ${c}`);
22
- }
23
- }
24
- }
25
- if (ctx.components.length > 10) {
26
- lines.push(` ... and ${ctx.components.length - 10} more`);
27
- }
28
- lines.push("");
29
- }
30
- if (ctx.hooks.length) {
31
- const allHooks = ctx.hooks.flatMap((f) => f.exports.functions.filter((fn) => fn.startsWith("use")));
32
- if (allHooks.length) {
33
- lines.push(`🪝 HOOKS (${allHooks.length})`, "");
34
- for (const hook of allHooks) {
35
- lines.push(` • ${hook}()`);
36
- }
37
- lines.push("");
38
- }
39
- }
40
- if (ctx.utils.length) {
41
- lines.push(`🔧 UTILITIES`, "");
42
- for (const f of ctx.utils.slice(0, 10)) {
43
- if (f.exports.functions.length) {
44
- lines.push(` ${f.name}`);
45
- for (const fn of f.exports.functions.slice(0, 3)) {
46
- lines.push(` • ${fn}()`);
47
- }
48
- }
49
- }
50
- lines.push("");
51
- }
52
- return lines.join("\n");
53
- }
@@ -1,2 +0,0 @@
1
- export { formatAI } from "./ai.js";
2
- export { formatHuman } from "./human.js";
@@ -1,2 +0,0 @@
1
- export { formatAI } from "./ai.js";
2
- export { formatHuman } from "./human.js";
@@ -1,2 +0,0 @@
1
- import type { AdapterContext } from "../adapters/types.js";
2
- export declare function formatMarkdown(data: AdapterContext): string;
@@ -1,77 +0,0 @@
1
- export function formatMarkdown(data) {
2
- let output = "";
3
- // Project type header
4
- output += `📦 Project: ${data.projectType}\n\n`;
5
- // Routes section (if exists)
6
- if (data.routes && data.routes.length > 0) {
7
- output += `=== ROUTES ===\n\n`;
8
- for (const route of data.routes) {
9
- output += `${route}\n`;
10
- }
11
- output += "\n";
12
- }
13
- // All sections
14
- for (const [sectionName, files] of data.sections) {
15
- if (sectionName === "_root")
16
- continue;
17
- if (files.length === 0)
18
- continue;
19
- // Check if any file has exports
20
- const hasExports = files.some((f) => f.functions.length > 0 ||
21
- f.constants.length > 0 ||
22
- f.types.length > 0 ||
23
- f.interfaces.length > 0 ||
24
- f.classes.length > 0);
25
- if (!hasExports)
26
- continue;
27
- output += `=== ${sectionName.toUpperCase()} ===\n\n`;
28
- for (const file of files) {
29
- output += formatFile(file);
30
- }
31
- output += "\n";
32
- }
33
- // Root files last
34
- const rootFiles = data.sections.get("_root");
35
- if (rootFiles && rootFiles.length > 0) {
36
- const hasRootExports = rootFiles.some((f) => f.functions.length > 0 ||
37
- f.constants.length > 0 ||
38
- f.types.length > 0 ||
39
- f.interfaces.length > 0 ||
40
- f.classes.length > 0);
41
- if (hasRootExports) {
42
- output += `=== ROOT FILES ===\n\n`;
43
- for (const file of rootFiles) {
44
- output += formatFile(file);
45
- }
46
- output += "\n";
47
- }
48
- }
49
- output += `=== DONE ===\n`;
50
- return output;
51
- }
52
- function formatFile(file) {
53
- const hasExports = file.functions.length > 0 ||
54
- file.constants.length > 0 ||
55
- file.types.length > 0 ||
56
- file.interfaces.length > 0 ||
57
- file.classes.length > 0;
58
- if (!hasExports)
59
- return "";
60
- let output = `${file.fileName}\n`;
61
- for (const fn of file.functions) {
62
- output += `• function: ${fn}\n`;
63
- }
64
- for (const constant of file.constants) {
65
- output += `• constant: ${constant}\n`;
66
- }
67
- for (const type of file.types) {
68
- output += `• type: ${type}\n`;
69
- }
70
- for (const iface of file.interfaces) {
71
- output += `• interface: ${iface}\n`;
72
- }
73
- for (const cls of file.classes) {
74
- output += `• class: ${cls}\n`;
75
- }
76
- return output;
77
- }
@@ -1,2 +0,0 @@
1
- import type { AdapterContext } from "../adapters/types.js";
2
- export declare function formatRaw(data: AdapterContext): string;
@@ -1,47 +0,0 @@
1
- export function formatRaw(data) {
2
- const lines = [];
3
- lines.push(`# ${data.projectType} Project Context`);
4
- lines.push("");
5
- // Routes
6
- if (data.routes && data.routes.length > 0) {
7
- lines.push("## Routes");
8
- lines.push("");
9
- for (const route of data.routes) {
10
- lines.push(`- ${route}`);
11
- }
12
- lines.push("");
13
- }
14
- // Sections
15
- for (const [sectionName, files] of data.sections) {
16
- if (sectionName === "_root")
17
- continue;
18
- if (files.length === 0)
19
- continue;
20
- lines.push(`## ${sectionName}`);
21
- lines.push("");
22
- for (const file of files) {
23
- const exports = getExportsSummary(file);
24
- if (exports) {
25
- lines.push(`- ${file.fileName}: ${exports}`);
26
- }
27
- }
28
- lines.push("");
29
- }
30
- return lines.join("\n");
31
- }
32
- function getExportsSummary(file) {
33
- const parts = [];
34
- if (file.functions.length > 0) {
35
- parts.push(file.functions.map((f) => `${f}()`).join(", "));
36
- }
37
- if (file.constants.length > 0) {
38
- parts.push(file.constants.join(", "));
39
- }
40
- if (file.types.length > 0) {
41
- parts.push(file.types.map((t) => `type ${t}`).join(", "));
42
- }
43
- if (file.interfaces.length > 0) {
44
- parts.push(file.interfaces.map((i) => `interface ${i}`).join(", "));
45
- }
46
- return parts.join(" | ");
47
- }
@@ -1,2 +0,0 @@
1
- import type { ParsedFile, OrganizedContext } from "./types.js";
2
- export declare function organize(files: ParsedFile[], baseDir: string): Promise<OrganizedContext>;
package/dist/organizer.js DELETED
@@ -1,70 +0,0 @@
1
- import path from "path";
2
- import fs from "fs/promises";
3
- export async function organize(files, baseDir) {
4
- const routes = await getRoutes(baseDir);
5
- const components = [];
6
- const hooks = [];
7
- const utils = [];
8
- const other = [];
9
- for (const file of files) {
10
- const rel = file.path.toLowerCase();
11
- const hasHooks = file.exports.functions.some((fn) => fn.startsWith("use"));
12
- if (hasHooks || file.name.startsWith("use") || rel.includes("/hooks/")) {
13
- hooks.push(file);
14
- }
15
- else if (rel.includes("/components/") || rel.includes("/ui/")) {
16
- components.push(file);
17
- }
18
- else if (rel.includes("/lib/") ||
19
- rel.includes("/utils/") ||
20
- rel.includes("/helpers/")) {
21
- utils.push(file);
22
- }
23
- else if (!rel.includes("/app/") && !rel.includes("/pages/")) {
24
- other.push(file);
25
- }
26
- }
27
- return { routes, components, hooks, utils, other };
28
- }
29
- async function getRoutes(baseDir) {
30
- const possibleDirs = [
31
- path.join(baseDir, "app"),
32
- path.join(baseDir, "src", "app"),
33
- path.join(baseDir, "pages"),
34
- path.join(baseDir, "src", "pages"),
35
- ];
36
- for (const dir of possibleDirs) {
37
- try {
38
- await fs.access(dir);
39
- return walkRoutes(dir, "");
40
- }
41
- catch {
42
- continue;
43
- }
44
- }
45
- return [];
46
- }
47
- async function walkRoutes(dir, prefix) {
48
- const routes = [];
49
- let entries;
50
- try {
51
- entries = await fs.readdir(dir, { withFileTypes: true });
52
- }
53
- catch {
54
- return routes;
55
- }
56
- for (const entry of entries) {
57
- if (!entry.isDirectory())
58
- continue;
59
- if (entry.name.startsWith("_"))
60
- continue;
61
- if (entry.name.startsWith("("))
62
- continue;
63
- if (entry.name === "api")
64
- continue;
65
- const route = prefix + "/" + entry.name;
66
- routes.push(route);
67
- routes.push(...(await walkRoutes(path.join(dir, entry.name), route)));
68
- }
69
- return routes;
70
- }