[Architecture] G1nation V2 Refactor
This commit is contained in:
+305
@@ -0,0 +1,305 @@
|
||||
import * as vscode from 'vscode';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
import axios from 'axios';
|
||||
import {
|
||||
getConfig,
|
||||
_getBrainDir,
|
||||
EXCLUDED_DIRS,
|
||||
MAX_CONTEXT_SIZE,
|
||||
MAX_AUTO_AGENT_STEPS,
|
||||
SYSTEM_PROMPT,
|
||||
shouldAutoPushBrain,
|
||||
getSecondBrainRepo
|
||||
} from './utils';
|
||||
import { validatePath, sanitizeCommand } from './security';
|
||||
|
||||
export interface ChatMessage {
|
||||
role: 'user' | 'assistant' | 'system';
|
||||
content: string | any[];
|
||||
}
|
||||
|
||||
export class AgentExecutor {
|
||||
constructor(
|
||||
private context: vscode.ExtensionContext,
|
||||
private webview: vscode.Webview | undefined,
|
||||
private chatHistory: ChatMessage[],
|
||||
private abortController: AbortController | null
|
||||
) {}
|
||||
|
||||
public async handlePrompt(
|
||||
prompt: string | null,
|
||||
modelName: string,
|
||||
options: {
|
||||
internetEnabled?: boolean,
|
||||
brainEnabled?: boolean,
|
||||
loopDepth?: number,
|
||||
visionContent?: any[],
|
||||
temperature?: number,
|
||||
systemPrompt?: string
|
||||
}
|
||||
) {
|
||||
const {
|
||||
internetEnabled = false,
|
||||
brainEnabled = false,
|
||||
loopDepth = 0,
|
||||
visionContent,
|
||||
temperature = 0.7,
|
||||
systemPrompt = SYSTEM_PROMPT
|
||||
} = options;
|
||||
|
||||
if (!this.webview) return;
|
||||
|
||||
try {
|
||||
// 1. Prepare Context
|
||||
const workspaceFolders = vscode.workspace.workspaceFolders;
|
||||
const rootPath = workspaceFolders ? workspaceFolders[0].uri.fsPath : '';
|
||||
|
||||
let contextBlock = '';
|
||||
const editor = vscode.window.activeTextEditor;
|
||||
if (editor && editor.document.uri.scheme === 'file') {
|
||||
const text = editor.document.getText();
|
||||
const name = path.basename(editor.document.fileName);
|
||||
if (text.trim().length > 0 && text.length < MAX_CONTEXT_SIZE) {
|
||||
contextBlock = `\n\n[Currently open file: ${name}]\n\`\`\`\n${text}\n\`\`\``;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Setup History
|
||||
if (prompt !== null) {
|
||||
this.chatHistory.push({ role: 'user', content: prompt });
|
||||
this.webview.postMessage({ type: 'streamChunk', value: '' }); // Trigger UI update if needed
|
||||
}
|
||||
|
||||
// 3. API Request Setup
|
||||
const { ollamaUrl, defaultModel, timeout } = getConfig();
|
||||
const reqMessages = [...this.chatHistory];
|
||||
|
||||
// Handle Vision Content Injection
|
||||
if (visionContent && reqMessages.length > 0) {
|
||||
const lastUserIdx = reqMessages.map(m => m.role).lastIndexOf('user');
|
||||
if (lastUserIdx >= 0) {
|
||||
reqMessages[lastUserIdx] = { role: 'user', content: visionContent };
|
||||
}
|
||||
}
|
||||
|
||||
// Inject System Directives
|
||||
if (reqMessages.length > 0) {
|
||||
const internetCtx = internetEnabled
|
||||
? `\n\n[CRITICAL: INTERNET ACCESS ENABLED]\nYou can use <read_url> to search. Current time: ${new Date().toLocaleString()}`
|
||||
: '';
|
||||
const fullSystemPrompt = `${systemPrompt}${internetCtx}\n\n[CONTEXT]\n${contextBlock}\n${internetCtx}`;
|
||||
|
||||
const firstUserIdx = reqMessages.findIndex(m => m.role === 'user');
|
||||
if (firstUserIdx >= 0) {
|
||||
let content = reqMessages[firstUserIdx].content;
|
||||
if (typeof content === 'string') {
|
||||
reqMessages[firstUserIdx].content = `${fullSystemPrompt}\n\n[USER QUERY]\n${content}`;
|
||||
if (loopDepth > 0) {
|
||||
reqMessages[firstUserIdx].content = `[Autonomous Step ${loopDepth}/${MAX_AUTO_AGENT_STEPS}]\n${reqMessages[firstUserIdx].content}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Call AI Engine
|
||||
const isLMStudio = ollamaUrl.includes('1234') || ollamaUrl.includes('v1');
|
||||
const apiUrl = isLMStudio ? `${ollamaUrl}/v1/chat/completions` : `${ollamaUrl}/api/chat`;
|
||||
|
||||
const streamBody = {
|
||||
model: modelName || defaultModel,
|
||||
messages: reqMessages,
|
||||
stream: true,
|
||||
...(isLMStudio
|
||||
? { max_tokens: 4096, temperature }
|
||||
: { options: { num_ctx: 16384, num_predict: 4096, temperature } }),
|
||||
};
|
||||
|
||||
if (loopDepth === 0) this.webview.postMessage({ type: 'streamStart' });
|
||||
|
||||
const response = await axios.post(apiUrl, streamBody, {
|
||||
timeout,
|
||||
responseType: 'stream',
|
||||
signal: this.abortController?.signal
|
||||
});
|
||||
|
||||
let aiResponseText = '';
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const stream = response.data;
|
||||
let buffer = '';
|
||||
stream.on('data', (chunk: Buffer) => {
|
||||
buffer += chunk.toString();
|
||||
const lines = buffer.split('\n');
|
||||
buffer = lines.pop() || '';
|
||||
for (const line of lines) {
|
||||
if (!line.trim() || line.trim() === 'data: [DONE]') continue;
|
||||
try {
|
||||
const raw = line.startsWith('data: ') ? line.slice(6) : line;
|
||||
const json = JSON.parse(raw);
|
||||
const token = isLMStudio ? json.choices?.[0]?.delta?.content || '' : json.message?.content || '';
|
||||
if (token) {
|
||||
aiResponseText += token;
|
||||
this.webview?.postMessage({ type: 'streamChunk', value: token });
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
});
|
||||
stream.on('end', () => resolve());
|
||||
stream.on('error', (err: any) => reject(err));
|
||||
});
|
||||
|
||||
if (loopDepth === 0) this.webview.postMessage({ type: 'streamEnd' });
|
||||
this.chatHistory.push({ role: 'assistant', content: aiResponseText });
|
||||
|
||||
// 5. Execute Actions
|
||||
const report = await this.executeActions(aiResponseText, rootPath);
|
||||
|
||||
if (report.length > 0) {
|
||||
const reportMsg = `\n\n---\n**[Agent Action Report] (${loopDepth + 1}/${MAX_AUTO_AGENT_STEPS})**\n${report.join("\n")}`;
|
||||
this.webview.postMessage({ type: 'streamChunk', value: reportMsg });
|
||||
|
||||
// Continue loop if needed
|
||||
if (loopDepth < MAX_AUTO_AGENT_STEPS) {
|
||||
const currentActionStr = report.join('|');
|
||||
const lastActionStr = this.context.workspaceState.get<string>('lastActionStr');
|
||||
|
||||
if (currentActionStr === lastActionStr) {
|
||||
this.webview.postMessage({ type: "streamChunk", value: "\n\n**[Loop Detected]** AI is repeating actions. Diverging..." });
|
||||
this.chatHistory.push({ role: "user", content: "[System] Action repeated. Try a different strategy." });
|
||||
}
|
||||
|
||||
await this.context.workspaceState.update('lastActionStr', currentActionStr);
|
||||
this.webview.postMessage({ type: 'autoContinue', value: `Thinking... (${loopDepth + 1}/${MAX_AUTO_AGENT_STEPS})` });
|
||||
|
||||
await new Promise(r => setTimeout(r, 800));
|
||||
await this.handlePrompt(null, modelName, { ...options, loopDepth: loopDepth + 1 });
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error: any) {
|
||||
this.webview.postMessage({ type: "error", value: `[Agent Error]: ${error.message}` });
|
||||
}
|
||||
}
|
||||
|
||||
private async executeActions(aiMessage: string, rootPath: string): Promise<string[]> {
|
||||
const report: string[] = [];
|
||||
let brainModified = false;
|
||||
let firstCreatedFile: string | undefined;
|
||||
|
||||
// Action 1: Create File
|
||||
const createRegex = /<create_file\s+path=['"]?([^'"]+)['"]?>([\s\S]*?)<\/create_file>/gi;
|
||||
let match;
|
||||
while ((match = createRegex.exec(aiMessage)) !== null) {
|
||||
const relPath = match[1].trim();
|
||||
const content = match[2].trim();
|
||||
try {
|
||||
const absPath = validatePath(rootPath, relPath);
|
||||
fs.mkdirSync(path.dirname(absPath), { recursive: true });
|
||||
fs.writeFileSync(absPath, content, 'utf-8');
|
||||
report.push(`✅ Created: ${relPath}`);
|
||||
if (!firstCreatedFile) firstCreatedFile = absPath;
|
||||
if (absPath.startsWith(_getBrainDir())) brainModified = true;
|
||||
} catch (err: any) { report.push(`❌ Error Creating ${relPath}: ${err.message}`); }
|
||||
}
|
||||
|
||||
// Action 2: Edit File
|
||||
const editRegex = /<edit_file\s+path=['"]?([^'"]+)['"]?>([\s\S]*?)<\/edit_file>/gi;
|
||||
while ((match = editRegex.exec(aiMessage)) !== null) {
|
||||
const relPath = match[1].trim();
|
||||
const editContent = match[2].trim();
|
||||
try {
|
||||
const absPath = validatePath(rootPath, relPath);
|
||||
if (fs.existsSync(absPath)) {
|
||||
let currentContent = fs.readFileSync(absPath, 'utf-8');
|
||||
const searchMatch = editContent.match(/<search>([\s\S]*?)<\/search>\s*<replace>([\s\S]*?)<\/replace>/i);
|
||||
if (searchMatch) {
|
||||
const searchStr = searchMatch[1];
|
||||
const replaceStr = searchMatch[2];
|
||||
if (currentContent.includes(searchStr)) {
|
||||
currentContent = currentContent.replace(searchStr, replaceStr);
|
||||
fs.writeFileSync(absPath, currentContent, 'utf-8');
|
||||
report.push(`📝 Updated: ${relPath}`);
|
||||
} else {
|
||||
report.push(`⚠️ Search string not found in ${relPath}`);
|
||||
}
|
||||
} else {
|
||||
fs.writeFileSync(absPath, editContent, 'utf-8');
|
||||
report.push(`📝 Updated (Full): ${relPath}`);
|
||||
}
|
||||
if (absPath.startsWith(_getBrainDir())) brainModified = true;
|
||||
} else {
|
||||
report.push(`❌ File not found: ${relPath}`);
|
||||
}
|
||||
} catch (err: any) { report.push(`❌ Error Editing ${relPath}: ${err.message}`); }
|
||||
}
|
||||
|
||||
// Action 3: Read File
|
||||
const readRegex = /<read_file\s+path=['"]?([^'"]+)['"]?\s*\/?>(?:<\/read_file>)?/gi;
|
||||
while ((match = readRegex.exec(aiMessage)) !== null) {
|
||||
const relPath = match[1].trim();
|
||||
try {
|
||||
const absPath = validatePath(rootPath, relPath);
|
||||
if (fs.existsSync(absPath)) {
|
||||
const content = fs.readFileSync(absPath, 'utf-8');
|
||||
const preview = content.length > 8000 ? content.slice(0, 8000) + "\n... (truncated)" : content;
|
||||
report.push(`📖 Read: ${relPath}`);
|
||||
this.chatHistory.push({ role: 'user', content: `[Result of read_file ${relPath}]\n\`\`\`\n${preview}\n\`\`\`` });
|
||||
} else {
|
||||
report.push(`❌ Read failed: ${relPath} not found`);
|
||||
}
|
||||
} catch (err: any) { report.push(`❌ Error Reading ${relPath}: ${err.message}`); }
|
||||
}
|
||||
|
||||
// Action 4: Run Command
|
||||
const cmdRegex = /<run_command>([\s\S]*?)<\/run_command>/gi;
|
||||
while ((match = cmdRegex.exec(aiMessage)) !== null) {
|
||||
const cmd = match[1].trim();
|
||||
try {
|
||||
const safeCmd = sanitizeCommand(cmd);
|
||||
const terminal = vscode.window.terminals.find(t => t.name === 'G1nation Terminal') || vscode.window.createTerminal({ name: 'G1nation Terminal', cwd: rootPath });
|
||||
terminal.show();
|
||||
terminal.sendText(safeCmd);
|
||||
report.push(`🚀 Executed: ${safeCmd}`);
|
||||
} catch (err: any) { report.push(`❌ Blocked: ${err.message}`); }
|
||||
}
|
||||
|
||||
// Action 5: List Files
|
||||
const listRegex = /<list_files\s+path=['"]?([^'"]+)['"]?\s*\/?>(?:<\/list_files>)?/gi;
|
||||
while ((match = listRegex.exec(aiMessage)) !== null) {
|
||||
const relPath = match[1].trim() || '.';
|
||||
try {
|
||||
const absPath = validatePath(rootPath, relPath);
|
||||
if (fs.existsSync(absPath) && fs.statSync(absPath).isDirectory()) {
|
||||
const entries = fs.readdirSync(absPath, { withFileTypes: true });
|
||||
const listing = entries
|
||||
.filter(e => !e.name.startsWith('.') && !EXCLUDED_DIRS.has(e.name))
|
||||
.map(e => e.isDirectory() ? `${e.name}/` : e.name)
|
||||
.join('\n');
|
||||
report.push(`📂 Listed: ${relPath}`);
|
||||
this.chatHistory.push({ role: 'user', content: `[Result of list_files ${relPath}]\n${listing}` });
|
||||
}
|
||||
} catch (err: any) { report.push(`❌ Listing failed: ${err.message}`); }
|
||||
}
|
||||
|
||||
if (firstCreatedFile) {
|
||||
vscode.window.showTextDocument(vscode.Uri.file(firstCreatedFile), { preview: false });
|
||||
}
|
||||
|
||||
// Brain Sync Logic
|
||||
if (brainModified && shouldAutoPushBrain() && getSecondBrainRepo()) {
|
||||
this.syncBrain();
|
||||
}
|
||||
|
||||
return report;
|
||||
}
|
||||
|
||||
private syncBrain() {
|
||||
try {
|
||||
const brainDir = _getBrainDir();
|
||||
const { execSync } = require('child_process');
|
||||
execSync(`git add .`, { cwd: brainDir });
|
||||
execSync(`git commit -m "[G1nation] Knowledge Update"`, { cwd: brainDir });
|
||||
execSync(`git push`, { cwd: brainDir });
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
+160
@@ -0,0 +1,160 @@
|
||||
import * as http from 'http';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import axios from 'axios';
|
||||
import { getConfig, _getBrainDir, _isBrainDirExplicitlySet, findBrainFiles } from './utils';
|
||||
|
||||
export interface BridgeInterface {
|
||||
injectSystemMessage(msg: string): void;
|
||||
getHistoryText(): string;
|
||||
sendPromptFromExtension(prompt: string): void;
|
||||
brainEnabled: boolean;
|
||||
findBrainFiles(dir: string): string[];
|
||||
}
|
||||
|
||||
export class BridgeServer {
|
||||
private server: http.Server | null = null;
|
||||
|
||||
constructor(private provider: BridgeInterface) {}
|
||||
|
||||
public start(port: number = 4825) {
|
||||
this.server = http.createServer((req, res) => {
|
||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
||||
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
||||
|
||||
if (req.method === 'OPTIONS') {
|
||||
res.writeHead(200);
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
|
||||
const url = req.url || '';
|
||||
|
||||
if (req.method === 'GET' && url === '/ping') {
|
||||
this.handlePing(res);
|
||||
} else if (req.method === 'POST' && url === '/api/exam') {
|
||||
this.handlePost(req, res, this.processExam.bind(this));
|
||||
} else if (req.method === 'POST' && url === '/api/evaluate') {
|
||||
this.handlePost(req, res, this.processEvaluate.bind(this));
|
||||
} else if (req.method === 'GET' && url === '/api/evaluate-history') {
|
||||
this.processEvaluateHistory(res);
|
||||
} else if (req.method === 'POST' && url === '/api/brain-inject') {
|
||||
this.handlePost(req, res, this.processBrainInject.bind(this));
|
||||
} else {
|
||||
res.writeHead(404);
|
||||
res.end();
|
||||
}
|
||||
});
|
||||
|
||||
this.server.listen(port, '127.0.0.1', () => {
|
||||
console.log(`[G1nation] Bridge Server active on port ${port}`);
|
||||
});
|
||||
}
|
||||
|
||||
private handlePing(res: http.ServerResponse) {
|
||||
const brainDir = _getBrainDir();
|
||||
const brainCount = fs.existsSync(brainDir) ? findBrainFiles(brainDir).length : 0;
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({
|
||||
status: 'ok',
|
||||
msg: 'G1nation Bridge Ready',
|
||||
config: getConfig(),
|
||||
brain: { fileCount: brainCount, enabled: this.provider.brainEnabled }
|
||||
}));
|
||||
}
|
||||
|
||||
private handlePost(req: http.IncomingMessage, res: http.ServerResponse, processor: (data: any, res: http.ServerResponse) => Promise<void>) {
|
||||
let body = '';
|
||||
req.on('data', chunk => body += chunk.toString());
|
||||
req.on('end', async () => {
|
||||
try {
|
||||
const parsed = JSON.parse(body);
|
||||
await processor(parsed, res);
|
||||
} catch (e: any) {
|
||||
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: e.message }));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async processExam(data: any, res: http.ServerResponse) {
|
||||
const prompt = data.prompt || 'Automatic Prompt Received';
|
||||
this.provider.sendPromptFromExtension(`[Bridge Input] ${prompt}`);
|
||||
const result = await this.callAI(prompt);
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ success: true, rawOutput: result }));
|
||||
}
|
||||
|
||||
private async processEvaluate(data: any, res: http.ServerResponse) {
|
||||
const prompt = data.prompt || '';
|
||||
this.provider.injectSystemMessage(`**[A.U Evaluation Started]**\nAnalyzing input: _"${prompt.substring(0, 60)}..."_`);
|
||||
|
||||
const evaluationPrompt = `[EVALUATION REQUEST]\nPlease evaluate the following input and provide a score/reasoning:\n\n${prompt}`;
|
||||
const result = await this.callAI(evaluationPrompt);
|
||||
|
||||
this.provider.injectSystemMessage(`**[Evaluation Complete]**\n${result.substring(0, 300)}...`);
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ rawOutput: result }));
|
||||
}
|
||||
|
||||
private async processEvaluateHistory(res: http.ServerResponse) {
|
||||
const historyText = this.provider.getHistoryText();
|
||||
if (!historyText || historyText.length < 50) {
|
||||
res.writeHead(400, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: "Insufficient chat history for evaluation." }));
|
||||
return;
|
||||
}
|
||||
|
||||
this.provider.injectSystemMessage(`**[History Evaluation]** Analyzing conversation flow...`);
|
||||
const historyPrompt = `Analyze this conversation history and return a JSON score for Math, Logic, Creative, and Code (0-100):\n\n${historyText.slice(-6000)}`;
|
||||
const result = await this.callAI(historyPrompt);
|
||||
|
||||
const jsonMatch = result.match(/\{[\s\S]*?\}/);
|
||||
if (jsonMatch) {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(jsonMatch[0]);
|
||||
} else {
|
||||
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: "Failed to parse evaluation JSON", raw: result }));
|
||||
}
|
||||
}
|
||||
|
||||
private async processBrainInject(data: any, res: http.ServerResponse) {
|
||||
const { title, markdown, prompt } = data;
|
||||
let brainDir = _getBrainDir();
|
||||
|
||||
if (!fs.existsSync(brainDir)) {
|
||||
fs.mkdirSync(brainDir, { recursive: true });
|
||||
}
|
||||
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
const datePath = path.join(brainDir, '00_Raw', today);
|
||||
fs.mkdirSync(datePath, { recursive: true });
|
||||
|
||||
const safeTitle = title.replace(/[^a-zA-Z0-9가-힣]/gi, '_');
|
||||
const filePath = path.join(datePath, `${safeTitle}.md`);
|
||||
fs.writeFileSync(filePath, markdown, 'utf-8');
|
||||
|
||||
this.provider.injectSystemMessage(`**[Brain Inject]** Knowledge captured: ${title}`);
|
||||
|
||||
const result = await this.callAI(prompt || `Analyze this new knowledge: ${title}`);
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ success: true, rawOutput: result }));
|
||||
}
|
||||
|
||||
private async callAI(prompt: string): Promise<string> {
|
||||
const config = getConfig();
|
||||
const isLMStudio = config.ollamaUrl.includes('1234') || config.ollamaUrl.includes('v1');
|
||||
const apiUrl = isLMStudio ? `${config.ollamaUrl}/v1/chat/completions` : `${config.ollamaUrl}/api/chat`;
|
||||
|
||||
const payload = {
|
||||
model: config.defaultModel,
|
||||
messages: [{ role: 'user', content: prompt }],
|
||||
stream: false
|
||||
};
|
||||
|
||||
const res = await axios.post(apiUrl, payload, { timeout: config.timeout });
|
||||
return isLMStudio ? (res.data.choices?.[0]?.message?.content || '') : (res.data.message?.content || '');
|
||||
}
|
||||
}
|
||||
+277
-2103
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,34 @@
|
||||
import * as vscode from 'vscode';
|
||||
import * as path from 'path';
|
||||
|
||||
/**
|
||||
* Validates that a path is within the workspace.
|
||||
* Prevents Path Traversal attacks.
|
||||
*/
|
||||
export function validatePath(workspaceRoot: string, targetPath: string): string {
|
||||
const absolutePath = path.resolve(workspaceRoot, targetPath);
|
||||
if (!absolutePath.startsWith(workspaceRoot)) {
|
||||
throw new Error(`Security Violation: Path traversal detected! Attempted to access ${absolutePath} which is outside the workspace ${workspaceRoot}`);
|
||||
}
|
||||
return absolutePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitizes terminal commands to prevent destructive actions.
|
||||
*/
|
||||
export function sanitizeCommand(command: string): string {
|
||||
const forbiddenPatterns = [
|
||||
/rm\s+-rf\s+\//,
|
||||
/mkfs/,
|
||||
/dd\s+if=/,
|
||||
/>\s*\/dev\/sd/,
|
||||
/:(){:|:&};:/ // Fork bomb
|
||||
];
|
||||
|
||||
for (const pattern of forbiddenPatterns) {
|
||||
if (pattern.test(command)) {
|
||||
throw new Error(`Security Violation: Destructive command pattern detected! Blocked: ${command}`);
|
||||
}
|
||||
}
|
||||
return command;
|
||||
}
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
import * as vscode from 'vscode';
|
||||
import * as path from 'path';
|
||||
import * as os from 'os';
|
||||
|
||||
export const EXCLUDED_DIRS = new Set([
|
||||
'node_modules', '.git', '.vscode', 'out', 'dist', 'build',
|
||||
'.next', '.cache', '__pycache__', '.DS_Store', 'coverage',
|
||||
'.turbo', '.nuxt', '.output', 'vendor', 'target'
|
||||
]);
|
||||
|
||||
export const MAX_CONTEXT_SIZE = 12_000;
|
||||
export const MAX_AUTO_AGENT_STEPS = 50;
|
||||
|
||||
export function getConfig() {
|
||||
const cfg = vscode.workspace.getConfiguration('g1nation');
|
||||
return {
|
||||
ollamaUrl: cfg.get<string>('ollamaUrl', 'http://127.0.0.1:11434'),
|
||||
defaultModel: cfg.get<string>('defaultModel', 'gemma4:e2b'),
|
||||
maxTreeFiles: 200,
|
||||
timeout: cfg.get<number>('requestTimeout', 300) * 1000,
|
||||
localBrainPath: cfg.get<string>('localBrainPath', '')
|
||||
};
|
||||
}
|
||||
|
||||
export function shouldAutoPushBrain(): boolean {
|
||||
const cfg = vscode.workspace.getConfiguration('g1nation');
|
||||
return cfg.get<boolean>('autoPushBrain', false);
|
||||
}
|
||||
|
||||
export function getSecondBrainRepo(): string {
|
||||
const cfg = vscode.workspace.getConfiguration('g1nation');
|
||||
return cfg.get<string>('secondBrainRepo', '');
|
||||
}
|
||||
|
||||
export function _getBrainDir(): string {
|
||||
const { localBrainPath } = getConfig();
|
||||
if (localBrainPath && localBrainPath.trim() !== '') {
|
||||
if (localBrainPath.startsWith('~/')) {
|
||||
return path.join(os.homedir(), localBrainPath.substring(2));
|
||||
}
|
||||
return localBrainPath.trim();
|
||||
}
|
||||
return path.join(os.homedir(), '.g1nation-brain');
|
||||
}
|
||||
|
||||
export function _isBrainDirExplicitlySet(): boolean {
|
||||
const { localBrainPath } = getConfig();
|
||||
return !!(localBrainPath && localBrainPath.trim() !== '');
|
||||
}
|
||||
|
||||
export function isTextAttachment(fileName: string, mimeType: string): boolean {
|
||||
const lower = fileName.toLowerCase();
|
||||
const textExtensions = [
|
||||
'.txt', '.md', '.csv', '.json', '.js', '.ts', '.jsx', '.tsx',
|
||||
'.html', '.css', '.py', '.java', '.rs', '.go', '.yaml', '.yml',
|
||||
'.xml', '.toml', '.sql', '.sh'
|
||||
];
|
||||
return mimeType.startsWith('text/')
|
||||
|| mimeType === 'application/json'
|
||||
|| textExtensions.some((ext) => lower.endsWith(ext));
|
||||
}
|
||||
|
||||
export function findBrainFiles(dir: string): string[] {
|
||||
let results: string[] = [];
|
||||
if (!fs.existsSync(dir)) return results;
|
||||
const list = fs.readdirSync(dir);
|
||||
list.forEach((file) => {
|
||||
const filePath = path.join(dir, file);
|
||||
const stat = fs.statSync(filePath);
|
||||
if (stat && stat.isDirectory()) {
|
||||
if (!EXCLUDED_DIRS.has(file)) {
|
||||
results = results.concat(findBrainFiles(filePath));
|
||||
}
|
||||
} else if (file.endsWith('.md')) {
|
||||
results.push(filePath);
|
||||
}
|
||||
});
|
||||
return results;
|
||||
}
|
||||
|
||||
export const SYSTEM_PROMPT = `You are "G1nation", a premium agentic AI coding assistant running 100% offline on the user's machine.
|
||||
You are DIRECTLY CONNECTED to the user's local file system and terminal. You MUST use the action tags below to create, edit, delete, read files and run commands. DO NOT just show code - ALWAYS wrap it in the appropriate action tag so it gets executed.
|
||||
|
||||
You have EIGHT powerful agent actions:
|
||||
|
||||
[ACTION 1: CREATE NEW FILES]
|
||||
<create_file path="relative/path/file.ext">
|
||||
file content here
|
||||
</create_file>
|
||||
|
||||
[ACTION 2: EDIT EXISTING FILES]
|
||||
<edit_file path="relative/path/file.ext">
|
||||
<find>exact text to find</find>
|
||||
<replace>replacement text</replace>
|
||||
</edit_file>
|
||||
|
||||
[ACTION 3: DELETE FILES]
|
||||
<delete_file path="relative/path/file.ext"/>
|
||||
|
||||
[ACTION 4: READ FILES]
|
||||
<read_file path="relative/path/file.ext"/>
|
||||
|
||||
[ACTION 5: LIST DIRECTORY]
|
||||
<list_files path="relative/path/to/dir"/>
|
||||
|
||||
[ACTION 6: RUN TERMINAL COMMANDS]
|
||||
<run_command>npm install express</run_command>
|
||||
|
||||
[ACTION 7: READ USER'S SECOND BRAIN]
|
||||
<read_brain>filename.md</read_brain>
|
||||
|
||||
[ACTION 8: READ WEBSITES & SEARCH INTERNET]
|
||||
<read_url>https://html.duckduckgo.com/html/?q=YOUR+SEARCH+QUERY</read_url>
|
||||
|
||||
CRITICAL RULES:
|
||||
1. ALWAYS respond in the same language the user uses.
|
||||
2. You MUST use action tags for any file/terminal operations.
|
||||
3. Be concise and professional.
|
||||
4. File paths are RELATIVE to the workspace.`;
|
||||
Reference in New Issue
Block a user