import fs from 'fs/promises';
import path from 'path';
import { spawn } from 'child_process';
export interface ContextResult {
file: string;
line: number;
radius: number;
snippet: string;
numbered_lines: string[];
valid_path: boolean;
}
export interface ProjectMapResult {
scripts: string[];
scenes: string[];
data: string[];
}
export class Context {
private projectRoot: string;
constructor(projectRoot: string) {
this.projectRoot = projectRoot;
}
public async getContext(file: string, line: number, radius: number = 20): Promise<ContextResult> {
const fullPath = path.join(this.projectRoot, file.replace('res://', ''));
try {
const content = await fs.readFile(fullPath, 'utf8');
const lines = content.split('\n');
const startLine = Math.max(0, line - radius - 1);
const endLine = Math.min(lines.length, line + radius);
const contextLines = lines.slice(startLine, endLine);
const numberedLines = contextLines.map((lineContent, index) => {
const lineNum = startLine + index + 1;
const marker = lineNum === line ? '→' : ' ';
return `${marker}${lineNum.toString().padStart(4)}: ${lineContent}`;
});
return {
file,
line,
radius,
snippet: contextLines.join('\n'),
numbered_lines: numberedLines,
valid_path: true
};
} catch (error) {
return {
file,
line,
radius,
snippet: '',
numbered_lines: [],
valid_path: false
};
}
}
public async readFile(filePath: string): Promise<{ content: string; valid: boolean }> {
// Security check: restrict to project root
const resolvedPath = path.resolve(this.projectRoot, filePath.replace('res://', ''));
const rootPath = path.resolve(this.projectRoot);
if (!resolvedPath.startsWith(rootPath)) {
throw new Error('Access denied: path outside project root');
}
try {
const content = await fs.readFile(resolvedPath, 'utf8');
return { content, valid: true };
} catch (error) {
return { content: '', valid: false };
}
}
public async writeFile(filePath: string, content: string): Promise<{ success: boolean; error?: string }> {
// Security check: restrict to project root
const resolvedPath = path.resolve(this.projectRoot, filePath.replace('res://', ''));
const rootPath = path.resolve(this.projectRoot);
if (!resolvedPath.startsWith(rootPath)) {
throw new Error('Access denied: path outside project root');
}
try {
// Ensure directory exists
const dir = path.dirname(resolvedPath);
await fs.mkdir(dir, { recursive: true });
await fs.writeFile(resolvedPath, content, 'utf8');
return { success: true };
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : String(error)
};
}
}
public async listMovesets(): Promise<{ movesets: string[] }> {
try {
const movesetDir = path.join(this.projectRoot, 'data', 'movesets');
const files = await fs.readdir(movesetDir);
const movesets = files
.filter(file => file.endsWith('.json'))
.map(file => path.basename(file, '.json'));
return { movesets };
} catch (error) {
return { movesets: [] };
}
}
public async readMoveset(name: string): Promise<{ moveset: any; valid: boolean }> {
try {
const movesetPath = path.join(this.projectRoot, 'data', 'movesets', `${name}.json`);
const content = await fs.readFile(movesetPath, 'utf8');
const moveset = JSON.parse(content);
return { moveset, valid: true };
} catch (error) {
return { moveset: null, valid: false };
}
}
public async writeMoveset(name: string, movesetData: any): Promise<{ success: boolean; error?: string }> {
try {
const movesetDir = path.join(this.projectRoot, 'data', 'movesets');
await fs.mkdir(movesetDir, { recursive: true });
const movesetPath = path.join(movesetDir, `${name}.json`);
const content = JSON.stringify(movesetData, null, 2);
await fs.writeFile(movesetPath, content, 'utf8');
return { success: true };
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : String(error)
};
}
}
public async getProjectMap(): Promise<ProjectMapResult> {
try {
const result: ProjectMapResult = {
scripts: [],
scenes: [],
data: []
};
// Get scripts
try {
result.scripts = await this.getFilesRecursive(
path.join(this.projectRoot, 'scripts'),
'.gd'
);
} catch (e) {
// Scripts directory might not exist
}
// Get scenes
try {
result.scenes = await this.getFilesRecursive(
path.join(this.projectRoot, 'scenes'),
'.tscn'
);
} catch (e) {
// Scenes directory might not exist
}
// Get data files
try {
result.data = await this.getFilesRecursive(
path.join(this.projectRoot, 'data'),
'.json'
);
} catch (e) {
// Data directory might not exist
}
return result;
} catch (error) {
return { scripts: [], scenes: [], data: [] };
}
}
public async getGitStatus(): Promise<string> {
try {
return await this.executeGit(['status', '--porcelain']);
} catch (error) {
return '';
}
}
public async getRecentDiff(): Promise<string> {
try {
return await this.executeGit(['diff', 'HEAD~1..HEAD']);
} catch (error) {
return '';
}
}
private async getFilesRecursive(dir: string, extension: string): Promise<string[]> {
const files: string[] = [];
const processDir = async (currentDir: string, relativePath: string = '') => {
try {
const entries = await fs.readdir(currentDir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(currentDir, entry.name);
const relPath = path.join(relativePath, entry.name);
if (entry.isDirectory()) {
await processDir(fullPath, relPath);
} else if (entry.isFile() && entry.name.endsWith(extension)) {
files.push(relPath.replace(/\\/g, '/'));
}
}
} catch (error) {
// Ignore errors for individual directories
}
};
await processDir(dir);
return files.sort();
}
private async executeGit(args: string[]): Promise<string> {
return new Promise((resolve, reject) => {
const child = spawn('git', args, { cwd: this.projectRoot });
let stdout = '';
let stderr = '';
if (child.stdout) {
child.stdout.on('data', (data) => {
stdout += data.toString();
});
}
if (child.stderr) {
child.stderr.on('data', (data) => {
stderr += data.toString();
});
}
child.on('close', (code) => {
if (code === 0) {
resolve(stdout);
} else {
reject(new Error(stderr || `Git command failed with code ${code}`));
}
});
child.on('error', reject);
});
}
}