banana-code 1.2.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/LICENSE +21 -0
- package/README.md +246 -0
- package/banana.js +5464 -0
- package/lib/agenticRunner.js +1884 -0
- package/lib/borderRenderer.js +41 -0
- package/lib/commandRunner.js +205 -0
- package/lib/completer.js +286 -0
- package/lib/config.js +301 -0
- package/lib/contextBuilder.js +324 -0
- package/lib/diffViewer.js +295 -0
- package/lib/fileManager.js +224 -0
- package/lib/historyManager.js +124 -0
- package/lib/hookManager.js +1143 -0
- package/lib/imageHandler.js +268 -0
- package/lib/inlineComplete.js +192 -0
- package/lib/interactivePicker.js +254 -0
- package/lib/lmStudio.js +226 -0
- package/lib/markdownRenderer.js +423 -0
- package/lib/mcpClient.js +288 -0
- package/lib/modelRegistry.js +350 -0
- package/lib/monkeyModels.js +97 -0
- package/lib/oauthOpenAI.js +167 -0
- package/lib/parser.js +134 -0
- package/lib/promptManager.js +96 -0
- package/lib/providerClients.js +1014 -0
- package/lib/providerManager.js +130 -0
- package/lib/providerStore.js +413 -0
- package/lib/statusBar.js +283 -0
- package/lib/streamHandler.js +306 -0
- package/lib/subAgentManager.js +406 -0
- package/lib/tokenCounter.js +132 -0
- package/lib/visionAnalyzer.js +163 -0
- package/lib/watcher.js +138 -0
- package/models.json +57 -0
- package/package.json +42 -0
- package/prompts/base.md +23 -0
- package/prompts/code-agent-glm.md +16 -0
- package/prompts/code-agent-gptoss.md +25 -0
- package/prompts/code-agent-nemotron.md +17 -0
- package/prompts/code-agent-qwen.md +20 -0
- package/prompts/code-agent.md +70 -0
- package/prompts/plan.md +44 -0
package/lib/config.js
ADDED
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration management for Banana Code
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
|
|
8
|
+
const DEFAULT_CONFIG = {
|
|
9
|
+
lmStudioUrl: 'http://localhost:1234',
|
|
10
|
+
compactMode: false,
|
|
11
|
+
maxTokens: 32000,
|
|
12
|
+
tokenWarningThreshold: 0.8, // Warn at 80% of max
|
|
13
|
+
streamingEnabled: true,
|
|
14
|
+
autoSaveHistory: true,
|
|
15
|
+
historyLimit: 50,
|
|
16
|
+
steeringEnabled: true,
|
|
17
|
+
agenticMode: true, // Enable AI tool calling (read files, search code)
|
|
18
|
+
activeModel: null, // Persisted model selection (friendly name from models.json)
|
|
19
|
+
activePrompt: 'base', // Active system prompt
|
|
20
|
+
mcpUrl: null, // MCP server URL (overrides MCP_SERVER_URL env var)
|
|
21
|
+
geminiApiKey: null, // For vision analysis fallback (Alt+V screenshots)
|
|
22
|
+
ignorePatterns: [
|
|
23
|
+
'node_modules/**',
|
|
24
|
+
'.git/**',
|
|
25
|
+
'dist/**',
|
|
26
|
+
'build/**',
|
|
27
|
+
'.next/**',
|
|
28
|
+
'coverage/**',
|
|
29
|
+
'*.lock',
|
|
30
|
+
'*.log'
|
|
31
|
+
]
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
class Config {
|
|
35
|
+
constructor(projectDir) {
|
|
36
|
+
this.projectDir = projectDir;
|
|
37
|
+
this.bananaDir = path.join(projectDir, '.banana');
|
|
38
|
+
this.configPath = path.join(this.bananaDir, 'config.json');
|
|
39
|
+
this.instructionsPath = path.join(this.bananaDir, 'instructions.md');
|
|
40
|
+
this.historyDir = path.join(this.bananaDir, 'history');
|
|
41
|
+
this.config = { ...DEFAULT_CONFIG };
|
|
42
|
+
this.load();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
ensureDir() {
|
|
46
|
+
if (!fs.existsSync(this.bananaDir)) {
|
|
47
|
+
fs.mkdirSync(this.bananaDir, { recursive: true });
|
|
48
|
+
}
|
|
49
|
+
if (!fs.existsSync(this.historyDir)) {
|
|
50
|
+
fs.mkdirSync(this.historyDir, { recursive: true });
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
load() {
|
|
55
|
+
try {
|
|
56
|
+
if (fs.existsSync(this.configPath)) {
|
|
57
|
+
const saved = JSON.parse(fs.readFileSync(this.configPath, 'utf-8'));
|
|
58
|
+
this.config = { ...DEFAULT_CONFIG, ...saved };
|
|
59
|
+
}
|
|
60
|
+
} catch {
|
|
61
|
+
// Use defaults
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Migrate old config: apiUrl -> lmStudioUrl
|
|
65
|
+
if (this.config.apiUrl && !this.config.lmStudioUrl) {
|
|
66
|
+
this.config.lmStudioUrl = DEFAULT_CONFIG.lmStudioUrl;
|
|
67
|
+
delete this.config.apiUrl;
|
|
68
|
+
this.save();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Env overrides (support legacy RIPLEY_ prefix with deprecation)
|
|
72
|
+
const envUrl = process.env.BANANA_LM_STUDIO_URL || process.env.RIPLEY_LM_STUDIO_URL;
|
|
73
|
+
if (envUrl) {
|
|
74
|
+
this.config.lmStudioUrl = envUrl;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
save() {
|
|
79
|
+
this.ensureDir();
|
|
80
|
+
fs.writeFileSync(this.configPath, JSON.stringify(this.config, null, 2));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
get(key) {
|
|
84
|
+
return this.config[key];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
set(key, value) {
|
|
88
|
+
this.config[key] = value;
|
|
89
|
+
this.save();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
getAll() {
|
|
93
|
+
return { ...this.config };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Project-specific instructions
|
|
97
|
+
// Checks BANANA.md at project root first, then .banana/instructions.md as fallback
|
|
98
|
+
getInstructions() {
|
|
99
|
+
// Primary: BANANA.md at project root (like CLAUDE.md pattern)
|
|
100
|
+
const bananaMdPath = path.join(this.projectDir, 'BANANA.md');
|
|
101
|
+
try {
|
|
102
|
+
if (fs.existsSync(bananaMdPath)) {
|
|
103
|
+
return { content: fs.readFileSync(bananaMdPath, 'utf-8'), source: 'BANANA.md' };
|
|
104
|
+
}
|
|
105
|
+
} catch {
|
|
106
|
+
// Fall through
|
|
107
|
+
}
|
|
108
|
+
// Fallback: .banana/instructions.md
|
|
109
|
+
try {
|
|
110
|
+
if (fs.existsSync(this.instructionsPath)) {
|
|
111
|
+
return { content: fs.readFileSync(this.instructionsPath, 'utf-8'), source: '.banana/instructions.md' };
|
|
112
|
+
}
|
|
113
|
+
} catch {
|
|
114
|
+
// No instructions
|
|
115
|
+
}
|
|
116
|
+
// Legacy fallback: RIPLEY.md at project root
|
|
117
|
+
const ripleyMdPath = path.join(this.projectDir, 'RIPLEY.md');
|
|
118
|
+
try {
|
|
119
|
+
if (fs.existsSync(ripleyMdPath)) {
|
|
120
|
+
return { content: fs.readFileSync(ripleyMdPath, 'utf-8'), source: 'RIPLEY.md' };
|
|
121
|
+
}
|
|
122
|
+
} catch {
|
|
123
|
+
// No legacy instructions
|
|
124
|
+
}
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
createDefaultInstructions() {
|
|
129
|
+
const bananaMdPath = path.join(this.projectDir, 'BANANA.md');
|
|
130
|
+
const template = `# BANANA.md
|
|
131
|
+
|
|
132
|
+
This file provides project-specific instructions to Banana Code.
|
|
133
|
+
It is automatically loaded into the AI context at the start of every conversation.
|
|
134
|
+
|
|
135
|
+
## Project Overview
|
|
136
|
+
<!-- Describe your project briefly -->
|
|
137
|
+
|
|
138
|
+
## Code Style
|
|
139
|
+
<!-- Describe your preferred code style -->
|
|
140
|
+
- We use TypeScript
|
|
141
|
+
- We prefer functional components with hooks
|
|
142
|
+
- We use Tailwind CSS for styling
|
|
143
|
+
|
|
144
|
+
## Important Notes
|
|
145
|
+
<!-- Any special instructions for the AI -->
|
|
146
|
+
- Always use pnpm instead of npm
|
|
147
|
+
- Follow the existing patterns in the codebase
|
|
148
|
+
|
|
149
|
+
## Off-Limits
|
|
150
|
+
<!-- Things the AI should NOT do -->
|
|
151
|
+
- Don't modify package-lock.json or pnpm-lock.yaml
|
|
152
|
+
- Don't change the build configuration without asking
|
|
153
|
+
`;
|
|
154
|
+
fs.writeFileSync(bananaMdPath, template);
|
|
155
|
+
return template;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Conversation history persistence
|
|
159
|
+
saveConversation(name, history) {
|
|
160
|
+
this.ensureDir();
|
|
161
|
+
const filename = `${name.replace(/[^a-z0-9]/gi, '_')}_${Date.now()}.json`;
|
|
162
|
+
const filepath = path.join(this.historyDir, filename);
|
|
163
|
+
fs.writeFileSync(filepath, JSON.stringify({
|
|
164
|
+
name,
|
|
165
|
+
savedAt: new Date().toISOString(),
|
|
166
|
+
history
|
|
167
|
+
}, null, 2));
|
|
168
|
+
return filename;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
listConversations() {
|
|
172
|
+
this.ensureDir();
|
|
173
|
+
try {
|
|
174
|
+
const files = fs.readdirSync(this.historyDir)
|
|
175
|
+
.filter(f => f.endsWith('.json'))
|
|
176
|
+
.map(f => {
|
|
177
|
+
try {
|
|
178
|
+
const content = JSON.parse(fs.readFileSync(path.join(this.historyDir, f), 'utf-8'));
|
|
179
|
+
return {
|
|
180
|
+
filename: f,
|
|
181
|
+
name: content.name,
|
|
182
|
+
savedAt: content.savedAt,
|
|
183
|
+
messageCount: content.history?.length || 0
|
|
184
|
+
};
|
|
185
|
+
} catch {
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
})
|
|
189
|
+
.filter(Boolean)
|
|
190
|
+
.sort((a, b) => new Date(b.savedAt) - new Date(a.savedAt));
|
|
191
|
+
return files;
|
|
192
|
+
} catch {
|
|
193
|
+
return [];
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
loadConversation(filename) {
|
|
198
|
+
const filepath = path.join(this.historyDir, filename);
|
|
199
|
+
try {
|
|
200
|
+
const content = JSON.parse(fs.readFileSync(filepath, 'utf-8'));
|
|
201
|
+
return content.history || [];
|
|
202
|
+
} catch {
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
deleteConversation(filename) {
|
|
208
|
+
const filepath = path.join(this.historyDir, filename);
|
|
209
|
+
try {
|
|
210
|
+
fs.unlinkSync(filepath);
|
|
211
|
+
return true;
|
|
212
|
+
} catch {
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Hooks config
|
|
218
|
+
getHooksPath() {
|
|
219
|
+
return path.join(this.bananaDir, 'hooks.json');
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
getHooks() {
|
|
223
|
+
try {
|
|
224
|
+
const hooksPath = this.getHooksPath();
|
|
225
|
+
if (fs.existsSync(hooksPath)) {
|
|
226
|
+
return JSON.parse(fs.readFileSync(hooksPath, 'utf-8'));
|
|
227
|
+
}
|
|
228
|
+
} catch {
|
|
229
|
+
// Invalid hooks.json
|
|
230
|
+
}
|
|
231
|
+
return {};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
saveHooks(hookConfig) {
|
|
235
|
+
this.ensureDir();
|
|
236
|
+
fs.writeFileSync(this.getHooksPath(), JSON.stringify(hookConfig, null, 2));
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// ─── Global Config (~/.banana/) ──────────────────────────────────────────────
|
|
241
|
+
|
|
242
|
+
const GLOBAL_BANANA_DIR = path.join(require('os').homedir(), '.banana');
|
|
243
|
+
|
|
244
|
+
class GlobalConfig {
|
|
245
|
+
constructor() {
|
|
246
|
+
this.bananaDir = GLOBAL_BANANA_DIR;
|
|
247
|
+
this.configPath = path.join(this.bananaDir, 'config.json');
|
|
248
|
+
this.instructionsPath = path.join(this.bananaDir, 'BANANA.md');
|
|
249
|
+
this.commandsDir = path.join(this.bananaDir, 'commands');
|
|
250
|
+
this.config = {};
|
|
251
|
+
this.load();
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
ensureDir() {
|
|
255
|
+
for (const dir of [this.bananaDir, this.commandsDir, path.join(this.bananaDir, 'logs')]) {
|
|
256
|
+
if (!fs.existsSync(dir)) {
|
|
257
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
load() {
|
|
263
|
+
try {
|
|
264
|
+
if (fs.existsSync(this.configPath)) {
|
|
265
|
+
this.config = JSON.parse(fs.readFileSync(this.configPath, 'utf-8'));
|
|
266
|
+
}
|
|
267
|
+
} catch {
|
|
268
|
+
this.config = {};
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
save() {
|
|
273
|
+
this.ensureDir();
|
|
274
|
+
fs.writeFileSync(this.configPath, JSON.stringify(this.config, null, 2));
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
get(key) {
|
|
278
|
+
return this.config[key];
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
set(key, value) {
|
|
282
|
+
this.config[key] = value;
|
|
283
|
+
this.save();
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Get global instructions from ~/.banana/BANANA.md
|
|
288
|
+
*/
|
|
289
|
+
getInstructions() {
|
|
290
|
+
try {
|
|
291
|
+
if (fs.existsSync(this.instructionsPath)) {
|
|
292
|
+
return { content: fs.readFileSync(this.instructionsPath, 'utf-8'), source: '~/.banana/BANANA.md' };
|
|
293
|
+
}
|
|
294
|
+
} catch {
|
|
295
|
+
// No global instructions
|
|
296
|
+
}
|
|
297
|
+
return null;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
module.exports = { Config, GlobalConfig, GLOBAL_BANANA_DIR };
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context Builder - Build project context for AI
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
|
|
8
|
+
// Directories to always ignore
|
|
9
|
+
const IGNORED_DIRS = [
|
|
10
|
+
'node_modules', '.git', '.next', 'dist', 'build', '.cache',
|
|
11
|
+
'coverage', '.nyc_output', '.vercel', '.netlify', '.svelte-kit',
|
|
12
|
+
'__pycache__', 'venv', '.venv', 'env', '.env',
|
|
13
|
+
'.banana', '.ripley', '.idea', '.vscode'
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
// Files to always ignore
|
|
17
|
+
const IGNORED_FILES = [
|
|
18
|
+
'.DS_Store', 'Thumbs.db', '.gitkeep',
|
|
19
|
+
'package-lock.json', 'yarn.lock', 'pnpm-lock.yaml'
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
// Priority files to always include if they exist
|
|
23
|
+
const PRIORITY_FILES = [
|
|
24
|
+
'package.json', 'tsconfig.json', 'README.md', 'README.txt',
|
|
25
|
+
'.env.example', 'next.config.js', 'next.config.mjs', 'next.config.ts',
|
|
26
|
+
'vite.config.js', 'vite.config.ts', 'tailwind.config.js', 'tailwind.config.ts',
|
|
27
|
+
'prisma/schema.prisma', 'drizzle.config.ts'
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
// Extensions for source files
|
|
31
|
+
const SOURCE_EXTENSIONS = [
|
|
32
|
+
'.js', '.jsx', '.ts', '.tsx', '.vue', '.svelte',
|
|
33
|
+
'.py', '.rb', '.go', '.rs', '.java', '.kt',
|
|
34
|
+
'.css', '.scss', '.sass', '.less',
|
|
35
|
+
'.html', '.htm', '.json', '.yaml', '.yml', '.toml',
|
|
36
|
+
'.md', '.mdx', '.sql', '.graphql', '.gql'
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
class ContextBuilder {
|
|
40
|
+
constructor(fileManager, ignorePatterns = []) {
|
|
41
|
+
this.fileManager = fileManager;
|
|
42
|
+
this.projectDir = fileManager.projectDir;
|
|
43
|
+
this.customIgnores = ignorePatterns;
|
|
44
|
+
this.loadedFiles = new Map(); // path -> content
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Load .gitignore patterns
|
|
49
|
+
*/
|
|
50
|
+
loadGitignore() {
|
|
51
|
+
const gitignorePath = path.join(this.projectDir, '.gitignore');
|
|
52
|
+
if (fs.existsSync(gitignorePath)) {
|
|
53
|
+
try {
|
|
54
|
+
const content = fs.readFileSync(gitignorePath, 'utf-8');
|
|
55
|
+
return content
|
|
56
|
+
.split('\n')
|
|
57
|
+
.map(line => line.trim())
|
|
58
|
+
.filter(line => line && !line.startsWith('#'));
|
|
59
|
+
} catch {
|
|
60
|
+
return [];
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return [];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Check if path should be ignored
|
|
68
|
+
*/
|
|
69
|
+
shouldIgnore(relativePath) {
|
|
70
|
+
const basename = path.basename(relativePath);
|
|
71
|
+
const parts = relativePath.split(path.sep);
|
|
72
|
+
|
|
73
|
+
// Check ignored directories
|
|
74
|
+
for (const dir of IGNORED_DIRS) {
|
|
75
|
+
if (parts.includes(dir)) return true;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Check ignored files
|
|
79
|
+
if (IGNORED_FILES.includes(basename)) return true;
|
|
80
|
+
|
|
81
|
+
// Check custom patterns (simple matching)
|
|
82
|
+
for (const pattern of this.customIgnores) {
|
|
83
|
+
if (pattern.endsWith('/') || pattern.endsWith('\\')) {
|
|
84
|
+
// Directory pattern
|
|
85
|
+
const dirName = pattern.slice(0, -1);
|
|
86
|
+
if (parts.includes(dirName)) return true;
|
|
87
|
+
} else if (pattern.startsWith('*.')) {
|
|
88
|
+
// Extension pattern
|
|
89
|
+
const ext = pattern.slice(1);
|
|
90
|
+
if (relativePath.endsWith(ext)) return true;
|
|
91
|
+
} else if (relativePath.includes(pattern)) {
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Recursively scan directory structure
|
|
101
|
+
* @param {string} dir - Directory to scan
|
|
102
|
+
* @param {number} depth - Current depth
|
|
103
|
+
* @param {number} maxDepth - Maximum recursion depth
|
|
104
|
+
* @param {number} maxFiles - Maximum total files to collect (prevents runaway scans)
|
|
105
|
+
* @param {object} counter - Shared counter across recursive calls
|
|
106
|
+
*/
|
|
107
|
+
scanDirectory(dir = this.projectDir, depth = 0, maxDepth = 6, maxFiles = 5000, counter = { count: 0 }) {
|
|
108
|
+
if (depth > maxDepth) return [];
|
|
109
|
+
if (counter.count >= maxFiles) return [];
|
|
110
|
+
|
|
111
|
+
const results = [];
|
|
112
|
+
const relativePath = path.relative(this.projectDir, dir) || '.';
|
|
113
|
+
|
|
114
|
+
if (this.shouldIgnore(relativePath)) return [];
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
118
|
+
|
|
119
|
+
for (const entry of entries) {
|
|
120
|
+
if (counter.count >= maxFiles) break;
|
|
121
|
+
|
|
122
|
+
const fullPath = path.join(dir, entry.name);
|
|
123
|
+
const entryRelPath = path.relative(this.projectDir, fullPath);
|
|
124
|
+
|
|
125
|
+
if (this.shouldIgnore(entryRelPath)) continue;
|
|
126
|
+
|
|
127
|
+
if (entry.isDirectory()) {
|
|
128
|
+
results.push({
|
|
129
|
+
type: 'dir',
|
|
130
|
+
name: entry.name,
|
|
131
|
+
path: entryRelPath,
|
|
132
|
+
depth
|
|
133
|
+
});
|
|
134
|
+
results.push(...this.scanDirectory(fullPath, depth + 1, maxDepth, maxFiles, counter));
|
|
135
|
+
} else {
|
|
136
|
+
const ext = path.extname(entry.name).toLowerCase();
|
|
137
|
+
const isSource = SOURCE_EXTENSIONS.includes(ext);
|
|
138
|
+
results.push({
|
|
139
|
+
type: 'file',
|
|
140
|
+
name: entry.name,
|
|
141
|
+
path: entryRelPath,
|
|
142
|
+
isSource,
|
|
143
|
+
depth
|
|
144
|
+
});
|
|
145
|
+
counter.count++;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
} catch {
|
|
149
|
+
// Ignore permission errors
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return results;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Build a tree string from scan results
|
|
157
|
+
*/
|
|
158
|
+
buildTreeString(items, maxItems = 100) {
|
|
159
|
+
let tree = '';
|
|
160
|
+
let count = 0;
|
|
161
|
+
|
|
162
|
+
for (const item of items) {
|
|
163
|
+
if (count >= maxItems) {
|
|
164
|
+
tree += `\n... and ${items.length - count} more items`;
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const indent = ' '.repeat(item.depth);
|
|
169
|
+
const icon = item.type === 'dir' ? '📁' : '📄';
|
|
170
|
+
tree += `${indent}${icon} ${item.name}\n`;
|
|
171
|
+
count++;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return tree;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Load priority files
|
|
179
|
+
*/
|
|
180
|
+
loadPriorityFiles() {
|
|
181
|
+
for (const file of PRIORITY_FILES) {
|
|
182
|
+
const filePath = path.join(this.projectDir, file);
|
|
183
|
+
if (fs.existsSync(filePath)) {
|
|
184
|
+
const result = this.fileManager.readFile(filePath);
|
|
185
|
+
if (result.success) {
|
|
186
|
+
this.loadedFiles.set(file, result.content);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Load a specific file into context
|
|
194
|
+
*/
|
|
195
|
+
loadFile(filePath) {
|
|
196
|
+
const relativePath = path.isAbsolute(filePath)
|
|
197
|
+
? path.relative(this.projectDir, filePath)
|
|
198
|
+
: filePath;
|
|
199
|
+
|
|
200
|
+
if (this.loadedFiles.has(relativePath)) {
|
|
201
|
+
return { success: true, alreadyLoaded: true };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const result = this.fileManager.readFile(filePath);
|
|
205
|
+
if (result.success) {
|
|
206
|
+
this.loadedFiles.set(relativePath, result.content);
|
|
207
|
+
return { success: true, path: relativePath };
|
|
208
|
+
}
|
|
209
|
+
return result;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Unload a file from context
|
|
214
|
+
*/
|
|
215
|
+
unloadFile(filePath) {
|
|
216
|
+
const relativePath = path.isAbsolute(filePath)
|
|
217
|
+
? path.relative(this.projectDir, filePath)
|
|
218
|
+
: filePath;
|
|
219
|
+
|
|
220
|
+
if (this.loadedFiles.has(relativePath)) {
|
|
221
|
+
this.loadedFiles.delete(relativePath);
|
|
222
|
+
return { success: true };
|
|
223
|
+
}
|
|
224
|
+
return { success: false, error: 'File not in context' };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Reload a file in context (for watch mode)
|
|
229
|
+
*/
|
|
230
|
+
reloadFile(filePath) {
|
|
231
|
+
const relativePath = path.isAbsolute(filePath)
|
|
232
|
+
? path.relative(this.projectDir, filePath)
|
|
233
|
+
: filePath;
|
|
234
|
+
|
|
235
|
+
if (!this.loadedFiles.has(relativePath)) {
|
|
236
|
+
return { success: false, error: 'File not in context' };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const result = this.fileManager.readFile(filePath);
|
|
240
|
+
if (result.success) {
|
|
241
|
+
this.loadedFiles.set(relativePath, result.content);
|
|
242
|
+
return { success: true, path: relativePath };
|
|
243
|
+
}
|
|
244
|
+
return result;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Get list of loaded files
|
|
249
|
+
*/
|
|
250
|
+
getLoadedFiles() {
|
|
251
|
+
return Array.from(this.loadedFiles.keys());
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Clear all loaded files
|
|
256
|
+
*/
|
|
257
|
+
clearFiles() {
|
|
258
|
+
this.loadedFiles.clear();
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Build the full context string for AI
|
|
263
|
+
*/
|
|
264
|
+
buildContext() {
|
|
265
|
+
// Load gitignore patterns
|
|
266
|
+
this.customIgnores = [...this.customIgnores, ...this.loadGitignore()];
|
|
267
|
+
|
|
268
|
+
// Load priority files first
|
|
269
|
+
this.loadPriorityFiles();
|
|
270
|
+
|
|
271
|
+
// Scan directory structure
|
|
272
|
+
const structure = this.scanDirectory();
|
|
273
|
+
const tree = this.buildTreeString(structure);
|
|
274
|
+
|
|
275
|
+
// Build context string
|
|
276
|
+
let context = '';
|
|
277
|
+
|
|
278
|
+
// Project info
|
|
279
|
+
const packageJson = this.loadedFiles.get('package.json');
|
|
280
|
+
if (packageJson) {
|
|
281
|
+
try {
|
|
282
|
+
const pkg = JSON.parse(packageJson);
|
|
283
|
+
context += `## Project: ${pkg.name || 'Unknown'}\n`;
|
|
284
|
+
if (pkg.description) context += `Description: ${pkg.description}\n`;
|
|
285
|
+
context += '\n';
|
|
286
|
+
} catch {
|
|
287
|
+
// Invalid JSON, skip
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Directory structure
|
|
292
|
+
context += `## Project Structure\n\`\`\`\n${tree}\`\`\`\n\n`;
|
|
293
|
+
|
|
294
|
+
// Loaded files
|
|
295
|
+
context += `## Files in Context (${this.loadedFiles.size})\n\n`;
|
|
296
|
+
|
|
297
|
+
for (const [filePath, content] of this.loadedFiles) {
|
|
298
|
+
const ext = path.extname(filePath).slice(1) || 'txt';
|
|
299
|
+
context += `### ${filePath}\n\`\`\`${ext}\n${content}\n\`\`\`\n\n`;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return context;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Get a summary of the context (for display)
|
|
307
|
+
*/
|
|
308
|
+
getSummary() {
|
|
309
|
+
const structure = this.scanDirectory();
|
|
310
|
+
const files = structure.filter(i => i.type === 'file');
|
|
311
|
+
const dirs = structure.filter(i => i.type === 'dir');
|
|
312
|
+
const sourceFiles = files.filter(f => f.isSource);
|
|
313
|
+
|
|
314
|
+
return {
|
|
315
|
+
totalFiles: files.length,
|
|
316
|
+
totalDirs: dirs.length,
|
|
317
|
+
sourceFiles: sourceFiles.length,
|
|
318
|
+
loadedFiles: this.loadedFiles.size,
|
|
319
|
+
loadedFilesList: this.getLoadedFiles()
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
module.exports = ContextBuilder;
|