@eloquence98/ctx 0.1.5 → 0.1.6

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/dist/cli.js CHANGED
@@ -1,48 +1,53 @@
1
1
  #!/usr/bin/env node
2
2
  import { program } from "commander";
3
- import path from "path";
4
3
  import fs from "fs/promises";
5
- import { scanDirectory } from "./scanner.js";
6
- import { parseFile } from "./parser.js";
7
- import { detectProject, getProjectLabel } from "./detectors/index.js";
4
+ import path from "path";
8
5
  import { getAdapter } from "./adapters/index.js";
9
- import { formatMarkdown } from "./formatters/markdown.js";
10
- import { formatAI } from "./formatters/ai.js";
6
+ import { detectProject, getProjectLabel } from "./detectors/index.js";
7
+ import { formatAIOptimized } from "./formatters/ai-optimized.js";
8
+ import { formatHuman } from "./formatters/human.js";
9
+ import { formatRaw } from "./formatters/raw.js";
10
+ import { parseFile } from "./parser.js";
11
+ import { scanDirectory } from "./scanner.js";
11
12
  program
12
13
  .name("ctx")
13
14
  .description("Generate AI-ready context from your codebase")
14
- .version("0.1.5")
15
+ .version("0.1.6")
15
16
  .argument("[path]", "Path to scan", ".")
16
- .option("-o, --output <format>", "Output format: md, json", "md")
17
- .option("--ai", "Output in AI-optimized compact format")
17
+ .option("--human", "Human-readable output for onboarding")
18
+ .option("--raw", "Verbose output with all details")
19
+ .option("-o, --output <format>", "Output format: json", "")
18
20
  .action(async (targetPath, options) => {
19
21
  const absolutePath = path.resolve(process.cwd(), targetPath);
20
- // Find project root and detect type
21
22
  const projectRoot = await findProjectRoot(absolutePath);
22
23
  const projectType = await detectProject(projectRoot);
23
24
  const adapter = getAdapter(projectType);
24
- console.log(`\nšŸ“ Scanning ${absolutePath}...`);
25
- console.log(`šŸ“¦ Detected: ${getProjectLabel(projectType)}\n`);
25
+ // Only show scanning message for human/raw modes
26
+ if (options.human || options.raw) {
27
+ console.log(`\nšŸ“ Scanning ${absolutePath}...`);
28
+ console.log(`šŸ“¦ Detected: ${getProjectLabel(projectType)}\n`);
29
+ }
26
30
  try {
27
- // Scan all files
28
31
  const files = await scanDirectory(absolutePath);
29
32
  if (files.length === 0) {
30
33
  console.log("No files found. Check your path.");
31
34
  process.exit(1);
32
35
  }
33
- // Parse all files
34
36
  const parsedFiles = await Promise.all(files.map((file) => parseFile(file)));
35
- // Use adapter to analyze
36
37
  const context = await adapter.analyze(absolutePath, parsedFiles);
37
- // Output
38
+ // Output based on flags
38
39
  if (options.output === "json") {
39
40
  console.log(JSON.stringify(contextToJSON(context), null, 2));
40
41
  }
41
- else if (options.ai) {
42
- console.log(formatAI(context));
42
+ else if (options.raw) {
43
+ console.log(formatRaw(context));
44
+ }
45
+ else if (options.human) {
46
+ console.log(formatHuman(context));
43
47
  }
44
48
  else {
45
- console.log(formatMarkdown(context));
49
+ // Default: AI-optimized
50
+ console.log(formatAIOptimized(context));
46
51
  }
47
52
  }
48
53
  catch (error) {
@@ -0,0 +1,2 @@
1
+ import type { AdapterContext } from "../adapters/types.js";
2
+ export declare function formatAIOptimized(data: AdapterContext): string;
@@ -0,0 +1,215 @@
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
+ }
@@ -0,0 +1,2 @@
1
+ import type { AdapterContext } from "../adapters/types.js";
2
+ export declare function formatHuman(data: AdapterContext): string;
@@ -0,0 +1,200 @@
1
+ export function formatHuman(data) {
2
+ const lines = [];
3
+ lines.push(`šŸ“ Project: ${data.projectType} App`);
4
+ lines.push("");
5
+ // Routes - ordered: client, admin, api
6
+ if (data.routes && data.routes.length > 0) {
7
+ lines.push("ā”Œā”€ Routes ─────────────────────────");
8
+ lines.push("│");
9
+ const routeGroups = parseRouteGroups(data.routes);
10
+ const orderedGroups = sortRouteGroups(routeGroups);
11
+ // Filter out empty groups
12
+ const filteredGroups = new Map();
13
+ for (const [key, value] of orderedGroups) {
14
+ if (value.length > 0) {
15
+ filteredGroups.set(key, value);
16
+ }
17
+ }
18
+ const groupKeys = [...filteredGroups.keys()];
19
+ for (let i = 0; i < groupKeys.length; i++) {
20
+ const group = groupKeys[i];
21
+ const children = filteredGroups.get(group) || [];
22
+ const isLast = i === groupKeys.length - 1;
23
+ const prefix = isLast ? "└──" : "ā”œā”€ā”€";
24
+ const childPrefix = isLast ? " " : "│ ";
25
+ const icon = getRouteIcon(group);
26
+ const displayGroup = group.split("/")[0];
27
+ lines.push(`${prefix} ${icon} ${capitalizeFirst(displayGroup)}`);
28
+ // Consolidate dynamic segments
29
+ const consolidatedChildren = consolidateRoutes(children);
30
+ // Show max 5 children
31
+ const visibleChildren = consolidatedChildren.slice(0, 5);
32
+ for (const child of visibleChildren) {
33
+ lines.push(`${childPrefix}/${child}`);
34
+ }
35
+ // Remaining as "(other X pages)"
36
+ if (consolidatedChildren.length > 5) {
37
+ const remaining = consolidatedChildren.length - 5;
38
+ const groupName = group.split("/")[0];
39
+ lines.push(`${childPrefix}(${remaining} more ${groupName} pages)`);
40
+ }
41
+ }
42
+ lines.push("");
43
+ }
44
+ // Domains
45
+ const features = getSectionsByPattern(data.sections, "features");
46
+ if (features.size > 0) {
47
+ lines.push("ā”Œā”€ Domains ────────────────────────");
48
+ lines.push("│");
49
+ const featureNames = [...features.keys()];
50
+ for (let i = 0; i < featureNames.length; i++) {
51
+ const name = featureNames[i];
52
+ const cleanName = name.replace(/features\//i, "").toLowerCase();
53
+ const icon = getDomainIcon(cleanName);
54
+ const isLast = i === featureNames.length - 1;
55
+ const prefix = isLast ? "└──" : "ā”œā”€ā”€";
56
+ lines.push(`${prefix} ${icon} features/${cleanName}`);
57
+ }
58
+ lines.push("");
59
+ }
60
+ // Infrastructure
61
+ lines.push("ā”Œā”€ Infrastructure ─────────────────");
62
+ lines.push("│");
63
+ lines.push("ā”œā”€ā”€ auth / session");
64
+ lines.push("ā”œā”€ā”€ shared utils");
65
+ lines.push("└── ui components");
66
+ lines.push("");
67
+ return lines.join("\n");
68
+ }
69
+ // === Route Parsing ===
70
+ function parseRouteGroups(routes) {
71
+ const groups = new Map();
72
+ let currentTopLevel = "";
73
+ let currentDynamic = "";
74
+ for (const route of routes) {
75
+ const trimmed = route.trim();
76
+ if (trimmed.startsWith("(") && trimmed.endsWith(")")) {
77
+ continue;
78
+ }
79
+ const depth = route.search(/\S/);
80
+ if (depth === 0) {
81
+ currentTopLevel = trimmed;
82
+ currentDynamic = "";
83
+ if (!groups.has(currentTopLevel)) {
84
+ groups.set(currentTopLevel, []);
85
+ }
86
+ }
87
+ else if (depth === 2 && trimmed.startsWith("[")) {
88
+ // Dynamic segment at first level - create combined key
89
+ currentDynamic = trimmed;
90
+ const dynamicKey = `${currentTopLevel}/${currentDynamic}`;
91
+ if (!groups.has(dynamicKey)) {
92
+ groups.set(dynamicKey, []);
93
+ }
94
+ }
95
+ else if (currentTopLevel) {
96
+ if (!isSkippableRoute(trimmed)) {
97
+ // Add to dynamic group if exists
98
+ if (currentDynamic && depth > 2) {
99
+ const dynamicKey = `${currentTopLevel}/${currentDynamic}`;
100
+ const children = groups.get(dynamicKey);
101
+ if (children && !children.includes(trimmed)) {
102
+ children.push(trimmed);
103
+ }
104
+ }
105
+ else {
106
+ const children = groups.get(currentTopLevel);
107
+ if (children && !children.includes(trimmed)) {
108
+ children.push(trimmed);
109
+ }
110
+ }
111
+ }
112
+ }
113
+ }
114
+ return groups;
115
+ }
116
+ function isSkippableRoute(route) {
117
+ const skip = ["error", "sync", "verify-email", "success", "...nextauth"];
118
+ const lower = route.toLowerCase();
119
+ return skip.some((p) => lower.includes(p));
120
+ }
121
+ function sortRouteGroups(groups) {
122
+ const sorted = new Map();
123
+ const order = ["client", "admin", "api"];
124
+ for (const key of order) {
125
+ for (const [group, children] of groups) {
126
+ if (group.toLowerCase().includes(key)) {
127
+ sorted.set(group, children);
128
+ }
129
+ }
130
+ }
131
+ for (const [group, children] of groups) {
132
+ if (!sorted.has(group)) {
133
+ sorted.set(group, children);
134
+ }
135
+ }
136
+ return sorted;
137
+ }
138
+ function consolidateRoutes(routes) {
139
+ const consolidated = [];
140
+ const seen = new Set();
141
+ for (const route of routes) {
142
+ // Skip dynamic segments
143
+ if (route.startsWith("[") && route.endsWith("]")) {
144
+ continue;
145
+ }
146
+ const baseName = route.toLowerCase();
147
+ if (!seen.has(baseName)) {
148
+ seen.add(baseName);
149
+ // Check if there's a dynamic child
150
+ const hasDynamic = routes.some((r) => r.startsWith("[") && r.toLowerCase().includes(baseName.slice(0, -1)));
151
+ if (hasDynamic) {
152
+ consolidated.push(`${route} (list, detail)`);
153
+ }
154
+ else {
155
+ consolidated.push(route);
156
+ }
157
+ }
158
+ }
159
+ return consolidated;
160
+ }
161
+ // === Helpers ===
162
+ function getSectionsByPattern(sections, pattern) {
163
+ const result = new Map();
164
+ for (const [key, value] of sections) {
165
+ if (key.toLowerCase().includes(pattern.toLowerCase())) {
166
+ result.set(key, value);
167
+ }
168
+ }
169
+ return result;
170
+ }
171
+ function getRouteIcon(group) {
172
+ const lower = group.toLowerCase();
173
+ if (lower.includes("client") || lower.includes("user"))
174
+ return "šŸ‘¤";
175
+ if (lower.includes("admin"))
176
+ return "šŸ› ļø";
177
+ if (lower.includes("api"))
178
+ return "šŸ”Œ";
179
+ if (lower.includes("auth") || lower.includes("login"))
180
+ return "šŸ”";
181
+ return "šŸ“„";
182
+ }
183
+ function getDomainIcon(name) {
184
+ if (name.includes("order"))
185
+ return "šŸ“¦";
186
+ if (name.includes("estimate"))
187
+ return "šŸ“‹";
188
+ if (name.includes("file"))
189
+ return "šŸ“";
190
+ if (name.includes("user"))
191
+ return "šŸ‘¤";
192
+ if (name.includes("auth"))
193
+ return "šŸ”";
194
+ if (name.includes("payment"))
195
+ return "šŸ’³";
196
+ return "šŸ“‚";
197
+ }
198
+ function capitalizeFirst(str) {
199
+ return str.charAt(0).toUpperCase() + str.slice(1);
200
+ }
@@ -1,2 +1,4 @@
1
1
  export { formatMarkdown } from "./markdown.js";
2
- export { formatAI } from "./ai.js";
2
+ export { formatAIOptimized } from "./ai-optimized.js";
3
+ export { formatRaw } from "./raw.js";
4
+ export { formatHuman } from "./human.js";
@@ -1,2 +1,4 @@
1
1
  export { formatMarkdown } from "./markdown.js";
2
- export { formatAI } from "./ai.js";
2
+ export { formatAIOptimized } from "./ai-optimized.js";
3
+ export { formatRaw } from "./raw.js";
4
+ export { formatHuman } from "./human.js";
@@ -0,0 +1,2 @@
1
+ import type { AdapterContext } from "../adapters/types.js";
2
+ export declare function formatRaw(data: AdapterContext): string;
@@ -0,0 +1,47 @@
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eloquence98/ctx",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "description": "Generate AI-ready context from your codebase. One command, zero config.",
5
5
  "type": "module",
6
6
  "bin": {