@chappibunny/repolens 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,312 @@
1
+ // Strict prompt templates for AI-generated documentation sections
2
+
3
+ export const SYSTEM_PROMPT = `You are a senior software architect and technical writer.
4
+ Your job is to turn structured repository analysis into clear documentation.
5
+
6
+ Rules:
7
+ - Use only the supplied context.
8
+ - Never invent files, modules, routes, APIs, or business capabilities.
9
+ - If context is insufficient, say so briefly and continue with what is known.
10
+ - Write for mixed audiences when requested.
11
+ - Prefer clear prose and short sections over bullet spam.
12
+ - Be concrete and practical.
13
+ - Output valid markdown only.
14
+ - Do not mention AI, LLMs, or that you are an assistant.
15
+ - No markdown tables unless specifically requested.
16
+ - Use simple formatting: headings, paragraphs, lists.
17
+ - Maximum 2 heading levels deep within sections.`;
18
+
19
+ export function createExecutiveSummaryPrompt(context) {
20
+ return `Write an executive summary for a mixed audience of technical and non-technical readers.
21
+
22
+ Use this context:
23
+ ${JSON.stringify(context, null, 2)}
24
+
25
+ Requirements:
26
+ - Explain what the system appears to do based on the modules and routes.
27
+ - Explain the main system areas using the domain information.
28
+ - Explain the business capabilities implied by the codebase structure.
29
+ - Mention key external dependencies only if they are present in the context.
30
+ - Mention architectural or operational risks if they are strongly supported by the context.
31
+ - Do not mention file counts more than once.
32
+ - Maximum 500 words.
33
+ - Use this structure:
34
+
35
+ # Executive Summary
36
+
37
+ ## What this system does
38
+
39
+ ## Who it serves
40
+
41
+ ## Core capabilities
42
+
43
+ ## Main system areas
44
+
45
+ ## Key dependencies
46
+
47
+ ## Operational and architectural risks
48
+
49
+ ## Recommended focus areas`;
50
+ }
51
+
52
+ export function createSystemOverviewPrompt(context) {
53
+ return `Write a system overview for a mixed audience.
54
+
55
+ Use this context:
56
+ ${JSON.stringify(context, null, 2)}
57
+
58
+ Requirements:
59
+ - Provide a concise, high-level orientation to the codebase.
60
+ - Technical enough for developers, readable enough for everyone else.
61
+ - Focus on what is observable from the structure.
62
+ - Maximum 400 words.
63
+ - Use this structure:
64
+
65
+ # System Overview
66
+
67
+ ## Repository snapshot
68
+
69
+ ## Main architectural layers
70
+
71
+ ## Dominant domains
72
+
73
+ ## Main technology patterns
74
+
75
+ ## Where most logic lives
76
+
77
+ ## Key observations`;
78
+ }
79
+
80
+ export function createBusinessDomainsPrompt(context) {
81
+ return `Write business domain documentation for a mixed audience, especially non-technical readers.
82
+
83
+ Use this context:
84
+ ${JSON.stringify(context, null, 2)}
85
+
86
+ Requirements:
87
+ - Translate codebase structure into business language.
88
+ - For each domain in the context, explain:
89
+ - Business responsibility
90
+ - Main directories/modules
91
+ - Dependencies
92
+ - User-visible functionality (inferred from structure)
93
+ - Maximum 150 words per domain.
94
+ - Use this structure:
95
+
96
+ # Business Domains
97
+
98
+ ## Domain: [Name]
99
+
100
+ [Business explanation]
101
+
102
+ Main modules:
103
+ - [module paths]
104
+
105
+ User-visible functionality:
106
+ [what users can do]
107
+
108
+ Dependencies:
109
+ [other domains this depends on]`;
110
+ }
111
+
112
+ export function createArchitectureOverviewPrompt(context) {
113
+ return `Write an architecture overview for engineers, architects, and technical PMs.
114
+
115
+ Use this context:
116
+ ${JSON.stringify(context, null, 2)}
117
+
118
+ Requirements:
119
+ - Explain the layered architecture based on observable patterns.
120
+ - Describe flows across layers.
121
+ - Be specific about what is present in the context.
122
+ - Maximum 600 words.
123
+ - Use this structure:
124
+
125
+ # Architecture Overview
126
+
127
+ ## Architecture style
128
+
129
+ ## Application layer
130
+
131
+ ## UI / component layer
132
+
133
+ ## Domain / service layer
134
+
135
+ ## Data / integration layer
136
+
137
+ ## Cross-cutting concerns
138
+
139
+ ## Dependency flow
140
+
141
+ ## Architectural strengths
142
+
143
+ ## Architectural weaknesses`;
144
+ }
145
+
146
+ export function createDataFlowsPrompt(flows, context) {
147
+ return `Write data flow documentation for a mixed audience.
148
+
149
+ Use this flow information:
150
+ ${JSON.stringify(flows, null, 2)}
151
+
152
+ And this context:
153
+ ${JSON.stringify(context, null, 2)}
154
+
155
+ Requirements:
156
+ - Explain major information flows in plain language.
157
+ - For each flow, describe the journey from user action to system response.
158
+ - Be concrete about which modules are involved.
159
+ - Maximum 200 words per flow.
160
+ - Use this structure:
161
+
162
+ # Data Flows
163
+
164
+ ## [Flow Name]
165
+
166
+ [Plain language description of the flow]
167
+
168
+ Flow steps:
169
+ [numbered steps]
170
+
171
+ Involved modules:
172
+ - [module paths]
173
+
174
+ Critical dependencies:
175
+ [what this flow depends on]`;
176
+ }
177
+
178
+ export function createDeveloperOnboardingPrompt(context) {
179
+ return `Write developer onboarding documentation to help new engineers get productive quickly.
180
+
181
+ Use this context:
182
+ ${JSON.stringify(context, null, 2)}
183
+
184
+ Requirements:
185
+ - Guide new developers through the codebase structure.
186
+ - Point out the most important folders and files.
187
+ - Explain core product flows.
188
+ - Highlight complexity hotspots.
189
+ - Be practical and actionable.
190
+ - Maximum 800 words.
191
+ - Use this structure:
192
+
193
+ # Developer Onboarding
194
+
195
+ ## Start here
196
+
197
+ ## Main folders
198
+
199
+ ## Core product flows
200
+
201
+ ## Important routes
202
+
203
+ ## Important shared libraries
204
+
205
+ ## Common change areas
206
+
207
+ ## What to understand first
208
+
209
+ ## Known complexity hotspots`;
210
+ }
211
+
212
+ export function createModuleSummaryPrompt(module, context) {
213
+ return `Write a short module documentation section.
214
+
215
+ Module: ${module.key}
216
+ File count: ${module.fileCount}
217
+ Type: ${module.type}
218
+ Domain: ${module.domain}
219
+
220
+ Additional context:
221
+ ${JSON.stringify(context, null, 2)}
222
+
223
+ Requirements:
224
+ - Explain the module's likely purpose.
225
+ - List concrete responsibilities.
226
+ - Mention important dependencies from context.
227
+ - Mention complexity or centrality if supported.
228
+ - No speculation beyond the provided context.
229
+ - Maximum 180 words.
230
+ - Use this structure:
231
+
232
+ ## ${module.key}
233
+
234
+ Purpose:
235
+ [brief explanation]
236
+
237
+ Responsibilities:
238
+ - [concrete responsibilities]
239
+
240
+ Main dependencies:
241
+ [from context]
242
+
243
+ Complexity notes:
244
+ [if applicable]`;
245
+ }
246
+
247
+ export function createRouteSummaryPrompt(route, context) {
248
+ return `Write a route documentation section.
249
+
250
+ Route: ${route.path}
251
+ File: ${route.file}
252
+ Type: ${route.type}
253
+
254
+ Additional context:
255
+ ${JSON.stringify(context, null, 2)}
256
+
257
+ Requirements:
258
+ - Explain the user purpose of this route.
259
+ - Explain the technical role.
260
+ - Mention main dependencies.
261
+ - Maximum 120 words.
262
+ - Use this structure:
263
+
264
+ ## ${route.path}
265
+
266
+ User purpose:
267
+ [what users do here]
268
+
269
+ Technical role:
270
+ [what this route does]
271
+
272
+ Main dependencies:
273
+ [modules this uses]
274
+
275
+ Notes:
276
+ [any important observations]`;
277
+ }
278
+
279
+ export function createAPIDocumentationPrompt(api, context) {
280
+ return `Write API endpoint documentation.
281
+
282
+ API: ${api.methods.join(", ")} ${api.path}
283
+ File: ${api.file}
284
+
285
+ Additional context:
286
+ ${JSON.stringify(context, null, 2)}
287
+
288
+ Requirements:
289
+ - Explain the purpose in plain language and technical language.
290
+ - Describe what it returns or does.
291
+ - Mention dependencies and integrations.
292
+ - Mention risks if any are apparent.
293
+ - Maximum 150 words.
294
+ - Use this structure:
295
+
296
+ ## ${api.methods.join(", ")} ${api.path}
297
+
298
+ Purpose:
299
+ [plain language explanation]
300
+
301
+ Source file:
302
+ ${api.file}
303
+
304
+ Used by:
305
+ [routes or components]
306
+
307
+ Dependencies:
308
+ [external services or internal modules]
309
+
310
+ Risks:
311
+ [if applicable]`;
312
+ }
@@ -0,0 +1,134 @@
1
+ // Provider-agnostic AI text generation
2
+
3
+ import { warn, info } from "../utils/logger.js";
4
+
5
+ const DEFAULT_TIMEOUT_MS = 60000;
6
+ const DEFAULT_TEMPERATURE = 0.2;
7
+ const DEFAULT_MAX_TOKENS = 2500;
8
+
9
+ export async function generateText({ system, user, temperature, maxTokens }) {
10
+ // Check if AI is enabled
11
+ const enabled = process.env.REPOLENS_AI_ENABLED === "true";
12
+
13
+ if (!enabled) {
14
+ return {
15
+ success: false,
16
+ error: "AI features are disabled. Set REPOLENS_AI_ENABLED=true to enable.",
17
+ fallback: true
18
+ };
19
+ }
20
+
21
+ // Get provider configuration from environment
22
+ const provider = process.env.REPOLENS_AI_PROVIDER || "openai_compatible";
23
+ const baseUrl = process.env.REPOLENS_AI_BASE_URL;
24
+ const apiKey = process.env.REPOLENS_AI_API_KEY;
25
+ const model = process.env.REPOLENS_AI_MODEL || "gpt-4-turbo-preview";
26
+ const timeoutMs = parseInt(process.env.REPOLENS_AI_TIMEOUT_MS || DEFAULT_TIMEOUT_MS);
27
+
28
+ // Validate configuration
29
+ if (!apiKey) {
30
+ warn("REPOLENS_AI_API_KEY not set. AI features disabled.");
31
+ return {
32
+ success: false,
33
+ error: "Missing API key",
34
+ fallback: true
35
+ };
36
+ }
37
+
38
+ if (!baseUrl && provider === "openai_compatible") {
39
+ warn("REPOLENS_AI_BASE_URL not set. Using OpenAI default.");
40
+ }
41
+
42
+ try {
43
+ const result = await callOpenAICompatibleAPI({
44
+ baseUrl: baseUrl || "https://api.openai.com/v1",
45
+ apiKey,
46
+ model,
47
+ system,
48
+ user,
49
+ temperature: temperature ?? DEFAULT_TEMPERATURE,
50
+ maxTokens: maxTokens ?? DEFAULT_MAX_TOKENS,
51
+ timeoutMs
52
+ });
53
+
54
+ return {
55
+ success: true,
56
+ text: result,
57
+ fallback: false
58
+ };
59
+
60
+ } catch (error) {
61
+ warn(`AI generation failed: ${error.message}`);
62
+ return {
63
+ success: false,
64
+ error: error.message,
65
+ fallback: true
66
+ };
67
+ }
68
+ }
69
+
70
+ async function callOpenAICompatibleAPI({ baseUrl, apiKey, model, system, user, temperature, maxTokens, timeoutMs }) {
71
+ const url = `${baseUrl}/chat/completions`;
72
+
73
+ const controller = new AbortController();
74
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
75
+
76
+ try {
77
+ const response = await fetch(url, {
78
+ method: "POST",
79
+ headers: {
80
+ "Content-Type": "application/json",
81
+ "Authorization": `Bearer ${apiKey}`
82
+ },
83
+ body: JSON.stringify({
84
+ model,
85
+ messages: [
86
+ { role: "system", content: system },
87
+ { role: "user", content: user }
88
+ ],
89
+ temperature,
90
+ max_tokens: maxTokens
91
+ }),
92
+ signal: controller.signal
93
+ });
94
+
95
+ clearTimeout(timeoutId);
96
+
97
+ if (!response.ok) {
98
+ const errorText = await response.text();
99
+ throw new Error(`API error (${response.status}): ${errorText}`);
100
+ }
101
+
102
+ const data = await response.json();
103
+
104
+ if (!data.choices || data.choices.length === 0) {
105
+ throw new Error("No completion returned from API");
106
+ }
107
+
108
+ return data.choices[0].message.content;
109
+
110
+ } catch (error) {
111
+ clearTimeout(timeoutId);
112
+
113
+ if (error.name === "AbortError") {
114
+ throw new Error(`Request timeout after ${timeoutMs}ms`);
115
+ }
116
+
117
+ throw error;
118
+ }
119
+ }
120
+
121
+ export function isAIEnabled() {
122
+ return process.env.REPOLENS_AI_ENABLED === "true";
123
+ }
124
+
125
+ export function getAIConfig() {
126
+ return {
127
+ enabled: isAIEnabled(),
128
+ provider: process.env.REPOLENS_AI_PROVIDER || "openai_compatible",
129
+ model: process.env.REPOLENS_AI_MODEL || "gpt-4-turbo-preview",
130
+ hasApiKey: !!process.env.REPOLENS_AI_API_KEY,
131
+ temperature: parseFloat(process.env.REPOLENS_AI_TEMPERATURE || DEFAULT_TEMPERATURE),
132
+ maxTokens: parseInt(process.env.REPOLENS_AI_MAX_TOKENS || DEFAULT_MAX_TOKENS)
133
+ };
134
+ }
@@ -0,0 +1,146 @@
1
+ // Build structured AI context from deterministic scan results
2
+
3
+ import { groupModulesByDomain } from "./domain-inference.js";
4
+
5
+ export function buildAIContext(scanResult, config) {
6
+ const { filesCount, modules, api, pages, metadata } = scanResult;
7
+
8
+ // Get domain hints from config if available
9
+ const customHints = config.domains
10
+ ? Object.entries(config.domains).map(([key, domain]) => ({
11
+ match: domain.match || [],
12
+ domain: key,
13
+ description: domain.description || key
14
+ }))
15
+ : [];
16
+
17
+ // Group modules by business domain
18
+ const domainGroups = groupModulesByDomain(modules, customHints);
19
+
20
+ // Identify top modules
21
+ const topModules = modules
22
+ .slice(0, 15)
23
+ .map(m => ({
24
+ key: m.key,
25
+ fileCount: m.fileCount,
26
+ type: inferModuleType(m.key)
27
+ }));
28
+
29
+ // Categorize routes
30
+ const pageRoutes = pages?.slice(0, 50).map(p => ({
31
+ path: p.path,
32
+ file: p.file,
33
+ type: "page"
34
+ })) || [];
35
+
36
+ const apiRoutes = api?.slice(0, 50).map(a => ({
37
+ path: a.path,
38
+ file: a.file,
39
+ methods: a.methods,
40
+ type: "api"
41
+ })) || [];
42
+
43
+ // Build technology profile
44
+ const techStack = {
45
+ frameworks: metadata?.frameworks || [],
46
+ languages: metadata?.languages ? [...metadata.languages] : [],
47
+ buildTools: metadata?.buildTools || [],
48
+ testFrameworks: metadata?.testFrameworks || []
49
+ };
50
+
51
+ // Identify key architectural patterns
52
+ const patterns = inferArchitecturalPatterns(modules);
53
+
54
+ // Build compact context object
55
+ return {
56
+ project: {
57
+ name: config.project.name,
58
+ filesScanned: filesCount,
59
+ modulesDetected: modules.length,
60
+ pagesDetected: pages?.length || 0,
61
+ apiRoutesDetected: api?.length || 0
62
+ },
63
+
64
+ domains: domainGroups.map(d => ({
65
+ name: d.name,
66
+ description: d.description,
67
+ moduleCount: d.modules.length,
68
+ fileCount: d.totalFiles,
69
+ topModules: d.modules.slice(0, 5).map(m => m.key)
70
+ })),
71
+
72
+ topModules,
73
+
74
+ routes: {
75
+ pages: pageRoutes,
76
+ apis: apiRoutes
77
+ },
78
+
79
+ techStack,
80
+
81
+ patterns,
82
+
83
+ repoRoots: config.module_roots || []
84
+ };
85
+ }
86
+
87
+ function inferModuleType(modulePath) {
88
+ const lower = modulePath.toLowerCase();
89
+
90
+ if (lower.includes("api")) return "api";
91
+ if (lower.includes("component")) return "ui";
92
+ if (lower.includes("lib") || lower.includes("util")) return "library";
93
+ if (lower.includes("hook")) return "hooks";
94
+ if (lower.includes("store") || lower.includes("state")) return "state";
95
+ if (lower.includes("page") || lower.includes("route")) return "route";
96
+ if (lower.includes("app")) return "app";
97
+
98
+ return "other";
99
+ }
100
+
101
+ function inferArchitecturalPatterns(modules) {
102
+ const patterns = [];
103
+
104
+ const hasAppRouter = modules.some(m => m.key.includes("app/"));
105
+ const hasPagesRouter = modules.some(m => m.key.includes("pages/"));
106
+ const hasComponents = modules.some(m => m.key.includes("component"));
107
+ const hasLib = modules.some(m => m.key.includes("lib"));
108
+ const hasHooks = modules.some(m => m.key.includes("hook"));
109
+ const hasStore = modules.some(m => m.key.includes("store") || m.key.includes("state"));
110
+ const hasApi = modules.some(m => m.key.includes("api"));
111
+
112
+ if (hasAppRouter) patterns.push("Next.js App Router");
113
+ if (hasPagesRouter) patterns.push("Next.js Pages Router");
114
+ if (hasComponents && hasLib) patterns.push("Layered component architecture");
115
+ if (hasHooks) patterns.push("React hooks pattern");
116
+ if (hasStore) patterns.push("Centralized state management");
117
+ if (hasApi) patterns.push("API route pattern");
118
+
119
+ return patterns;
120
+ }
121
+
122
+ export function buildModuleContext(modules, config) {
123
+ const customHints = config.domains
124
+ ? Object.entries(config.domains).map(([key, domain]) => ({
125
+ match: domain.match || [],
126
+ domain: key,
127
+ description: domain.description || key
128
+ }))
129
+ : [];
130
+
131
+ const domainGroups = groupModulesByDomain(modules, customHints);
132
+
133
+ return modules.map(module => {
134
+ const domain = domainGroups.find(d =>
135
+ d.modules.some(m => m.key === module.key)
136
+ );
137
+
138
+ return {
139
+ key: module.key,
140
+ fileCount: module.fileCount,
141
+ type: inferModuleType(module.key),
142
+ domain: domain?.name || "Other",
143
+ domainDescription: domain?.description || "Uncategorized"
144
+ };
145
+ });
146
+ }
@@ -0,0 +1,127 @@
1
+ // Domain inference - map folder/file names to business domains
2
+
3
+ const DEFAULT_DOMAIN_HINTS = [
4
+ {
5
+ match: ["auth", "login", "signup", "session", "user", "account"],
6
+ domain: "Authentication",
7
+ description: "User authentication and identity flows"
8
+ },
9
+ {
10
+ match: ["stock", "chart", "price", "market", "watchlist", "ticker", "quote"],
11
+ domain: "Market Data & Analysis",
12
+ description: "Market data retrieval, analysis, and visualization"
13
+ },
14
+ {
15
+ match: ["article", "newsletter", "news", "research", "content", "blog", "post"],
16
+ domain: "Content & Research",
17
+ description: "Content publishing, research, and insight delivery"
18
+ },
19
+ {
20
+ match: ["portfolio", "positions", "holdings", "trades", "orders"],
21
+ domain: "Portfolio Management",
22
+ description: "Portfolio tracking and trading functionality"
23
+ },
24
+ {
25
+ match: ["alert", "notification", "email", "sms", "webhook"],
26
+ domain: "Alerts & Notifications",
27
+ description: "User notification and alerting system"
28
+ },
29
+ {
30
+ match: ["payment", "subscription", "billing", "stripe", "checkout"],
31
+ domain: "Payments & Billing",
32
+ description: "Payment processing and subscription management"
33
+ },
34
+ {
35
+ match: ["api", "endpoint", "route", "handler"],
36
+ domain: "API Layer",
37
+ description: "Backend API endpoints and request handling"
38
+ },
39
+ {
40
+ match: ["component", "ui", "button", "form", "modal", "dialog"],
41
+ domain: "UI Components",
42
+ description: "Reusable user interface components"
43
+ },
44
+ {
45
+ match: ["hook", "hooks", "use"],
46
+ domain: "React Hooks",
47
+ description: "Custom React hooks for state and behavior"
48
+ },
49
+ {
50
+ match: ["store", "state", "redux", "zustand", "context"],
51
+ domain: "State Management",
52
+ description: "Application state management"
53
+ },
54
+ {
55
+ match: ["lib", "util", "helper", "common", "shared"],
56
+ domain: "Shared Utilities",
57
+ description: "Common utilities and helper functions"
58
+ },
59
+ {
60
+ match: ["data", "database", "db", "prisma", "sql"],
61
+ domain: "Data Layer",
62
+ description: "Database access and data persistence"
63
+ }
64
+ ];
65
+
66
+ export function inferDomain(modulePath, customHints = []) {
67
+ const hints = [...customHints, ...DEFAULT_DOMAIN_HINTS];
68
+ const lowerPath = modulePath.toLowerCase();
69
+
70
+ for (const hint of hints) {
71
+ for (const keyword of hint.match) {
72
+ if (lowerPath.includes(keyword)) {
73
+ return {
74
+ domain: hint.domain,
75
+ description: hint.description,
76
+ confidence: "pattern_match",
77
+ matchedKeyword: keyword
78
+ };
79
+ }
80
+ }
81
+ }
82
+
83
+ return {
84
+ domain: "Other",
85
+ description: "Uncategorized module",
86
+ confidence: "none",
87
+ matchedKeyword: null
88
+ };
89
+ }
90
+
91
+ export function groupModulesByDomain(modules, customHints = []) {
92
+ const domainGroups = {};
93
+
94
+ for (const module of modules) {
95
+ const inference = inferDomain(module.key, customHints);
96
+ const domainName = inference.domain;
97
+
98
+ if (!domainGroups[domainName]) {
99
+ domainGroups[domainName] = {
100
+ name: domainName,
101
+ description: inference.description,
102
+ modules: [],
103
+ totalFiles: 0
104
+ };
105
+ }
106
+
107
+ domainGroups[domainName].modules.push({
108
+ ...module,
109
+ inference
110
+ });
111
+ domainGroups[domainName].totalFiles += module.fileCount;
112
+ }
113
+
114
+ // Sort domains by total files (most important first)
115
+ const sortedDomains = Object.values(domainGroups)
116
+ .sort((a, b) => b.totalFiles - a.totalFiles);
117
+
118
+ return sortedDomains;
119
+ }
120
+
121
+ export function inferRouteDomain(routePath, customHints = []) {
122
+ return inferDomain(routePath, customHints);
123
+ }
124
+
125
+ export function inferApiDomain(apiPath, customHints = []) {
126
+ return inferDomain(apiPath, customHints);
127
+ }