@eloquence98/ctx 0.1.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 ADDED
@@ -0,0 +1,138 @@
1
+ # ctx
2
+
3
+ Generate AI-ready context from your codebase in seconds.
4
+
5
+ `ctx` scans a project directory and produces a clean, structured overview of folders and exported symbols. The output is designed to be pasted directly into AI tools to give them accurate context about your codebase.
6
+
7
+ ---
8
+
9
+ ## Why ctx
10
+
11
+ AI assistants do not understand your project unless you explain it.
12
+
13
+ Without ctx, you usually have to:
14
+
15
+ - Manually copy files
16
+ - Describe folder structure
17
+ - Explain what exists and how things connect
18
+
19
+ `ctx` automates this by generating a concise snapshot of your project with a single command.
20
+
21
+ ---
22
+
23
+ ## What It Does
24
+
25
+ - Recursively scans a directory
26
+ - Detects common project conventions
27
+ - Extracts exported symbols
28
+ - Produces readable, AI-friendly output
29
+ - Requires zero configuration
30
+
31
+ ---
32
+
33
+ ## Installation
34
+
35
+ ### No install (recommended)
36
+
37
+ ```bash
38
+ npx @eloquence98/ctx ./src
39
+ ```
40
+
41
+ ### Global install
42
+
43
+ ```bash
44
+ npm install -g @eloquence98/ctx
45
+ ```
46
+
47
+ ### Local install (dev dependency)
48
+
49
+ ```bash
50
+ npm install --save-dev @eloquence98/ctx
51
+ ```
52
+
53
+ ---
54
+
55
+ ## Usage
56
+
57
+ ### Scan a directory
58
+
59
+ ```bash
60
+ ctx ./src
61
+ ```
62
+
63
+ ### Scan current directory
64
+
65
+ ```bash
66
+ ctx .
67
+ ```
68
+
69
+ ### Output as JSON
70
+
71
+ ```bash
72
+ ctx ./src -o json
73
+ ```
74
+
75
+ ### Write output to a file
76
+
77
+ ```bash
78
+ ctx ./src > CONTEXT.md
79
+ ```
80
+
81
+ ---
82
+
83
+ ## What Gets Extracted
84
+
85
+ ### Code Symbols
86
+
87
+ - Functions
88
+ - Constants
89
+ - Types
90
+ - Interfaces
91
+
92
+ ### Project Structure
93
+
94
+ - Routes (App Router / Pages Router)
95
+ - Features or modules
96
+ - Components
97
+ - Hooks
98
+ - Utilities and libraries
99
+
100
+ Detection is convention-based and works with most modern JavaScript and TypeScript projects.
101
+
102
+ ---
103
+
104
+ ## Use Cases
105
+
106
+ - Providing context to ChatGPT, Claude, or Copilot
107
+ - Generating architecture documentation
108
+ - Onboarding new team members
109
+ - Understanding unfamiliar codebases
110
+ - Preparing projects for AI-assisted refactors
111
+
112
+ ---
113
+
114
+ ## Output Philosophy
115
+
116
+ The output is:
117
+
118
+ - Minimal
119
+ - Structured
120
+ - Human-readable
121
+ - Optimized for AI comprehension
122
+
123
+ Only high-signal information is included.
124
+
125
+ ---
126
+
127
+ ## Roadmap
128
+
129
+ - Clipboard support (`--copy`)
130
+ - Config file support
131
+ - VS Code extension
132
+ - Watch mode
133
+
134
+ ---
135
+
136
+ ## License
137
+
138
+ MIT
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,107 @@
1
+ #!/usr/bin/env node
2
+ import { program } from "commander";
3
+ import path from "path";
4
+ import fs from "fs/promises";
5
+ import { scanDirectory, getRoutes } from "./scanner.js";
6
+ import { parseFile } from "./parser.js";
7
+ import { formatMarkdown } from "./formatters/index.js";
8
+ program
9
+ .name("ctx")
10
+ .description("Generate AI-ready context from your codebase")
11
+ .version("0.1.0")
12
+ .argument("[path]", "Path to scan", "./src")
13
+ .option("-o, --output <format>", "Output format: md, json", "md")
14
+ .action(async (targetPath, options) => {
15
+ const absolutePath = path.resolve(process.cwd(), targetPath);
16
+ console.log(`\n📁 Scanning ${absolutePath}...\n`);
17
+ try {
18
+ // Scan all files
19
+ const files = await scanDirectory(absolutePath);
20
+ if (files.length === 0) {
21
+ console.log("No files found. Check your path.");
22
+ process.exit(1);
23
+ }
24
+ // Parse all files
25
+ const parsedFiles = await Promise.all(files.map((file) => parseFile(file)));
26
+ // Get routes - try to find app directory
27
+ let routes = [];
28
+ const appDir = await findAppDirectory(absolutePath);
29
+ if (appDir) {
30
+ routes = await getRoutes(appDir);
31
+ }
32
+ // Build context
33
+ const context = {
34
+ routes,
35
+ features: groupByFolder(parsedFiles, absolutePath, "features"),
36
+ hooks: getHooks(parsedFiles),
37
+ lib: groupByFolder(parsedFiles, absolutePath, "lib"),
38
+ components: groupByFolder(parsedFiles, absolutePath, "components"),
39
+ };
40
+ // Output
41
+ if (options.output === "json") {
42
+ console.log(JSON.stringify(contextToJSON(context), null, 2));
43
+ }
44
+ else {
45
+ console.log(formatMarkdown(context, absolutePath));
46
+ }
47
+ }
48
+ catch (error) {
49
+ console.error("Error:", error);
50
+ process.exit(1);
51
+ }
52
+ });
53
+ async function findAppDirectory(basePath) {
54
+ const possiblePaths = [
55
+ path.join(basePath, "app"),
56
+ path.join(basePath, "src", "app"),
57
+ path.join(basePath, "src/app"),
58
+ ];
59
+ for (const p of possiblePaths) {
60
+ try {
61
+ const stat = await fs.stat(p);
62
+ if (stat.isDirectory()) {
63
+ return p;
64
+ }
65
+ }
66
+ catch {
67
+ // Path doesn't exist, try next
68
+ }
69
+ }
70
+ return null;
71
+ }
72
+ function groupByFolder(files, basePath, folderName) {
73
+ const grouped = new Map();
74
+ for (const file of files) {
75
+ const relativePath = path.relative(basePath, file.filePath);
76
+ const parts = relativePath.split(path.sep);
77
+ const folderIndex = parts.indexOf(folderName);
78
+ if (folderIndex === -1)
79
+ continue;
80
+ const remainingParts = parts.slice(folderIndex + 1);
81
+ // Need at least 2 parts: subfolder + filename
82
+ if (remainingParts.length < 2)
83
+ continue;
84
+ const subFolder = remainingParts[0];
85
+ if (!grouped.has(subFolder)) {
86
+ grouped.set(subFolder, []);
87
+ }
88
+ grouped.get(subFolder).push(file);
89
+ }
90
+ return grouped;
91
+ }
92
+ function getHooks(files) {
93
+ return files.filter((f) => f.filePath.includes("/hooks/") ||
94
+ f.filePath.includes("\\hooks\\") ||
95
+ f.fileName.startsWith("use-") ||
96
+ f.fileName.startsWith("use."));
97
+ }
98
+ function contextToJSON(context) {
99
+ return {
100
+ routes: context.routes,
101
+ features: Object.fromEntries(context.features),
102
+ hooks: context.hooks,
103
+ lib: Object.fromEntries(context.lib),
104
+ components: Object.fromEntries(context.components),
105
+ };
106
+ }
107
+ program.parse();
@@ -0,0 +1,9 @@
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 ADDED
@@ -0,0 +1,24 @@
1
+ export const defaultConfig = {
2
+ entry: "./src",
3
+ extensions: [".ts", ".tsx", ".js", ".jsx"],
4
+ ignore: [
5
+ "node_modules",
6
+ ".git",
7
+ "dist",
8
+ "build",
9
+ ".next",
10
+ "*.test.*",
11
+ "*.spec.*",
12
+ "__tests__",
13
+ "*.d.ts",
14
+ ".DS_Store",
15
+ ],
16
+ maxDepth: 10,
17
+ };
18
+ export const folderCategories = {
19
+ routes: ["app", "pages"],
20
+ features: ["features", "modules", "domains"],
21
+ hooks: ["hooks"],
22
+ lib: ["lib", "utils", "helpers", "services"],
23
+ components: ["components", "ui"],
24
+ };
@@ -0,0 +1 @@
1
+ export { formatMarkdown } from "./markdown.js";
@@ -0,0 +1 @@
1
+ export { formatMarkdown } from "./markdown.js";
@@ -0,0 +1,2 @@
1
+ import type { ProjectContext } from "../types.js";
2
+ export declare function formatMarkdown(data: ProjectContext, basePath: string): string;
@@ -0,0 +1,73 @@
1
+ export function formatMarkdown(data, basePath) {
2
+ let output = "";
3
+ // Routes section
4
+ if (data.routes.length > 0) {
5
+ output += `=== ROUTES (src/app) ===\n\n`;
6
+ for (const route of data.routes) {
7
+ output += `${route}\n`;
8
+ }
9
+ output += "\n";
10
+ }
11
+ // Features section
12
+ if (data.features.size > 0) {
13
+ output += `=== FEATURES ===\n\n`;
14
+ output += formatFolderGroup(data.features);
15
+ }
16
+ // Components section
17
+ if (data.components.size > 0) {
18
+ output += `=== COMPONENTS ===\n\n`;
19
+ output += formatFolderGroup(data.components);
20
+ }
21
+ // Hooks section
22
+ if (data.hooks.length > 0) {
23
+ output += `=== HOOKS ===\n\n`;
24
+ for (const file of data.hooks) {
25
+ output += formatSingleFile(file);
26
+ }
27
+ output += "\n";
28
+ }
29
+ // Lib section
30
+ if (data.lib.size > 0) {
31
+ output += `=== LIB ===\n\n`;
32
+ output += formatFolderGroup(data.lib);
33
+ }
34
+ output += `=== DONE ===\n`;
35
+ return output;
36
+ }
37
+ function formatFolderGroup(folders) {
38
+ let output = "";
39
+ for (const [folderName, files] of folders) {
40
+ output += `${folderName}/\n`;
41
+ for (const file of files) {
42
+ output += formatSingleFile(file);
43
+ }
44
+ output += "\n";
45
+ }
46
+ return output;
47
+ }
48
+ function formatSingleFile(file) {
49
+ const hasExports = file.functions.length > 0 ||
50
+ file.constants.length > 0 ||
51
+ file.types.length > 0 ||
52
+ file.interfaces.length > 0 ||
53
+ file.classes.length > 0;
54
+ if (!hasExports)
55
+ return "";
56
+ let output = `${file.fileName}\n`;
57
+ for (const fn of file.functions) {
58
+ output += `• function: ${fn}\n`;
59
+ }
60
+ for (const constant of file.constants) {
61
+ output += `• constant: ${constant}\n`;
62
+ }
63
+ for (const type of file.types) {
64
+ output += `• type: ${type}\n`;
65
+ }
66
+ for (const iface of file.interfaces) {
67
+ output += `• interface: ${iface}\n`;
68
+ }
69
+ for (const cls of file.classes) {
70
+ output += `• class: ${cls}\n`;
71
+ }
72
+ return output;
73
+ }
@@ -0,0 +1,2 @@
1
+ import type { FileExports } from "./types.js";
2
+ export declare function parseFile(filePath: string): Promise<FileExports>;
package/dist/parser.js ADDED
@@ -0,0 +1,73 @@
1
+ import fs from "fs/promises";
2
+ import path from "path";
3
+ export async function parseFile(filePath) {
4
+ const content = await fs.readFile(filePath, "utf-8");
5
+ const fileName = path.basename(filePath);
6
+ return {
7
+ filePath,
8
+ fileName,
9
+ functions: extractFunctions(content),
10
+ constants: extractConstants(content),
11
+ types: extractTypes(content),
12
+ interfaces: extractInterfaces(content),
13
+ classes: extractClasses(content),
14
+ defaultExport: extractDefaultExport(content),
15
+ };
16
+ }
17
+ function extractFunctions(content) {
18
+ const functions = [];
19
+ // export function name
20
+ const funcMatches = content.matchAll(/export\s+(?:async\s+)?function\s+(\w+)/g);
21
+ for (const match of funcMatches) {
22
+ functions.push(match[1]);
23
+ }
24
+ // export const name = async? (...) => or function(
25
+ const arrowMatches = content.matchAll(/export\s+const\s+(\w+)\s*=\s*(?:async\s*)?(?:\([^)]*\)|[a-zA-Z_]\w*)\s*(?:=>|\{)/g);
26
+ for (const match of arrowMatches) {
27
+ if (!functions.includes(match[1])) {
28
+ functions.push(match[1]);
29
+ }
30
+ }
31
+ return functions;
32
+ }
33
+ function extractConstants(content) {
34
+ const constants = [];
35
+ // Match export const that are NOT functions
36
+ const lines = content.split("\n");
37
+ for (const line of lines) {
38
+ // export const NAME = value (not a function)
39
+ const match = line.match(/export\s+const\s+(\w+)\s*=\s*(?!(?:async\s*)?(?:\(|function|\w+\s*=>))/);
40
+ if (match) {
41
+ constants.push(match[1]);
42
+ }
43
+ // Also catch: export const NAME: Type =
44
+ const typedMatch = line.match(/export\s+const\s+(\w+)\s*:\s*[^=]+=\s*(?!(?:async\s*)?(?:\(|function|\w+\s*=>))/);
45
+ if (typedMatch && !constants.includes(typedMatch[1])) {
46
+ constants.push(typedMatch[1]);
47
+ }
48
+ }
49
+ return constants;
50
+ }
51
+ function extractTypes(content) {
52
+ const matches = content.matchAll(/export\s+type\s+(\w+)/g);
53
+ return [...matches].map((m) => m[1]);
54
+ }
55
+ function extractInterfaces(content) {
56
+ const matches = content.matchAll(/export\s+interface\s+(\w+)/g);
57
+ return [...matches].map((m) => m[1]);
58
+ }
59
+ function extractClasses(content) {
60
+ const matches = content.matchAll(/export\s+(?:default\s+)?class\s+(\w+)/g);
61
+ return [...matches].map((m) => m[1]);
62
+ }
63
+ function extractDefaultExport(content) {
64
+ // export default function Name
65
+ const funcMatch = content.match(/export\s+default\s+function\s+(\w+)/);
66
+ if (funcMatch)
67
+ return funcMatch[1];
68
+ // export default Name
69
+ const simpleMatch = content.match(/export\s+default\s+(\w+)/);
70
+ if (simpleMatch)
71
+ return simpleMatch[1];
72
+ return undefined;
73
+ }
@@ -0,0 +1,3 @@
1
+ import type { ScanOptions } from "./types.js";
2
+ export declare function scanDirectory(dir: string, options?: Partial<ScanOptions>): Promise<string[]>;
3
+ export declare function getRoutes(appDir: string): Promise<string[]>;
@@ -0,0 +1,73 @@
1
+ import fs from "fs/promises";
2
+ import path from "path";
3
+ import { defaultConfig } from "./config.js";
4
+ export async function scanDirectory(dir, options = {}) {
5
+ const config = { ...defaultConfig, ...options };
6
+ const files = [];
7
+ async function walk(currentDir, depth = 0) {
8
+ if (depth > config.maxDepth)
9
+ return;
10
+ let entries;
11
+ try {
12
+ entries = await fs.readdir(currentDir, { withFileTypes: true });
13
+ }
14
+ catch {
15
+ return;
16
+ }
17
+ for (const entry of entries) {
18
+ const fullPath = path.join(currentDir, entry.name);
19
+ // Check ignore patterns
20
+ const shouldIgnore = config.ignore.some((pattern) => {
21
+ if (pattern.includes("*")) {
22
+ const regex = new RegExp(pattern.replace(/\./g, "\\.").replace(/\*/g, ".*"));
23
+ return regex.test(entry.name);
24
+ }
25
+ return entry.name === pattern;
26
+ });
27
+ if (shouldIgnore)
28
+ continue;
29
+ if (entry.isDirectory()) {
30
+ await walk(fullPath, depth + 1);
31
+ }
32
+ else if (entry.isFile()) {
33
+ const ext = path.extname(entry.name);
34
+ if (config.extensions.includes(ext)) {
35
+ files.push(fullPath);
36
+ }
37
+ }
38
+ }
39
+ }
40
+ await walk(dir);
41
+ return files;
42
+ }
43
+ export async function getRoutes(appDir) {
44
+ const routes = [];
45
+ async function walkRoutes(dir, indent = "") {
46
+ let entries;
47
+ try {
48
+ entries = await fs.readdir(dir, { withFileTypes: true });
49
+ }
50
+ catch {
51
+ return;
52
+ }
53
+ // Sort: groups first (parentheses), then regular folders
54
+ const sorted = entries
55
+ .filter((e) => e.isDirectory())
56
+ .filter((e) => !e.name.startsWith("_"))
57
+ .sort((a, b) => {
58
+ const aIsGroup = a.name.startsWith("(");
59
+ const bIsGroup = b.name.startsWith("(");
60
+ if (aIsGroup && !bIsGroup)
61
+ return -1;
62
+ if (!aIsGroup && bIsGroup)
63
+ return 1;
64
+ return a.name.localeCompare(b.name);
65
+ });
66
+ for (const entry of sorted) {
67
+ routes.push(`${indent}${entry.name}`);
68
+ await walkRoutes(path.join(dir, entry.name), indent + " ");
69
+ }
70
+ }
71
+ await walkRoutes(appDir);
72
+ return routes;
73
+ }
@@ -0,0 +1,23 @@
1
+ export interface FileExports {
2
+ filePath: string;
3
+ fileName: string;
4
+ functions: string[];
5
+ constants: string[];
6
+ types: string[];
7
+ interfaces: string[];
8
+ classes: string[];
9
+ defaultExport?: string;
10
+ }
11
+ export interface ScanOptions {
12
+ entry: string;
13
+ extensions: string[];
14
+ ignore: string[];
15
+ maxDepth: number;
16
+ }
17
+ export interface ProjectContext {
18
+ routes: string[];
19
+ features: Map<string, FileExports[]>;
20
+ hooks: FileExports[];
21
+ lib: Map<string, FileExports[]>;
22
+ components: Map<string, FileExports[]>;
23
+ }
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@eloquence98/ctx",
3
+ "version": "0.1.0",
4
+ "description": "Generate AI-ready context from your codebase. One command, zero config.",
5
+ "type": "module",
6
+ "bin": {
7
+ "ctx": "./dist/cli.js"
8
+ },
9
+ "scripts": {
10
+ "build": "tsc",
11
+ "dev": "tsx src/cli.ts",
12
+ "test": "tsx src/cli.ts ."
13
+ },
14
+ "keywords": [
15
+ "ai",
16
+ "context",
17
+ "codebase",
18
+ "documentation",
19
+ "cli",
20
+ "typescript",
21
+ "nextjs",
22
+ "react"
23
+ ],
24
+ "author": "Eloquence98",
25
+ "license": "MIT",
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "https://github.com/Eloquence98/ctx.git"
29
+ },
30
+ "homepage": "https://github.com/Eloquence98/ctx",
31
+ "files": [
32
+ "dist"
33
+ ],
34
+ "devDependencies": {
35
+ "@types/node": "^20.0.0",
36
+ "tsx": "^4.0.0",
37
+ "typescript": "^5.0.0"
38
+ },
39
+ "dependencies": {
40
+ "commander": "^12.0.0",
41
+ "fast-glob": "^3.3.0"
42
+ }
43
+ }