Skip to content

Commit c80b1eb

Browse files
committed
feat: Add intelligent file selection to generate-implementation
- Implement keyword-based file identification (like agent.mjs) - Automatically identifies relevant files based on task keywords - Includes priority files (package.json, index.js) first - Then includes task-relevant files (e.g., 'alias' -> index.js, command-handlers.js) - Finally includes other lib files if space allows - Much more efficient than hardcoded file list - Reduces token usage and improves accuracy
1 parent 141016f commit c80b1eb

1 file changed

Lines changed: 177 additions & 41 deletions

File tree

.github/agent/generate-implementation.mjs

Lines changed: 177 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import Anthropic from "@anthropic-ai/sdk";
2-
import { readFileSync, writeFileSync, existsSync } from "fs";
3-
import { resolve, dirname } from "path";
2+
import { readFileSync, writeFileSync, existsSync, readdirSync, statSync } from "fs";
3+
import { resolve, dirname, join } from "path";
44
import { fileURLToPath } from "url";
55
import { execSync } from "child_process";
66

@@ -10,7 +10,7 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
1010
* Generate Implementation - Use Claude to analyze codebase and IMPLEMENT code changes
1111
*
1212
* This script uses Claude to:
13-
* 1. Analyze the existing codebase
13+
* 1. Intelligently identify relevant files based on the task
1414
* 2. Generate actual code changes (diffs)
1515
* 3. Apply the changes to the codebase
1616
*/
@@ -32,61 +32,197 @@ if (!anthropicApiKey) {
3232

3333
const anthropic = new Anthropic({ apiKey: anthropicApiKey });
3434

35+
/**
36+
* Get all files in the repository
37+
*/
38+
function getAllFiles() {
39+
const repoRoot = resolve(__dirname, "../..");
40+
const files = [];
41+
42+
function walkDir(dir, basePath = "") {
43+
try {
44+
const entries = readdirSync(dir, { withFileTypes: true });
45+
for (const entry of entries) {
46+
const fullPath = join(dir, entry.name);
47+
const relPath = basePath ? join(basePath, entry.name) : entry.name;
48+
49+
// Skip excluded paths
50+
if (relPath.includes('node_modules/') ||
51+
relPath.includes('.git/') ||
52+
relPath.includes('coverage/') ||
53+
relPath.includes('dist/') ||
54+
relPath.includes('build/') ||
55+
relPath.startsWith('.') && !relPath.startsWith('.github/')) {
56+
continue;
57+
}
58+
59+
if (entry.isDirectory()) {
60+
walkDir(fullPath, relPath);
61+
} else if (entry.isFile()) {
62+
// Only include code files
63+
const ext = entry.name.split('.').pop();
64+
if (['js', 'mjs', 'json', 'html', 'css', 'txt', 'yml', 'yaml'].includes(ext)) {
65+
files.push(relPath);
66+
}
67+
}
68+
}
69+
} catch (e) {
70+
// Skip directories we can't read
71+
}
72+
}
73+
74+
walkDir(repoRoot);
75+
return files;
76+
}
77+
78+
/**
79+
* Select relevant files based on task description (keyword-based)
80+
*/
81+
function selectRelevantFiles(task, fileList) {
82+
const taskLower = task.toLowerCase();
83+
const relevantFiles = new Set();
84+
85+
// Keyword to file mapping (updated for current codebase structure)
86+
const keywordMap = {
87+
'spotify': ['lib/spotify.js', 'lib/command-handlers.js'],
88+
'discord': ['lib/discord.js'],
89+
'slack': ['lib/slack.js'],
90+
'sonos': ['index.js'],
91+
'vote': ['lib/voting.js', 'index.js'],
92+
'gong': ['lib/voting.js', 'index.js'],
93+
'admin': ['public/setup/admin.js', 'public/setup/admin.html'],
94+
'auth': ['lib/auth-handler.js', 'lib/webauthn-handler.js'],
95+
'ai': ['lib/ai-handler.js'],
96+
'soundcraft': ['lib/soundcraft-handler.js'],
97+
'help': ['templates/help/', 'index.js'],
98+
'web': ['public/setup/', 'public/'],
99+
'config': ['index.js'],
100+
'queue': ['lib/add-handlers.js', 'index.js'],
101+
'search': ['lib/spotify.js', 'lib/command-handlers.js', 'index.js'],
102+
'command': ['lib/command-handlers.js', 'index.js'],
103+
'feature': ['index.js', 'lib/command-handlers.js'],
104+
'alias': ['index.js', 'lib/command-handlers.js'],
105+
'github': ['lib/github-app.js', 'index.js']
106+
};
107+
108+
// Find relevant files based on keywords
109+
for (const [keyword, files] of Object.entries(keywordMap)) {
110+
if (taskLower.includes(keyword)) {
111+
for (const file of files) {
112+
// Check if file exists in fileList
113+
const matchingFiles = fileList.filter(f =>
114+
f.includes(file) || f.endsWith(file) || f === file
115+
);
116+
matchingFiles.forEach(f => relevantFiles.add(f));
117+
}
118+
}
119+
}
120+
121+
// Also check for direct file mentions
122+
for (const file of fileList) {
123+
const fileName = file.split('/').pop();
124+
if (taskLower.includes(fileName.toLowerCase().replace(/\.(js|mjs|json)$/, ''))) {
125+
relevantFiles.add(file);
126+
}
127+
}
128+
129+
return Array.from(relevantFiles);
130+
}
131+
35132
/**
36133
* Read relevant project files to give Claude context
37134
*/
38-
function getProjectContext() {
135+
function getProjectContext(task) {
39136
const repoRoot = resolve(__dirname, "../..");
40137
const files = [];
138+
const fileList = getAllFiles();
139+
140+
// Priority files (always include)
141+
const priorityFiles = [
142+
'package.json',
143+
'index.js'
144+
];
145+
146+
// Get task-relevant files
147+
const relevantFiles = selectRelevantFiles(task, fileList);
148+
console.log(`[IMPLEMENTATION] Identified ${relevantFiles.length} relevant files based on task keywords`);
149+
if (relevantFiles.length > 0) {
150+
console.log(`[IMPLEMENTATION] Relevant files: ${relevantFiles.join(', ')}`);
151+
}
41152

42-
// Read package.json for dependencies
43-
try {
44-
const packageJson = readFileSync(resolve(repoRoot, "package.json"), "utf8");
45-
files.push({
46-
path: "package.json",
47-
content: packageJson
48-
});
49-
} catch (e) {
50-
console.warn("[IMPLEMENTATION] Could not read package.json");
153+
// First pass: Include priority files
154+
for (const filePath of priorityFiles) {
155+
try {
156+
const fullPath = resolve(repoRoot, filePath);
157+
if (existsSync(fullPath)) {
158+
const content = readFileSync(fullPath, "utf8");
159+
// Include full content for priority files
160+
files.push({
161+
path: filePath,
162+
content: filePath === 'index.js' ? content.substring(0, 15000) : content
163+
});
164+
}
165+
} catch (e) {
166+
console.warn(`[IMPLEMENTATION] Could not read ${filePath}`);
167+
}
51168
}
52169

53-
// Read main index file
54-
try {
55-
const indexJs = readFileSync(resolve(repoRoot, "index.js"), "utf8");
56-
files.push({
57-
path: "index.js",
58-
content: indexJs.substring(0, 10000) // More context for main file
59-
});
60-
} catch (e) {
61-
console.warn("[IMPLEMENTATION] Could not read index.js");
170+
// Second pass: Include task-relevant files
171+
for (const filePath of relevantFiles) {
172+
try {
173+
const fullPath = resolve(repoRoot, filePath);
174+
if (existsSync(fullPath)) {
175+
const stats = statSync(fullPath);
176+
// Skip very large files
177+
if (stats.size > 100000) {
178+
console.log(`[IMPLEMENTATION] Skipping large file: ${filePath} (${stats.size} bytes)`);
179+
continue;
180+
}
181+
182+
const content = readFileSync(fullPath, "utf8");
183+
files.push({
184+
path: filePath,
185+
content: content // Include full content for relevant files
186+
});
187+
}
188+
} catch (e) {
189+
console.warn(`[IMPLEMENTATION] Could not read ${filePath}`);
190+
}
62191
}
63192

64-
// Read lib directory files
65-
try {
66-
const libFiles = [
67-
"lib/slack.js",
68-
"lib/discord.js",
69-
"lib/voting.js",
70-
"lib/command-handlers.js",
71-
"lib/ai-handler.js",
72-
"lib/spotify.js",
73-
"lib/add-handlers.js"
74-
];
75-
76-
for (const libFile of libFiles) {
77-
const fullPath = resolve(repoRoot, libFile);
193+
// Third pass: Include other lib files if we have space (limit to prevent token overflow)
194+
const maxFiles = 15; // Limit total files
195+
const libFiles = [
196+
'lib/slack.js',
197+
'lib/discord.js',
198+
'lib/voting.js',
199+
'lib/command-handlers.js',
200+
'lib/ai-handler.js',
201+
'lib/spotify.js',
202+
'lib/add-handlers.js'
203+
];
204+
205+
for (const filePath of libFiles) {
206+
if (files.length >= maxFiles) break;
207+
208+
// Skip if already included
209+
if (files.some(f => f.path === filePath)) continue;
210+
211+
try {
212+
const fullPath = resolve(repoRoot, filePath);
78213
if (existsSync(fullPath)) {
79214
const content = readFileSync(fullPath, "utf8");
80215
files.push({
81-
path: libFile,
82-
content: content.substring(0, 5000) // More context per file
216+
path: filePath,
217+
content: content.substring(0, 8000) // Truncate non-priority files
83218
});
84219
}
220+
} catch (e) {
221+
// Skip if can't read
85222
}
86-
} catch (e) {
87-
console.warn("[IMPLEMENTATION] Could not read lib files");
88223
}
89224

225+
console.log(`[IMPLEMENTATION] Including ${files.length} files in context`);
90226
return files;
91227
}
92228

@@ -144,7 +280,7 @@ async function generateImplementation() {
144280
console.log(`[IMPLEMENTATION] Generating code implementation with ${model}...`);
145281
console.log(`[IMPLEMENTATION] Feature request: ${enhancedTask}`);
146282

147-
const projectFiles = getProjectContext();
283+
const projectFiles = getProjectContext(enhancedTask);
148284

149285
// Build context from project files
150286
let contextText = "Here are relevant files from the project:\n\n";

0 commit comments

Comments
 (0)