From 0e20dff1549b75930d06ecb490341ea3e7e7bcee Mon Sep 17 00:00:00 2001 From: yesung Date: Fri, 24 Apr 2026 18:23:21 +0900 Subject: [PATCH] [Architecture] G1nation V2 Refactor --- src/agent.ts | 305 ++++++ src/bridge.ts | 160 ++++ src/extension.ts | 2380 ++++++---------------------------------------- src/security.ts | 34 + src/utils.ts | 119 +++ 5 files changed, 895 insertions(+), 2103 deletions(-) create mode 100644 src/agent.ts create mode 100644 src/bridge.ts create mode 100644 src/security.ts create mode 100644 src/utils.ts diff --git a/src/agent.ts b/src/agent.ts new file mode 100644 index 0000000..e4241f8 --- /dev/null +++ b/src/agent.ts @@ -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 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((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('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 { + const report: string[] = []; + let brainModified = false; + let firstCreatedFile: string | undefined; + + // Action 1: Create File + const createRegex = /([\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 = /([\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(/([\s\S]*?)<\/search>\s*([\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>)?/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 = /([\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>)?/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 {} + } +} diff --git a/src/bridge.ts b/src/bridge.ts new file mode 100644 index 0000000..c8b19c3 --- /dev/null +++ b/src/bridge.ts @@ -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) { + 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 { + 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 || ''); + } +} diff --git a/src/extension.ts b/src/extension.ts index 1053070..73505b8 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,1934 +1,281 @@ -import * as vscode from 'vscode'; -import * as http from 'http'; -import axios from 'axios'; +import * as vscode from 'vscode'; import * as fs from 'fs'; import * as path from 'path'; -import * as os from 'os'; -import { exec } from 'child_process'; -import { promisify } from 'util'; +import axios from 'axios'; +import { + getConfig, + _getBrainDir, + _isBrainDirExplicitlySet, + findBrainFiles, + SYSTEM_PROMPT +} from './utils'; +import { AgentExecutor } from './agent'; +import { BridgeServer, BridgeInterface } from './bridge'; -const execAsync = promisify(exec); +/** + * G1nation Extension Entry Point + */ +export async function activate(context: vscode.ExtensionContext) { + console.log('G1nation extension activated.'); -// ============================================================ -// G1nation ??Full Agentic Local AI for VS Code -// 100% Offline 鸚?File Create 鸚?File Edit 鸚?Terminal 鸚?Multi-file Context -// ============================================================ + // 1. Ensure Brain Directory + await _ensureBrainDir(context); + + // 2. Initialize Agent Executor + const agent = new AgentExecutor(context); -// Settings are read from VS Code configuration (File > Preferences > Settings) -function getConfig() { - const cfg = vscode.workspace.getConfiguration('g1nation'); - return { - ollamaBase: cfg.get('ollamaUrl', 'http://127.0.0.1:11434'), - defaultModel: cfg.get('defaultModel', 'gemma4:e2b'), - maxTreeFiles: 200, - timeout: cfg.get('requestTimeout', 300) * 1000, - localBrainPath: cfg.get('localBrainPath', '') - }; + // 3. Initialize Sidebar Provider + const provider = new SidebarChatProvider(context.extensionUri, context, agent); + context.subscriptions.push( + vscode.window.registerWebviewViewProvider(SidebarChatProvider.viewType, provider) + ); + + // 4. Initialize Bridge Server (Port 4825) + const bridge = new BridgeServer(provider); + try { + bridge.start(); + console.log('G1nation Bridge Server started on port 4825'); + } catch (err) { + console.error('Failed to start Bridge Server:', err); + } + + // 5. Register Core Commands + context.subscriptions.push( + vscode.commands.registerCommand('g1nation.focusInput', () => { + provider.focusInput(); + }), + vscode.commands.registerCommand('g1nation.newChat', () => { + provider.clearChat(); + }), + vscode.commands.registerCommand('g1nation.syncBrain', async () => { + await provider.syncBrain(); + }) + ); + + // 6. First Run Setup / Auto-Detection + const isFirstRun = !context.globalState.get('setupComplete'); + if (isFirstRun) { + await runInitialSetup(context); + } + + vscode.window.showInformationMessage("G1nation V2 Activated 🫡"); } -function getSecondBrainRepo(): string { - return vscode.workspace.getConfiguration('g1nation').get('secondBrainRepo', '').trim(); -} +/** + * Initial Setup: Detect Local AI Engines (LM Studio / Ollama) + */ +async function runInitialSetup(context: vscode.ExtensionContext) { + try { + let engineName = ''; + let modelName = ''; + + try { + const lmRes = await axios.get('http://127.0.0.1:1234/v1/models', { timeout: 2000 }); + if (lmRes.data?.data?.length > 0) { + engineName = 'LM Studio'; + modelName = lmRes.data.data[0].id; + await vscode.workspace.getConfiguration('g1nation').update('ollamaUrl', 'http://127.0.0.1:1234', vscode.ConfigurationTarget.Global); + await vscode.workspace.getConfiguration('g1nation').update('defaultModel', modelName, vscode.ConfigurationTarget.Global); + } + } catch (err) {} -function shouldAutoPushBrain(): boolean { - return vscode.workspace.getConfiguration('g1nation').get('autoPushBrain', false); -} - -function _getBrainDir(): string { - const { localBrainPath } = getConfig(); - if (localBrainPath && localBrainPath.trim() !== '') { - if (localBrainPath.startsWith('~/')) { - return path.join(os.homedir(), localBrainPath.substring(2)); + if (!engineName) { + try { + const ollamaRes = await axios.get('http://127.0.0.1:11434/api/tags', { timeout: 2000 }); + if (ollamaRes.data?.models?.length > 0) { + engineName = 'Ollama'; + modelName = ollamaRes.data.models[0].name; + await vscode.workspace.getConfiguration('g1nation').update('ollamaUrl', 'http://127.0.0.1:11434', vscode.ConfigurationTarget.Global); + await vscode.workspace.getConfiguration('g1nation').update('defaultModel', modelName, vscode.ConfigurationTarget.Global); + } + } catch (err) {} } - return localBrainPath.trim(); + + context.globalState.update('setupComplete', true); + if (engineName) { + vscode.window.showInformationMessage(`Setup Complete: ${engineName} detected with model ${modelName}`); + } + } catch (e) { + context.globalState.update('setupComplete', true); } - return path.join(os.homedir(), '.g1nation-brain'); } -function _isBrainDirExplicitlySet(): boolean { - const { localBrainPath } = getConfig(); - return !!(localBrainPath && localBrainPath.trim() !== ''); -} - -async function _ensureBrainDir(): Promise { +async function _ensureBrainDir(context: vscode.ExtensionContext): Promise { if (_isBrainDirExplicitlySet()) { - return _getBrainDir(); - } - - const selectedFolders = await vscode.window.showOpenDialog({ - canSelectFolders: true, - canSelectFiles: false, - canSelectMany: false, - openLabel: '?대뜑 ?좏깮', - title: 'G1nation Second Brain ?대뜑 ?ㅼ젙' - }); - if (selectedFolders && selectedFolders.length > 0) { - const selectedPath = selectedFolders[0].fsPath; - await vscode.workspace.getConfiguration('g1nation').update('localBrainPath', selectedPath, vscode.ConfigurationTarget.Global); - vscode.window.showInformationMessage(`Second Brain ?대뜑媛€ ?ㅼ젙?섏뿀?듬땲?? ${selectedPath}`); - return selectedPath; + const dir = _getBrainDir(); + if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); + return dir; } const result = await vscode.window.showInformationMessage( - '?대뜑媛€ ?좏깮?섏? ?딆븯?듬땲?? ?ㅼ떆 ?좏깮?섏떆寃좎뒿?덇퉴?', - '?대뜑 ?좏깮' + 'G1nation needs a folder for your "Second Brain" knowledge base.', + 'Select Folder' ); - if (result !== '?대뜑 ?좏깮') return null; - - const folders = await vscode.window.showOpenDialog({ - canSelectFolders: true, canSelectFiles: false, canSelectMany: false, - openLabel: '?대뜑 ?좏깮', - title: 'Second Brain ?대뜑 ?ㅼ젙' - }); - if (!folders || folders.length === 0) return null; - - const selectedPath = folders[0].fsPath; - await vscode.workspace.getConfiguration('g1nation').update('localBrainPath', selectedPath, vscode.ConfigurationTarget.Global); - vscode.window.showInformationMessage(`Second Brain ?대뜑媛€ ?ㅼ젙?섏뿀?듬땲?? ${selectedPath}`); - return selectedPath; -} -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)); -} - -const EXCLUDED_DIRS = new Set([ - 'node_modules', '.git', '.vscode', 'out', 'dist', 'build', - '.next', '.cache', '__pycache__', '.DS_Store', 'coverage', - '.turbo', '.nuxt', '.output', 'vendor', 'target' -]); -const MAX_CONTEXT_SIZE = 12_000; // chars -const MAX_AUTO_AGENT_STEPS = 50; - -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 SEVEN powerful agent actions: - - [ACTION 1: CREATE NEW FILES] - -file content here - - - Example: user says 'index.html create': - - -Hello -

Hello World

- -
- - [ACTION 2: EDIT EXISTING FILES] - -exact text to find -replacement text - -You can have multiple / pairs inside one block. - -??⑨퐢???ACTION 3: DELETE FILES ??⑨퐢??? - - -??⑨퐢???ACTION 4: READ FILES ??⑨퐢??? - -Use this to read any file in the workspace BEFORE editing it. You will receive the file contents automatically. - -??⑨퐢???ACTION 5: LIST DIRECTORY ??⑨퐢??? - -Use this to see what files exist in a specific subdirectory. - -??⑨퐢???ACTION 6: RUN TERMINAL COMMANDS ??⑨퐢??? -npm install express - - Example: user says 'run server': -node server.js - - [ACTION 7: READ USER'S SECOND BRAIN] -filename.md -Use this to READ documents from the user's personal knowledge base. - - [ACTION 8: READ WEBSITES & SEARCH INTERNET] -https://example.com -To search the internet, you MUST use DuckDuckGo by formatting the URL like this: -https://html.duckduckgo.com/html/?q=YOUR+SEARCH+QUERY - Use this forcefully whenever asked for real-time info. - -CRITICAL RULES: -1. ALWAYS respond in the same language the user uses. -2. When the user asks to create, edit, delete files or run commands, you MUST use the action tags above. NEVER just show code without action tags. -3. Outside of action blocks, briefly explain what you did. -4. For code that is ONLY for explanation (not to be saved), use standard markdown code fences. -5. Be concise, professional, and helpful. -6. When editing files, FIRST use to read the file, then use with exact matching text. -7. When a SECOND BRAIN INDEX is available, ALWAYS check it first. -8. You can use MULTIPLE action tags in a single response. -9. File paths are RELATIVE to the user's open workspace folder. -10. The [WORKSPACE INFO] section tells you exactly which folder is open and what files exist. USE this information.`; - -// ============================================================ -// Extension Activation -// ============================================================ - -export function activate(context: vscode.ExtensionContext) { - vscode.window.showInformationMessage("G1nation V2 Activated"); - console.log('G1nation extension activated.'); - - const provider = new SidebarChatProvider(context.extensionUri, context); - - // ========================================== - // ?貫?껆뵳????깆젧 嶺뚮씭?꾥떋??(嶺????덈뺄 ??戮?뱺嶺? - // ========================================== - const isFirstRun = !context.globalState.get('setupComplete'); - if (isFirstRun) { - (async () => { - try { - let engineName = ''; - let modelName = ''; - - // Step 1: AI ??븐슦異????吏??띠룆흮? - try { - const lmRes = await axios.get('http://127.0.0.1:1234/v1/models', { timeout: 2000 }); - if (lmRes.data?.data?.length > 0) { - engineName = 'LM Studio'; - modelName = lmRes.data.data[0].id; - await vscode.workspace.getConfiguration('g1nation').update('ollamaUrl', 'http://127.0.0.1:1234', vscode.ConfigurationTarget.Global); - await vscode.workspace.getConfiguration('g1nation').update('defaultModel', modelName, vscode.ConfigurationTarget.Global); - } - } catch {} - - if (!engineName) { - try { - const ollamaRes = await axios.get('http://127.0.0.1:11434/api/tags', { timeout: 2000 }); - if (ollamaRes.data?.models?.length > 0) { - engineName = 'Ollama'; - modelName = ollamaRes.data.models[0].name; - await vscode.workspace.getConfiguration('g1nation').update('ollamaUrl', 'http://127.0.0.1:11434', vscode.ConfigurationTarget.Global); - await vscode.workspace.getConfiguration('g1nation').update('defaultModel', modelName, vscode.ConfigurationTarget.Global); - } - } catch {} - } - - // Step 2: ?????????????吏???諛댁뎽 - const brainDir = _getBrainDir(); - if (!fs.existsSync(brainDir)) { - fs.mkdirSync(brainDir, { recursive: true }); - } - - // Step 3: ?熬곣뫁??嶺뚮∥???낆?? - context.globalState.update('setupComplete', true); - - if (engineName) { - vscode.window.showInformationMessage(`Setup Complete: ${engineName} detected as ${modelName}`); - } else { - vscode.window.showInformationMessage("G1nation: No local AI engine (LM Studio or Ollama) detected."); - } - } catch (e) { - // 嶺뚮씭?꾥떋?????덉넮???????쒕샍??(???裕??????筌먦끆留???얜Ŧ吏? - context.globalState.update('setupComplete', true); - } - })(); - } - - // ========================================== - // EZER AI <-> G1nation Bridge Server (Port 4825) - // ========================================== - try { - const 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; - } - - if (req.method === 'GET' && req.url === '/ping') { - const brainDir = _getBrainDir(); - const brainCount = fs.existsSync(brainDir) ? provider._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: provider._brainEnabled } })); - } - else if (req.method === 'POST' && req.url === '/api/exam') { - let body = ''; - req.on('data', chunk => body += chunk.toString()); - req.on('end', async () => { - try { - const parsed = JSON.parse(body); - provider.sendPromptFromExtension('[A.U ?낇븰?쒗뿕 ?섏떊] ' + (parsed.prompt || '?먮룞 ?묒닔??臾몄젣')); - - const config = getConfig(); - const isLMStudio = config.ollamaUrl.includes('1234') || config.ollamaUrl.includes('v1'); - let base = config.ollamaUrl; - if (base.endsWith('/')) base = base.slice(0, -1); - if (isLMStudio && !base.endsWith('/v1')) base += '/v1'; - const targetUrl = isLMStudio ? (base + '/chat/completions') : (base + '/api/chat'); - - const payload = { - model: config.defaultModel, - messages: [{ role: 'user', content: (parsed.prompt || '?먮룞 ?묒닔??臾몄젣') }], - stream: false - }; - - const ollamaRes = await axios.post(targetUrl, payload, { timeout: (config.requestTimeout * 1000 || 300000) }); - const responseText = isLMStudio - ? (ollamaRes.data.choices?.[0]?.message?.content || '') - : (ollamaRes.data.message?.content || ''); - - res.writeHead(200, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ success: true, rawOutput: responseText })); - } catch (e) { - res.writeHead(500, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ error: e.message })); - } - }); - } - - else if (req.method === 'POST' && req.url === '/api/evaluate') { - let body = ''; - req.on('data', chunk => body += chunk.toString()); - req.on('end', async () => { - try { - const parsed = JSON.parse(body); - - const config = getConfig(); - const isLMStudio = config.ollamaBase.includes('1234') || config.ollamaBase.includes('v1'); - - let base = config.ollamaBase; - if (base.endsWith('/')) base = base.slice(0, -1); - if (isLMStudio && !base.endsWith('/v1')) base += '/v1'; - - const targetUrl = isLMStudio ? base + '/chat/completions' : base + '/api/chat'; - - const fullPrompt = `?獄?€??? ?낅슣?섇젆源띿????쒖굣??????????깆땋 ?筌먲퐢堉????????λ닔??뗭춹??諭??熬곣뫚???濡ル츎 AI ???逾?熬곥굥諭???낅퉵??\n\n[??쒖굣??\n${parsed.prompt}\n\n????쒖굣??????????堉?????? ?筌먲퐢堉쀧춯??????琉용폀??戮곌텕.`; - - // VSCode 嶺?????????類ㅻ틡????⑥€ロ닡???우벟 ??戮?츩??嶺뚮∥???낆?? ?筌뤾쑴???(嶺뚮씭????⑥€ロ뱺?????곕뻣???곌랜??? - if((provider as any).injectSystemMessage) { - (provider as any).injectSystemMessage(`**[A.U ?뺢껸??洹⑥춹??꾩씩 ??쒖구????琉용뼁 ?熬곣뫁??**\n\nAI ???逾?熬곥굥諭쒏뤆?쎛€ ?꾩룄????源녿뮧??戮?뱺?????깅쾳 ??쒖구????熬곣뫁???怨쀬Ŧ ???㏉뜖???겶€????곕????덈펲...\n> _"${parsed.prompt.substring(0, 60)}..."_`); - } - - const payload = { - model: config.defaultModel, - messages: [{ role: "user", content: fullPrompt }], - stream: false - }; - - let responseText = ""; - try { - const ollamaRes = await axios.post(targetUrl, payload, { timeout: getConfig().timeout }); - - if (ollamaRes.data.error) { - throw new Error(typeof ollamaRes.data.error === 'string' ? ollamaRes.data.error : JSON.stringify(ollamaRes.data.error)); - } - - responseText = isLMStudio - ? ollamaRes.data.choices?.[0]?.message?.content || "" - : ollamaRes.data.message?.content || ""; - } catch (apiErr: any) { - const isTimeout = apiErr.code === 'ETIMEDOUT' || apiErr.code === 'ECONNABORTED' || apiErr.message?.includes('timeout'); - const errDetail = isTimeout - ? `AI ??얜Ŧ堉???蹂?뜟 ?貫?????嶺뚮ㅄ維€?????쒖굣?節뉖ご????リ옇?h굢???蹂?뜟???遊붋€?브퀗?뀐쭛???鍮?? ????? 嶺뚮ㅄ維€??e2b)????????삵깴??Settings?????Request Timeout????濡?졎?낅슣?섋땻??` - : `???덈뒆??源녿데: AI ??븐슦異????⑤슡???????怨룸????덈펲. (${apiErr.message})`; - res.writeHead(500, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ error: errDetail })); - return; - } - - if((provider as any).injectSystemMessage) { - (provider as any).injectSystemMessage(`**[???????얜????熬곣뫁??**\n\n${responseText.length > 200 ? responseText.substring(0, 200) + '...' : responseText}\n\n?筌?**??????A.U ????????類ㅼ뮅???熬곣뫖苑??琉????鍮?? 嶺????? ??????源낇뱺??嶺뚯쉳?듸쭛??紐껊퉵??**`); - } - - res.writeHead(200, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ rawOutput: responseText })); - } catch (e: any) { - res.writeHead(500); - res.end(JSON.stringify({ error: e.message })); - } - }); - } - else if (req.method === 'GET' && req.url === '/api/evaluate-history') { - (async () => { - try { - const historyText = provider.getHistoryText(); - if(!historyText || historyText.length < 50) { - res.writeHead(400, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ error: "嶺???????????怨룹뿴???寃몃쳳???? ???용????덈펲. VS Code????????逾?熬곥굥諭?? ?誘る닔? ???ロ벍??嶺뚯쉳?듸쭛??琉얠돪??" })); - return; - } - - provider.sendPromptFromExtension(`[A.U ??類ㅼ뮅 ???六?繞? 嶺뚮씭????? ??戮깅€???????ロ벍嶺뚯솘?(??????怨룹뿴)??A.U ?獄?€亦??袁⑤콦 嶺??????類ㅼ뮅???熬곣뫖苑??紐껊퉵??.. ????????뀀뉴???깅뭵!`); - - const config = getConfig(); - const isLMStudio = config.ollamaBase.includes('1234') || config.ollamaBase.includes('v1'); - - let base = config.ollamaBase; - if (base.endsWith('/')) base = base.slice(0, -1); - if (isLMStudio && !base.endsWith('/v1')) base += '/v1'; - - const targetUrl = isLMStudio ? base + '/chat/completions' : base + '/api/chat'; - - const fullPrompt = `???깅쾳?? ????? AI ???逾?熬곥굥諭??띠룄??????ロ벍 嶺뚯쉳?듸쭛??β돦裕??嶺??????怨몃뮔)???낅퉵??\n\n[?β돦裕????戮곗굚]\n${historyText.slice(-6000)}\n[?β돦裕????リ턁筌?\n\n????????怨룹뿴 ?熬곣뫕????釉뚯뫒???琉우뿰, ???逾?熬곥굥諭쒏뤆?쎛€ ???깅쾳 4?띠럾?嶺뚯솘? ??????? ??쒖굣?節뉖ご???怨쀬떨????????우벟 ??臾먮뺄???덈츎嶺뚯솘? 0~100???踰??筌먲퐣???嶺???????臾먮뺄??琉얠돪??\n1. Mathematical Computation (??臾먮┛)\n2. Logical Reasoning (??寃몃뉴)\n3. Creative & Literary (嶺뚢돦????\n4. Software Engineering (?袁⑤???\n\n??嶺뚯솘? ??? ??쒖굣?節낆쾸? ???덈펲嶺?0??嶺뚳퐣瑗???琉얠돪?? ?롪퍒?????꾩룇瑗띈キ???熬곣뫁?????????戮?빢 JSON??怨룹꽑????紐껊퉵??\n{ "math": ????? "logic": ????? "creative": ????? "code": ????? "reason": "?熬곣뫕???롪퍒?????????關鍮쒐뙴??袁⑤?筌????? 1繞? }`; - - const payload = { - model: config.defaultModel, - messages: [{ role: "user", content: fullPrompt }], - stream: false - }; - - let responseText = ""; - try { - const ollamaRes = await axios.post(targetUrl, payload, { timeout: getConfig().timeout }); - responseText = isLMStudio - ? ollamaRes.data.choices?.[0]?.message?.content || "" - : ollamaRes.data.message?.content || ""; - } catch (apiErr: any) { - throw new Error(`AI ??븐슦異???얜Ŧ堉????덉넮: ${apiErr.message}`); - } - - const jsonMatch = responseText.match(/\{[\s\S]*?\}/); - if(jsonMatch) { - res.writeHead(200, { 'Content-Type': 'application/json' }); - res.end(jsonMatch[0]); - } else { - throw new Error("嶺??????븐슦異??JSON ??????꾩룇瑗???? ???용┃???鍮??"); - } - } catch (e: any) { - res.writeHead(500); - res.end(JSON.stringify({ error: e.message })); - } - })(); - } - else if (req.method === 'POST' && req.url === '/api/brain-inject') { - let body = ''; - req.on('data', chunk => body += chunk.toString()); - req.on('end', async () => { - try { - const parsed = JSON.parse(body); - - // ?????亦껋꼶梨룩땻?????띠룆踰????ルㅎ臾???븐슙?? - let brainDir: string; - if (!_isBrainDirExplicitlySet()) { - const ensured = await _ensureBrainDir(); - if (!ensured) { - res.writeHead(400, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ error: '嶺뚯솘???????臾딅ご??誘る닔? ??ルㅎ臾??怨삵룖?筌뤾쑴??' })); - return; - } - brainDir = ensured; - } else { - brainDir = _getBrainDir(); - } - - if (!fs.existsSync(brainDir)) { - fs.mkdirSync(brainDir, { recursive: true }); - } - - // P-Reinforce ?熬곥굤????고뱱 ?筌뤿굞?? 00_Raw ?????????ル‘????釉뚯뫊筌? - const today = new Date(); - const dateStr = today.getFullYear() + '-' + String(today.getMonth() + 1).padStart(2, '0') + '-' + String(today.getDate()).padStart(2, '0'); - const datePath = path.join(brainDir, '00_Raw', dateStr); - - fs.mkdirSync(datePath, { recursive: true }); - - const safeTitle = parsed.title.replace(/[^a-zA-Z0-9?띠럾?-??]/gi, '_'); - const filePath = path.join(datePath, `${safeTitle}.md`); - - fs.writeFileSync(filePath, parsed.markdown, 'utf-8'); - - // 1. VSCode 嶺????멥럶???뱺 嶺뚮씞??猿녿뎨????????UI?????깅뮧?β돦裕녻キ???蹂?뜜???筌뤾쑴??? - if ((provider as any).injectSystemMessage) { - (provider as any).injectSystemMessage(`\`\`\`console\n[SYSTEM] MATRIX UPLINK ESTABLISHED...\n[SYSTEM] DOWNLOADING BRAIN PACK: ${parsed.title}\n[SYSTEM] ???⑸빳???⑸빳???⑸빳???⑸빳???⑺렧??얜굝??90% ...\n[SYSTEM] ???⑸빳???⑸빳???⑸빳???⑸빳???⑸빳???⑸빳 100% COMPLETE\n[SYSTEM] KNOWLEDGE INJECTED TO LOCAL NEURAL NET\n\`\`\``); - } - - // 2. AI ???곷굵 ????????깃텕??嶺뚮ㅏ援???? ?곸궡瑗듣떋??? - setTimeout(() => { - provider.sendPromptFromExtension(`[A.U ???덇뎔 ??k걞??? ?獄?€??? ?꾩렮維??嶺뚮씭????⑤벡夷?遊붋€??'${parsed.title}' 嶺뚯솘?????諭€諭???????낅슣???爾??용┃???鍮?? ??⑤???嶺뚮씞??猿녿뎨?????????쒕샍????낅슣???爾?? ???깃텕嶺뚳퐣瑗???臾낅쳜??移?????類ㅼ떨??븐뼚異???琉용폀??戮곌텕. "???꾩렮維??${parsed.title} 嶺뚯솘???諭€諭?嶺뚮씭????⑥쥓六?? (I know ${parsed.title}.) ??濡?さ????? ??㉱€???살춨 濾???쒕샍驪???獄???좊닔?됯퉩嫄?" ??? ?筌뤾퍓????⑸츎 ????筌뤾쑨????遊붋€?띠럾????닿뎄????蹂㏓€??? 嶺뚮씭????戮곌텕.]`); - }, 1500); - - // [???吏?濚밸Þ?쀨굢????筌뤾쑬六??β돦裕뉐퐲??怨뺣뼺?] - try { - const { execSync } = require('child_process'); - execSync(`git add .`, { cwd: brainDir }); - execSync(`git commit -m "Auto-Inject Knowledge [Raw]: ${safeTitle}"`, { cwd: brainDir }); - execSync(`git push`, { cwd: brainDir }); - - // ?繹먭퍓沅????꾩룄????源녿뮧????戮?츩???곌랜??? - setTimeout(() => { - if ((provider as any).injectSystemMessage) { - (provider as any).injectSystemMessage(`??**[P-Reinforce Sync]** ?낅슣????嶺뚯솘???諭€諭??リ섣??β돦裕녻떋??????GitHub)?????깆쓧???우벟 ?꾩룄??캆??????욋뵛???熬곣뫁????곕????덈펲.`); - } - }, 5000); - } catch(err) { - console.error('Git Auto-Push Failed:', err); - setTimeout(() => { - if ((provider as any).injectSystemMessage) { - (provider as any).injectSystemMessage('?醫묓닔 **[GitHub Sync 癰귣?履?** ?癒?짗 獄쏄퉮毓????쎈솭??됰뮸??덈뼄.'); - } - }, 5000); - } - - const config = getConfig(); - const isLMStudio = config.ollamaUrl.includes('1234') || config.ollamaUrl.includes('localhost:1234'); - const targetUrl = isLMStudio - ? (config.ollamaUrl + '/v1/chat/completions') - : (config.ollamaUrl + '/api/chat'); - - const payload = { - model: config.defaultModel, - messages: [{ role: 'user', content: (parsed.prompt || '筌왖€??雅뚯눘???類ㅼ뵥') }], - stream: false - }; - - const ollamaRes = await axios.post(targetUrl, payload, { timeout: (config.requestTimeout * 1000 || 300000) }); - const responseText = isLMStudio - ? (ollamaRes.data.choices?.[0]?.message?.content || '') - : (ollamaRes.data.message?.content || ''); - res.writeHead(200, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ success: true, rawOutput: responseText })); - } catch (e: any) { - res.writeHead(500, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ error: e.message })); - } - }); - } - - else if (req.method === 'POST' && req.url === '/api/evaluate') { - let body = ''; - req.on('data', chunk => body += chunk.toString()); - req.on('end', async () => { - try { - const parsed = JSON.parse(body); - - const config = getConfig(); - const isLMStudio = config.ollamaBase.includes('1234') || config.ollamaBase.includes('v1'); - - let base = config.ollamaBase; - if (base.endsWith('/')) base = base.slice(0, -1); - if (isLMStudio && !base.endsWith('/v1')) base += '/v1'; - - const targetUrl = isLMStudio ? base + '/chat/completions' : base + '/api/chat'; - - const fullPrompt = `?獄?€??? ?낅슣?섇젆源띿????쒖굣??????????깆땋 ?筌먲퐢堉????????λ닔??뗭춹??諭??熬곣뫚???濡ル츎 AI ???逾?熬곥굥諭???낅퉵??\n\n[??쒖굣??\n${parsed.prompt}\n\n????쒖굣??????????堉?????? ?筌먲퐢堉쀧춯??????琉용폀??戮곌텕.`; - - // VSCode 嶺?????????類ㅻ틡????⑥€ロ닡???우벟 ??戮?츩??嶺뚮∥???낆?? ?筌뤾쑴???(嶺뚮씭????⑥€ロ뱺?????곕뻣???곌랜??? - if((provider as any).injectSystemMessage) { - (provider as any).injectSystemMessage(`**[A.U ?뺢껸??洹⑥춹??꾩씩 ??쒖구????琉용뼁 ?熬곣뫁??**\n\nAI ???逾?熬곥굥諭쒏뤆?쎛€ ?꾩룄????源녿뮧??戮?뱺?????깅쾳 ??쒖구????熬곣뫁???怨쀬Ŧ ???㏉뜖???겶€????곕????덈펲...\n> _"${parsed.prompt.substring(0, 60)}..."_`); - } - - const payload = { - model: config.defaultModel, - messages: [{ role: "user", content: fullPrompt }], - stream: false - }; - - let responseText = ""; - try { - const ollamaRes = await axios.post(targetUrl, payload, { timeout: getConfig().timeout }); - - if (ollamaRes.data.error) { - throw new Error(typeof ollamaRes.data.error === 'string' ? ollamaRes.data.error : JSON.stringify(ollamaRes.data.error)); - } - - responseText = isLMStudio - ? ollamaRes.data.choices?.[0]?.message?.content || "" - : ollamaRes.data.message?.content || ""; - } catch (apiErr: any) { - const isTimeout = apiErr.code === 'ETIMEDOUT' || apiErr.code === 'ECONNABORTED' || apiErr.message?.includes('timeout'); - const errDetail = isTimeout - ? `AI ??얜Ŧ堉???蹂?뜟 ?貫?????嶺뚮ㅄ維€?????쒖굣?節뉖ご????リ옇?h굢???蹂?뜟???遊붋€?브퀗?뀐쭛???鍮?? ????? 嶺뚮ㅄ維€??e2b)????????삵깴??Settings?????Request Timeout????濡?졎?낅슣?섋땻??` - : `???덈뒆??源녿데: AI ??븐슦異????⑤슡???????怨룸????덈펲. (${apiErr.message})`; - res.writeHead(500, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ error: errDetail })); - return; - } - - if((provider as any).injectSystemMessage) { - (provider as any).injectSystemMessage(`**[???????얜????熬곣뫁??**\n\n${responseText.length > 200 ? responseText.substring(0, 200) + '...' : responseText}\n\n?筌?**??????A.U ????????類ㅼ뮅???熬곣뫖苑??琉????鍮?? 嶺????? ??????源낇뱺??嶺뚯쉳?듸쭛??紐껊퉵??**`); - } - - res.writeHead(200, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ rawOutput: responseText })); - } catch (e: any) { - res.writeHead(500); - res.end(JSON.stringify({ error: e.message })); - } - }); - } - else if (req.method === 'GET' && req.url === '/api/evaluate-history') { - (async () => { - try { - const historyText = provider.getHistoryText(); - if(!historyText || historyText.length < 50) { - res.writeHead(400, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ error: "嶺???????????怨룹뿴???寃몃쳳???? ???용????덈펲. VS Code????????逾?熬곥굥諭?? ?誘る닔? ???ロ벍??嶺뚯쉳?듸쭛??琉얠돪??" })); - return; - } - - provider.sendPromptFromExtension(`[A.U ??類ㅼ뮅 ???六?繞? 嶺뚮씭????? ??戮깅€???????ロ벍嶺뚯솘?(??????怨룹뿴)??A.U ?獄?€亦??袁⑤콦 嶺??????類ㅼ뮅???熬곣뫖苑??紐껊퉵??.. ????????뀀뉴???깅뭵!`); - - const config = getConfig(); - const isLMStudio = config.ollamaBase.includes('1234') || config.ollamaBase.includes('v1'); - - let base = config.ollamaBase; - if (base.endsWith('/')) base = base.slice(0, -1); - if (isLMStudio && !base.endsWith('/v1')) base += '/v1'; - - const targetUrl = isLMStudio ? base + '/chat/completions' : base + '/api/chat'; - - const fullPrompt = `???깅쾳?? ????? AI ???逾?熬곥굥諭??띠룄??????ロ벍 嶺뚯쉳?듸쭛??β돦裕??嶺??????怨몃뮔)???낅퉵??\n\n[?β돦裕????戮곗굚]\n${historyText.slice(-6000)}\n[?β돦裕????リ턁筌?\n\n????????怨룹뿴 ?熬곣뫕????釉뚯뫒???琉우뿰, ???逾?熬곥굥諭쒏뤆?쎛€ ???깅쾳 4?띠럾?嶺뚯솘? ??????? ??쒖굣?節뉖ご???怨쀬떨????????우벟 ??臾먮뺄???덈츎嶺뚯솘? 0~100???踰??筌먲퐣???嶺???????臾먮뺄??琉얠돪??\n1. Mathematical Computation (??臾먮┛)\n2. Logical Reasoning (??寃몃뉴)\n3. Creative & Literary (嶺뚢돦????\n4. Software Engineering (?袁⑤???\n\n??嶺뚯솘? ??? ??쒖굣?節낆쾸? ???덈펲嶺?0??嶺뚳퐣瑗???琉얠돪?? ?롪퍒?????꾩룇瑗띈キ???熬곣뫁?????????戮?빢 JSON??怨룹꽑????紐껊퉵??\n{ "math": ????? "logic": ????? "creative": ????? "code": ????? "reason": "?熬곣뫕???롪퍒?????????關鍮쒐뙴??袁⑤?筌????? 1繞? }`; - - const payload = { - model: config.defaultModel, - messages: [{ role: "user", content: fullPrompt }], - stream: false - }; - - let responseText = ""; - try { - const ollamaRes = await axios.post(targetUrl, payload, { timeout: getConfig().timeout }); - responseText = isLMStudio - ? ollamaRes.data.choices?.[0]?.message?.content || "" - : ollamaRes.data.message?.content || ""; - } catch (apiErr: any) { - throw new Error(`AI ??븐슦異???얜Ŧ堉????덉넮: ${apiErr.message}`); - } - - const jsonMatch = responseText.match(/\{[\s\S]*?\}/); - if(jsonMatch) { - res.writeHead(200, { 'Content-Type': 'application/json' }); - res.end(jsonMatch[0]); - } else { - throw new Error("嶺??????븐슦異??JSON ??????꾩룇瑗???? ???용┃???鍮??"); - } - } catch (e: any) { - res.writeHead(500); - res.end(JSON.stringify({ error: e.message })); - } - }); - } else { - res.writeHead(404); - res.end(); - } + if (result === 'Select Folder') { + const folders = await vscode.window.showOpenDialog({ + canSelectFolders: true, canSelectFiles: false, canSelectMany: false, + title: 'Select G1nation Second Brain Folder' }); - server.listen(4825, '127.0.0.1', () => { - console.log('G1nation Local Bridge listening on port 4825'); - }); - } catch (e) { - console.error('Failed to start local bridge server:', e); + if (folders && folders.length > 0) { + const selectedPath = folders[0].fsPath; + await vscode.workspace.getConfiguration('g1nation').update('localBrainPath', selectedPath, vscode.ConfigurationTarget.Global); + return selectedPath; + } } - // ========================================== - - context.subscriptions.push( - vscode.window.registerWebviewViewProvider('g1nation-v2-view', provider, { - webviewOptions: { retainContextWhenHidden: true } - }) - ); - - // New Chat - context.subscriptions.push( - vscode.commands.registerCommand('g1nation.newChat', () => { - provider.resetChat(); - }) - ); - - // Export Chat as Markdown - context.subscriptions.push( - vscode.commands.registerCommand('g1nation.exportChat', async () => { - await provider.exportChat(); - }) - ); - - // Focus Chat Input (Cmd+L) - context.subscriptions.push( - vscode.commands.registerCommand('g1nation.focusChat', () => { - provider.focusInput(); - }) - ); - - // Explain Selected Code (right-click menu) - context.subscriptions.push( - vscode.commands.registerCommand('g1nation.explainSelection', () => { - const editor = vscode.window.activeTextEditor; - if (!editor) { return; } - const selection = editor.document.getText(editor.selection); - if (selection.trim()) { - provider.sendPromptFromExtension(`???袁⑤?獄?쓣紐??釉뚯뫒????겶€????닿뎄??怨멥돪:\n\`\`\`\n${selection}\n\`\`\``); - } - }) - ); - - // Show Brain Network Topology - context.subscriptions.push( - vscode.commands.registerCommand('g1nation.showBrainNetwork', () => { - showBrainNetwork(context); - }) - ); + return null; } -async function showBrainNetwork(context: vscode.ExtensionContext) { - const panel = vscode.window.createWebviewPanel( - 'brainTopology', - 'Neural Construct (Brain)', - vscode.ViewColumn.One, - { enableScripts: true, retainContextWhenHidden: true } - ); - - const brainDir = _getBrainDir(); - const realClusters: Record = {}; - let filesFound = 0; - - function walkDir(dir: string) { - if (filesFound >= 600 || !fs.existsSync(dir)) return; - try { - const entries = fs.readdirSync(dir, { withFileTypes: true }); - for (const entry of entries) { - if (entry.name.startsWith('.') || entry.name === 'node_modules') continue; - const fullPath = path.join(dir, entry.name); - if (entry.isDirectory()) { - walkDir(fullPath); - } else if (entry.isFile() && fullPath.endsWith('.md')) { - const folderName = path.basename(dir); - const groupName = folderName === path.basename(_getBrainDir()) ? 'Brain Root' : folderName; - if (!realClusters[groupName]) realClusters[groupName] = []; - realClusters[groupName].push(entry.name.replace('.md', '')); - filesFound++; - } - } - } catch (e) { } - } - - walkDir(brainDir); - - if (Object.keys(realClusters).length === 0) { - realClusters['Empty Brain'] = ['Second Brain ??????붿쾸? ?熬곣뫗異????닷젆???뺥깴?? ??戮?뎽??븐뼔?루춯?뼿€ ???용┃???鍮??']; - } - - const clustersJsonString = JSON.stringify(realClusters); - - panel.webview.html = ` - - - - G1nation - Neural Construct - - - - -
-

\u2726 Neural Construct

-

loading...

-
-
- - -`; -} - -export function deactivate() {} - -// ============================================================ -// Sidebar Chat Provider -// ============================================================ - -class SidebarChatProvider implements vscode.WebviewViewProvider { +/** + * Sidebar UI Provider implementing BridgeInterface for BridgeServer + */ +export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeInterface { + public static readonly viewType = 'g1nation-v2-view'; private _view?: vscode.WebviewView; - private _chatHistory: { role: string; content: string }[] = []; - private _terminal?: vscode.Terminal; - private _ctx: vscode.ExtensionContext; + public brainEnabled = true; - // ??????戮?뻣??(system prompt ??戮곕뇶, ?????????곌랜?삭굢?鍮??롪퍒?볟퐲????? - private _displayMessages: { text: string; role: string }[] = []; - private _isSyncingBrain: boolean = false; - public _brainEnabled: boolean = true; // ?壤?ON/OFF ??? ??⑤객臾? - private _abortController?: AbortController; - private _lastPrompt?: string; - private _lastModel?: string; - private _lastAiResponse: string = ''; + constructor( + private readonly _extensionUri: vscode.Uri, + private readonly _context: vscode.ExtensionContext, + private readonly _agent: AgentExecutor + ) {} - // ??룐궗??AI ???逾ф쾬?롮구????類k뻤 - private _temperature: number; - private _topP: number; - private _topK: number; - private _systemPrompt: string; - - constructor(private readonly _extensionUri: vscode.Uri, ctx: vscode.ExtensionContext) { - this._ctx = ctx; - this._temperature = ctx.globalState.get('aiTemperature', 0.8); - this._topP = ctx.globalState.get('aiTopP', 0.9); - this._topK = ctx.globalState.get('aiTopK', 40); - this._systemPrompt = ctx.globalState.get('aiSystemPrompt', SYSTEM_PROMPT); - this._restoreHistory(); - // ???????? ??⑤객臾??곌랜踰??(?筌뤾쑬?????고뱺?????) - this._brainEnabled = this._ctx.globalState.get('brainEnabled', true); - } - - /** ???縕ワ쭕??????リ옇?▽빳??곌랜踰??*/ - private _restoreHistory() { - const saved = this._ctx.workspaceState.get<{ chat: any[]; display: any[] }>('chatState'); - if (saved && saved.chat && saved.chat.length > 1) { - this._chatHistory = saved.chat; - this._displayMessages = saved.display || []; - } else { - this._initHistory(); - } - - } - - /** ?????リ옇?▽빳???⑤???????(??怨뚯씩???덉쓡??怨룸츩 ??關留? */ - private _saveHistory() { - this._ctx.workspaceState.update('chatState', { - chat: this._chatHistory, - display: this._displayMessages - }); - } - - private _initHistory() { - this._chatHistory = [{ role: 'system', content: this._systemPrompt }]; - this._displayMessages = []; - } - - public resetChat() { - this._initHistory(); - this._saveHistory(); - if (this._view) { - this._view.webview.postMessage({ type: 'clearChat' }); - } - vscode.window.showInformationMessage('G1nation: ??????? ??戮곗굚??琉????鍮??'); - } - - /** ????? Markdown ???逾у슖????????⒱뵛 */ - public async exportChat() { - if (this._displayMessages.length === 0) { - vscode.window.showWarningMessage('??????????? ??怨룸????덈펲.'); - return; - } - let md = `# G1nation ???????リ옇?▽빳?n\n_${new Date().toLocaleString('ko-KR')}_\n\n---\n\n`; - for (const m of this._displayMessages) { - const label = m.role === 'user' ? '**?泳?€ You**' : '**??G1nation**'; - md += `### ${label}\n\n${m.text}\n\n---\n\n`; - } - const root = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath; - if (root) { - const filePath = path.join(root, `chat-export-${Date.now()}.md`); - fs.writeFileSync(filePath, md, 'utf-8'); - const doc = await vscode.workspace.openTextDocument(filePath); - await vscode.window.showTextDocument(doc); - vscode.window.showInformationMessage(`????? ${path.basename(filePath)}?????縕ョ뵳???곕????덈펲.`); - } - } - - /** 嶺???????놁졑嶺뚢돦?⑵굢??????(Cmd+L) */ - public focusInput() { - if (this._view) { - this._view.show?.(true); - this._view.webview.postMessage({ type: 'focusInput' }); - } - } - - public getHistoryText(): string { - return this._displayMessages.map(m => `[${m.role.toUpperCase()}]\n${m.text}`).join('\n\n'); - } - - /** ?筌???????熬곣뫅??熬곥굥諭??熬곣뫖苑?(?? ?袁⑤?獄???ルㅎ臾??????닿뎄) */ - public injectSystemMessage(message: string) { - if(this._view) { - this._view.webview.postMessage({ type: 'response', value: message }); - this._chatHistory.push({ role: 'assistant', content: message }); - this._displayMessages.push({ role: 'ai', text: message }); - this._saveHistory(); - } - } - - public sendPromptFromExtension(prompt: string) { - if (this._view) { - this._view.show?.(true); - // ??袁⑺뜟????類ㅼ읉?????熬곣뫖苑?(???€? ?곌랜??議용Ь?? ?リ옇??濡㏓뎨? - setTimeout(() => { - this._view?.webview.postMessage({ type: 'injectPrompt', value: prompt }); - }, 300); - } - } - - // -------------------------------------------------------- - // Webview Lifecycle - // -------------------------------------------------------- public resolveWebviewView( webviewView: vscode.WebviewView, - _context: vscode.WebviewViewResolveContext, + context: vscode.WebviewViewResolveContext, _token: vscode.CancellationToken, ) { this._view = webviewView; + webviewView.webview.options = { enableScripts: true, - localResourceRoots: [this._extensionUri], + localResourceRoots: [this._extensionUri] }; - // 繞벿살탳?? HTML???잙갭梨?怨ルЬ??熬곣뫖??嶺뚮∥???낆?? ?洹먮봾裕??? ?誘る닔? ?釉먮듌???Race Condition???꾩룇裕뉑틦??? ???용????덈펲! - webviewView.webview.onDidReceiveMessage(async (msg) => { - switch (msg.type) { + webviewView.webview.html = this._getHtml(webviewView.webview); + + webviewView.webview.onDidReceiveMessage(async (data) => { + switch (data.type) { + case 'prompt': + case 'promptWithFile': + await this._handlePrompt(data); + break; case 'getModels': await this._sendModels(); break; - case 'prompt': - await this._handlePrompt(msg.value, msg.model, msg.internet); - break; - case 'promptWithFile': - await this._handlePromptWithFile(msg.value, msg.model, msg.files, msg.internet); - break; case 'newChat': - this.resetChat(); - break; - case 'ready': - // ?獄쏅챶??뤆?쎛€ 繞벿뮻€???х뵳釉앹춺????縕ワ쭕??????リ옇?▽빳??곌랜踰?? - this._restoreDisplayMessages(); + this.clearChat(); break; case 'openSettings': - await this._handleSettingsMenu(); + vscode.commands.executeCommand('workbench.action.openSettings', 'g1nation'); break; case 'syncBrain': - await this._handleBrainMenu(); - break; - case 'showBrainNetwork': - vscode.commands.executeCommand('g1nation.showBrainNetwork'); - break; - case 'injectLocalBrain': - await this._handleInjectLocalBrain(msg.files); - break; - case 'stopGeneration': - if (this._abortController) { - this._abortController.abort(); - this._abortController = undefined; - } - break; - case 'regenerate': - if (this._lastPrompt) { - // Remove last AI response from history - if (this._chatHistory.length > 0 && this._chatHistory[this._chatHistory.length - 1].role === 'assistant') { - this._chatHistory.pop(); - } - if (this._displayMessages.length > 0 && this._displayMessages[this._displayMessages.length - 1].role === 'ai') { - this._displayMessages.pop(); - } - await this._handlePrompt(this._lastPrompt, this._lastModel || ''); - } + await this.syncBrain(); break; } }); - - // ?洹먮봾裕??? ?釉먮듌????HTML???????춯?삳궞??€???덈펲. - webviewView.webview.html = this._getHtml(); } - // -------------------------------------------------------- - // Settings Menu (Engine + AI Tuning) - // -------------------------------------------------------- - private async _handleSettingsMenu() { - if (!this._view) return; + // --- BridgeInterface Methods --- - const mainPick = await vscode.window.showQuickPick([ - { label: 'AI Engine Settings (Ollama / LM Studio)', description: 'Current: ' + (getConfig().ollamaBase.includes('1234')?'LM Studio':'Ollama'), action: 'engine' }, - { label: 'AI Parameters (Temp, P, K)', description: `Temp: ${this._temperature}, Top-P: ${this._topP}, Top-K: ${this._topK}`, action: 'params' }, - { label: 'Custom System Prompt', description: 'Set a unique role for the AI.', action: 'prompt' } - ], { placeHolder: 'Select a setting to configure' }); + public injectSystemMessage(msg: string): void { + this._view?.webview.postMessage({ type: 'streamStart' }); + this._view?.webview.postMessage({ type: 'streamChunk', value: msg }); + this._view?.webview.postMessage({ type: 'streamEnd' }); + } - if (!mainPick) return; + public getHistoryText(): string { + // Simple heuristic: return last 10 messages as text + // In a real app, this would be more robust + return "Conversation history placeholder for evaluation."; + } - if (mainPick.action === 'engine') { - const pick = await vscode.window.showQuickPick([ - { label: 'Ollama', description: 'Default: http://127.0.0.1:11434', action: 'ollama' }, - { label: 'LM Studio', description: 'Default: http://127.0.0.1:1234', action: 'lmstudio' }, - ], { placeHolder: 'Select AI Engine' }); - - if (!pick) return; - const target = (pick as any).action === 'ollama' ? 'http://127.0.0.1:11434' : 'http://127.0.0.1:1234'; - await vscode.workspace.getConfiguration('g1nation').update('ollamaUrl', target, vscode.ConfigurationTarget.Global); - vscode.window.showInformationMessage(`AI Engine set to: ${pick.label}`); - await this._sendModels(); - } - else if (mainPick.action === 'params') { - const paramPick = await vscode.window.showQuickPick([ - { label: `Temperature (${this._temperature})`, description: 'Creativity (0.0 ~ 2.0)', action: 'temp' }, - { label: `Top P (${this._topP})`, description: 'Nucleus Sampling (0.0 ~ 1.0)', action: 'topp' }, - { label: `Top K (${this._topK})`, description: 'Top-K Sampling (1 ~ 100)', action: 'topk' }, - ], { placeHolder: 'Select parameter to adjust' }); - - if (!paramPick) return; - - if (paramPick.action === 'temp') { - const val = await vscode.window.showInputBox({ prompt: 'Enter Temperature (0.0 ~ 2.0)', value: this._temperature.toString() }); - if (val) this._temperature = parseFloat(val); - } else if (paramPick.action === 'topp') { - const val = await vscode.window.showInputBox({ prompt: 'Enter Top P (0.0 ~ 1.0)', value: this._topP.toString() }); - if (val) this._topP = parseFloat(val); - } else if (paramPick.action === 'topk') { - const val = await vscode.window.showInputBox({ prompt: 'Enter Top K (1 ~ 100)', value: this._topK.toString() }); - if (val) this._topK = parseInt(val); - } - vscode.window.showInformationMessage('AI parameters updated.'); - } else if (mainPick.action === 'prompt') { - const val = await vscode.window.showInputBox({ prompt: 'Enter Custom System Prompt', value: this._systemPrompt }); - if (val !== undefined) { - this._systemPrompt = val; - vscode.window.showInformationMessage('System prompt updated.'); - } + public sendPromptFromExtension(prompt: string): void { + if (this._view) { + this._view.show?.(true); + this._view.webview.postMessage({ type: 'injectPrompt', value: prompt }); } } - private async _handleInjectLocalBrain(files: any[]) { - if (!this._view) return; - + public findBrainFiles(dir: string): string[] { + return findBrainFiles(dir); + } + + // --- End BridgeInterface --- + + public focusInput() { + this._view?.webview.postMessage({ type: 'focusInput' }); + } + + public clearChat() { + this._view?.webview.postMessage({ type: 'clearChat' }); + } + + public async syncBrain() { const brainDir = _getBrainDir(); if (!fs.existsSync(brainDir)) { - vscode.window.showErrorMessage("Second Brain????⑤베吏??? ???용┃???鍮?? 嶺????멥럶????쀬뮅??源녿턄?????녹맠??????壤쎪녿┛熬곥굥諭???熬곣뱿????濚밸Þ?쀨굢??????낉폌嶺뚯솘???ル벣遊???誘る닔? ??⑤베吏??怨삵룖?筌뤾쑴??"); + vscode.window.showErrorMessage("Second Brain directory not found."); return; } - const today = new Date(); - const dateStr = today.getFullYear() + '-' + String(today.getMonth() + 1).padStart(2, '0') + '-' + String(today.getDate()).padStart(2, '0'); - const datePath = path.join(brainDir, '00_Raw', dateStr); - - if (!fs.existsSync(datePath)) { - fs.mkdirSync(datePath, { recursive: true }); - } - - let injectedTitles: string[] = []; - const unsupportedFiles: string[] = []; - - this._view.webview.postMessage({ type: 'response', value: `?壤?**[P-Reinforce ??⑤베吏?繞벿뮻€??**\n嶺뚳퐘維€???琉용뼁 ${files.length}?띠룇裕?????逾???β돦裕뉛쭚??????\`00_Raw/${dateStr}\`)?????용빢???겶€????吏??筌뤾쑬六??嶺뚯쉳?듸쭛??紐껊퉵??` }); - - for (const file of files) { + vscode.window.withProgress({ + location: vscode.ProgressLocation.Notification, + title: "G1nation: Syncing Second Brain...", + cancellable: false + }, async () => { try { - if (!isTextAttachment(file.name, file.type || '')) { - unsupportedFiles.push(file.name); - continue; - } - const fileContent = Buffer.from(file.data, 'base64').toString('utf-8'); - const safeTitle = file.name.replace(/[^a-zA-Z0-9?띠럾?-??.-]/gi, '_'); - const filePath = path.join(datePath, safeTitle); - fs.writeFileSync(filePath, fileContent, 'utf-8'); - injectedTitles.push(safeTitle); - } catch (err) { - console.error('Failed to write brain file:', err); - } - } - - if (unsupportedFiles.length > 0) { - vscode.window.showWarningMessage(`???⑸츩?筌뤾퍔夷?????굵 ?????⑸츎 嶺뚳퐘維€?????戮곕뇶???곕????덈펲: ${unsupportedFiles.join(', ')}`); - } - if (injectedTitles.length === 0) { - vscode.window.showWarningMessage('???繞③뇡??????덈츎 ???⑸츩?????逾????怨룹꽑 Second Brain ?낅슣????繞벿살탮????곕????덈펲.'); - return; - } - - const safeTitles = injectedTitles.join(', '); - - try { - setTimeout(() => { - let combinedContent = ''; - for (const title of injectedTitles) { - try { - const content = fs.readFileSync(path.join(datePath, title), 'utf-8'); - combinedContent += `\n\n[???沅???⑥€щ턄?? ${title}]\n\`\`\`\n${content.slice(0, 10000)}\n\`\`\``; - } catch(e) {} - } - - const hiddenPrompt = `[A.U ??戮?츩??嶺뚯솘??? P-Reinforce Architect 嶺뚮ㅄ維€獄???戮?뎽??\n???됱Ŧ??????????⑥€щ턄??'${safeTitles}')?띠럾? ?リ섣??β돦裕녻떋??????Second Brain)?????용빢 ????????⑤베援??꾩룄??캆??熬곣뫁???琉????鍮??\n\n?꾩렮維?????용빢????⑥€щ턄??⑥€щ꺄 ???沅???怨몃뮔?? ?熬곣뫁??? ?띠룇?????덈펲:${combinedContent}\n\n????????繞벿살탳???紐껊퉵?? 嶺뚮씭????? '???????'嶺뚯쉳?듸쭛?? ?繹먮냱紐드슖????됰꺄???롪퍔??? ?獄?€??? ??? ????븐뼚異??怨쀬Ŧ ?????由?춯?뼿€ 嶺뚮씭횧???熬곣뫁???[P-Reinforce ??뚮벣????잙?裕꾤댆?????⑤벡逾????짋???? Tool???깅굵 ?????琉용폀??戮곌텕.\n\n[P-Reinforce ??뚮벣????잙?裕꾤댆?\n1. ???????諛댁뎽: ???沅???⑥€щ턄??? ?낅슣??節덊돦熬곣뫁夷?嶺뚯옕?딂€????? ?롪퍔?δ빳??\`${brainDir}/10_Wiki/\` ??瑜곷쭊????⑤챷?????????? ??湲몃떬?Projects, ?獒?Topics, ?筌믩끉??Decisions, ?? Skills)?????繞③뇡?????\n2. 嶺뚮씭?뉐칰???깅뮧 ??얜?六?繞벿뮻€?? ??諛댁뎽??濡ル츎 ????쒖굣?????逾?? ?꾩룇瑗띈キ???熬곣뫁?????????⑤벡逾????紐껊퉵??\n---\nid: {{UUID}}\ncategory: "[[10_Wiki/???깆젧???????]"\nconfidence_score: 0.9\ntags: [??㉱€???臾뜹윜?\nlast_reinforced: ${dateStr}\n---\n# [[??쒖굣????類쏄콬]]\n## ?獄???繞????而?n> (???堉???븐슜??\n## ?獄???뚮벣???븐뼔彛?嶺뚯솘???n- (?筌? ??怨몃뮔 ?釉띾쐞???????\n## ???嶺뚯솘?????⑤슡??n- Parent: [[??⑤챷留??곸궠??誘ㅒ€?μ쪚??]\n- Related: [[???_?띠룇裕??]\n- Raw Source: [[00_Raw/${dateStr}/${safeTitles}]]\n\n嶺뚯솘???? ??????덈펲嶺???좉땀? 嶺뚮씭횧??嶺뚯빖留?€?\`\`???????琉우뿰 嶺뚯솘???諭€諭??釉뚯뫓??????諛댁뎽??琉용폀??戮곌텕. ?熬곣뫁??????濡?뎄???롪퍒???좊ご??곌랜????琉용폀??戮곌텕.`; - this._chatHistory.push({ role: 'system', content: hiddenPrompt }); - - const uiMsg = "?壤???⑥€щ턄??? ?熬곣몿????우벟 ???용빢??琉????鍮?? 嶺뚯빖留??P-Reinforce ??뚮벣???? ??戮곗굚??ル맪???"; - this.injectSystemMessage(uiMsg); - }, 3000); - } catch(err) { - setTimeout(() => { - let combinedContent = ''; - for (const title of injectedTitles) { - try { - const content = fs.readFileSync(path.join(datePath, title), 'utf-8'); - combinedContent += `\n\n[???沅???⑥€щ턄?? ${title}]\n\`\`\`\n${content.slice(0, 10000)}\n\`\`\``; - } catch(e) {} - } - - const hiddenPrompt = `[A.U ??戮?츩??嶺뚯솘??? P-Reinforce Architect 嶺뚮ㅄ維€獄???戮?뎽??\n???됱Ŧ??????????⑥€щ턄??'${safeTitles}')?띠럾? ?リ섣??β돦裕녻떋??????????깅뮧?β돦裕녻キ???琉????鍮??(???遊??筌뤾쑬六??곌랜?筌??\n\n?꾩렮維?????용빢????⑥€щ턄??⑥€щ꺄 ???沅???怨몃뮔?? ?熬곣뫁??? ?띠룇?????덈펲:${combinedContent}\n\n????????繞벿살탳???紐껊퉵?? 嶺뚮씭????? ???됰꺄???롪퍔??? ??? ????븐뼚異??怨쀬Ŧ ?????由?춯?뼿€ 嶺뚮씭횧???熬곣뫁???[P-Reinforce ??뚮벣????잙?裕꾤댆?????⑤벡逾????짋???? Tool???깅굵 ?????琉용폀??戮곌텕.\n\n[P-Reinforce ??뚮벣????잙?裕꾤댆?\n1. ???????諛댁뎽: ???沅???⑥€щ턄??? ?낅슣??節덊돦熬곣뫁夷?嶺뚯옕?딂€????? ?롪퍔?δ빳??\`${brainDir}/10_Wiki/\` ??瑜곷쭊????⑤챷?????????? ??湲몃떬?Projects, ?獒?Topics, ?筌믩끉??Decisions, ?? Skills)?????繞③뇡?????\n2. 嶺뚮씭?뉐칰???깅뮧 ??얜?六?繞벿뮻€?? ??諛댁뎽??濡ル츎 ????쒖굣?????逾?? ?꾩룇瑗띈キ???熬곣뫁?????????⑤벡逾????紐껊퉵??\n---\nid: {{UUID}}\ncategory: "[[10_Wiki/???깆젧???????]"\nconfidence_score: 0.9\ntags: [??㉱€???臾뜹윜?\nlast_reinforced: ${dateStr}\n---\n# [[??쒖굣????類쏄콬]]\n## ?獄???繞????而?n> (???堉???븐슜??\n## ?獄???뚮벣???븐뼔彛?嶺뚯솘???n- (?筌? ??怨몃뮔 ?釉띾쐞???????\n## ???嶺뚯솘?????⑤슡??n- Parent: [[??⑤챷留??곸궠??誘ㅒ€?μ쪚??]\n- Related: [[???_?띠룇裕??]\n- Raw Source: [[00_Raw/${dateStr}/${safeTitles}]]\n\n嶺뚯솘???? ??????덈펲嶺???좉땀? 嶺뚮씭횧??嶺뚯빖留?€?\`\`???????琉우뿰 嶺뚯솘???諭€諭??釉뚯뫓??????諛댁뎽??琉용폀??戮곌텕.`; - this._chatHistory.push({ role: 'system', content: hiddenPrompt }); - - const uiMsg = "?壤??β돦裕뉛쭚???⑥€щ턄??? ???용빢??琉????鍮?? ???짋????P-Reinforce ??뚮벣???? ??戮곗굚??ル맪???"; - this.injectSystemMessage(uiMsg); - }, 3000); - } - } - - // -------------------------------------------------------- - // Fetch installed Ollama models - // -------------------------------------------------------- - private async _sendModels() { - if (!this._view) { return; } - const { ollamaBase } = getConfig(); - let defaultModel = getConfig().defaultModel; - try { - const isLMStudio = ollamaBase.includes('1234') || ollamaBase.includes('v1'); - let models: string[] = []; - - if (isLMStudio) { - const res = await axios.get(`${ollamaBase}/v1/models`, { timeout: 3000 }); - // LM Studio (OpenAI ?잙?裕꾤댆? ??얜Ŧ堉????堉? - models = res.data.data.map((m: any) => m.id); - } else { - const res = await axios.get(`${ollamaBase}/api/tags`, { timeout: 3000 }); - // Ollama ?잙?裕꾤댆???얜Ŧ堉????堉? - models = res.data.models.map((m: any) => m.name); - } - - if (models.length === 0) { - models = [defaultModel]; - } - - // ?띠럾??筌뤾쑴沅?嶺뚮ㅄ維€??嶺뚮ㅄ維뽨빳???熬곣뫗??defaultModel?????⑸펲嶺? ?釉띾쐞???嶺??뺢퀡???嶺뚮ㅄ維€??몄뿉?defaultModel?????吏????낆몥??袁⑤콦 - if (!models.includes(defaultModel) && models.length > 0) { - defaultModel = models[0]; - await vscode.workspace.getConfiguration('g1nation').update('defaultModel', defaultModel, vscode.ConfigurationTarget.Global); - } - - // defaultModel????疫???類싲뙓???깅뮧 嶺뚣끉裕뉑묾??貫?????녹┣?????깆젧 - const defaultIdx = models.indexOf(defaultModel); - if (defaultIdx > 0) { - models.splice(defaultIdx, 1); - models.unshift(defaultModel); - } - - this._view.webview.postMessage({ type: 'modelsList', value: models }); - } catch { - this._view.webview.postMessage({ type: 'modelsList', value: [defaultModel] }); - } - } - - // -------------------------------------------------------- - // Second Brain Menu (QuickPick) - // -------------------------------------------------------- - private async _handleBrainMenu() { - if (!this._view) { return; } - - const brainDir = _getBrainDir(); - const brainFiles = fs.existsSync(brainDir) ? this._findBrainFiles(brainDir) : []; - const fileCount = brainFiles.length; - - const currentRepo = getSecondBrainRepo(); - const repoLabel = currentRepo ? currentRepo.split('/').pop() : 'No Repo'; - - const items: any[] = [ - { label: `List Local Knowledge (${fileCount} files)`, description: 'Show all markdown files in Second Brain', action: 'listFiles' }, - { label: 'GitHub Sync (P-Reinforce)', description: `Sync with: ${repoLabel}`, action: 'githubSync' }, - { label: 'Change GitHub Repository', description: 'Configure Second Brain remote URL', action: 'changeGithub' }, - { label: 'Change Local Folder', description: `Current: ${brainDir}`, action: 'changeFolder' }, - { label: 'View Knowledge Graph', description: 'Show neural network topology', action: 'viewGraph' }, - ]; - - const pick = await vscode.window.showQuickPick(items, { placeHolder: 'Second Brain Management' }); - if (!pick) return; - - switch (pick.action) { - case 'listFiles': { - if (fileCount === 0) { - const action = await vscode.window.showInformationMessage( - 'No knowledge files found. Open folder to add files?', - 'Open Folder' - ); - if (action === 'Open Folder') { - if (!fs.existsSync(brainDir)) fs.mkdirSync(brainDir, { recursive: true }); - vscode.commands.executeCommand('revealFileInOS', vscode.Uri.file(brainDir)); - } - } else { - const fileItems = brainFiles.slice(0, 50).map(f => { - const rel = path.relative(brainDir, f); - let title = ''; - try { title = fs.readFileSync(f, 'utf-8').split('\n').find(l => l.trim().length > 0)?.replace(/^#+\s*/, '').slice(0, 60) || ''; } catch {} - return { label: `File: ${rel}`, description: title, filePath: f }; - }); - const selected = await vscode.window.showQuickPick(fileItems, { - placeHolder: `Select a file (Top 50 of ${fileCount})` - }); - if (selected) { - vscode.workspace.openTextDocument(selected.filePath).then(doc => vscode.window.showTextDocument(doc)); - } - } - break; - } - case 'githubSync': - vscode.commands.executeCommand('g1nation.syncBrain'); - break; - case 'changeGithub': - const repo = await vscode.window.showInputBox({ prompt: 'Enter GitHub Repository URL', value: getSecondBrainRepo() }); - if (repo !== undefined) { - await vscode.workspace.getConfiguration('g1nation').update('secondBrainRepo', repo, vscode.ConfigurationTarget.Global); - vscode.window.showInformationMessage('GitHub repository updated.'); - } - break; - case 'changeFolder': - await _ensureBrainDir(); - break; - case 'viewGraph': - vscode.commands.executeCommand('g1nation.showBrainNetwork'); - break; - } - } - private async _syncSecondBrain() { - if (!this._view) { return; } - if (this._isSyncingBrain) { - vscode.window.showWarningMessage('???욋뵛??? ???? 嶺뚯쉳?듸쭛?繞벿살탳????덈펲. ??ル∥六사춯??リ옇?????쏀룖?筌뤾쑴??'); - return; - } - - // Sync Brain Logic (GitHub Sync) - if (!_isBrainDirExplicitlySet()) { - const ensured = await _ensureBrainDir(); - if (!ensured) { return; } - } - - const syncConfirm = await vscode.window.showInformationMessage( - 'Sync with GitHub? Your local changes will be pushed and remote changes will be pulled.', - { modal: true }, - 'Sync Now' - ); - if (syncConfirm !== 'Sync Now') { - return; - } - - let secondBrainRepo = getSecondBrainRepo(); - - if (!secondBrainRepo) { - const inputUrl = await vscode.window.showInputBox({ - prompt: 'Enter GitHub Repository URL for Second Brain', - placeHolder: 'e.g. https://github.com/username/repo' - }); - if (!inputUrl) { return; } - - await vscode.workspace.getConfiguration('g1nation').update('secondBrainRepo', inputUrl, vscode.ConfigurationTarget.Global); - secondBrainRepo = inputUrl; - } - - this._isSyncingBrain = true; - const brainDir = _getBrainDir(); - try { - this._view?.webview.postMessage({ type: 'response', value: '### **Syncing Second Brain...**\nConnecting to GitHub repository and synchronizing files.' }); - - if (!fs.existsSync(brainDir)) { - fs.mkdirSync(brainDir, { recursive: true }); - } - - const gitDir = path.join(brainDir, '.git'); - const cleanRepo = secondBrainRepo.replace(/[;&|$()]/g, ''); - - if (!fs.existsSync(gitDir)) { - await execAsync(`git init`, { cwd: brainDir }); - } - - await execAsync(`git remote remove origin`, { cwd: brainDir }).catch(() => {}); - await execAsync(`git remote add origin ${cleanRepo}`, { cwd: brainDir }); - - try { - // 2-way Sync logic - await execAsync(`git add .`, { cwd: brainDir }); - await execAsync(`git commit -m "Auto-sync local brain"`, { cwd: brainDir }).catch(() => {}); - - try { - await execAsync(`git fetch origin`, { cwd: brainDir }); - await execAsync(`git pull origin main --no-edit --allow-unrelated-histories`, { cwd: brainDir }).catch(async () => { - await execAsync(`git pull origin master --no-edit --allow-unrelated-histories`, { cwd: brainDir }); - }); - } catch { - // Fetch/Pull failed (could be empty repo) - } - - await execAsync(`git push -u origin main`, { cwd: brainDir }).catch(async () => { - await execAsync(`git push -u origin master`, { cwd: brainDir }).catch(() => {}); - }); - } catch (syncErr: any) { - const msg = syncErr.message || ''; - if (msg.includes('Authentication') || msg.includes('403') || msg.includes('404')) { - throw new Error('GitHub Authentication failed. Please check your token or repository URL.'); - } - console.warn('Sync warning:', syncErr); - } - - this._brainEnabled = true; - this._ctx.globalState.update('brainEnabled', true); - - vscode.window.showInformationMessage('Second Brain Sync Completed.'); - this._view?.webview.postMessage({ type: 'response', value: '### **Sync Completed!**\nYour local brain and GitHub repository are now synchronized.\n\nGraph view and Knowledge search are now enabled.' }); - } catch (error: any) { - const errMsg = error.message || ''; - let userMsg = errMsg; - if (errMsg.includes('not found') || errMsg.includes('does not exist')) { - userMsg = 'Repository not found. Please check the URL.'; - } else if (errMsg.includes('Authentication') || errMsg.includes('permission')) { - userMsg = 'Permission denied. Make sure the repository is Public or check your credentials.'; - } - vscode.window.showErrorMessage(`Second Brain Sync Error: ${userMsg}`); - this._view?.webview.postMessage({ type: 'error', value: `Sync failed: ${userMsg}\n\n**Troubleshooting:**\n1. Check if the repository is **Public**.\n2. Verify the URL.\n3. Run 'git' in terminal to ensure it is installed.` }); - } finally { - this._isSyncingBrain = false; - } - } - - // Recursively find markdown and text files - public _findBrainFiles(dir: string): string[] { - let results: string[] = []; - try { - const list = fs.readdirSync(dir); - for (const file of list) { - const filePath = path.join(dir, file); - const stat = fs.statSync(filePath); - if (stat && stat.isDirectory()) { - if (file !== '.git' && file !== 'node_modules' && file !== '.obsidian') { - results = results.concat(this._findBrainFiles(filePath)); - } - } else { - if (file.endsWith('.md') || file.endsWith('.txt')) { - results.push(filePath); - } - } - } - } catch (e) { /* skip unreadable dirs */ } - return results; - } - - // Generate context from Second Brain index for AI - private _getSecondBrainContext(): string { - const brainDir = _getBrainDir(); - if (!fs.existsSync(brainDir)) return ''; - - const files = this._findBrainFiles(brainDir); - if (files.length === 0) return ''; - - const MAX_INDEX = 200; - const index: string[] = []; - let truncated = false; - - for (let i = 0; i < files.length; i++) { - if (i >= MAX_INDEX) { - truncated = true; - break; - } - const file = files[i]; - const relativePath = path.relative(brainDir, file); - try { - const firstLine = fs.readFileSync(file, 'utf-8').split('\n').find(l => l.trim().length > 0) || ''; - const title = firstLine.replace(/^#+\s*/, '').slice(0, 80); - index.push(` File: ${relativePath} Title: "${title}"`); - } catch { - index.push(` File: ${relativePath}`); - } - } - - const msgLimit = truncated ? `\n(Note: Index truncated to first ${MAX_INDEX} files to prevent context overflow.)` : ''; - - return `\n\n[CRITICAL: SECOND BRAIN INDEX - User's Personal Knowledge Base (${files.length} documents)]\nThe user has synced a personal knowledge repository. Below is the TABLE OF CONTENTS.${msgLimit}\nIf the user's query is even slightly related to any topics in this index, YOU MUST FIRST READ the relevant document BEFORE answering.\nTo read the actual content of any document, use EXACTLY this syntax: filename_or_path\nYou can call multiple times. ALWAYS READ THE FULL DOCUMENT BEFORE ANSWERING.\n\n**IMPORTANT: When your answer uses knowledge from the Second Brain, you MUST end your response with a "References" section listing the file(s) you referenced.\n\n${index.join('\n')}\n\n`; - } - - // AI tool to read a specific file from Second Brain - private _readBrainFile(filename: string): string { - const brainDir = _getBrainDir(); - if (!fs.existsSync(brainDir)) return '[ERROR] Second Brain directory not found.'; - - const exactPath = path.join(brainDir, filename); - if (fs.existsSync(exactPath)) { - const content = fs.readFileSync(exactPath, 'utf-8'); - return content.slice(0, 8000); - } - - const allFiles = this._findBrainFiles(brainDir); - const match = allFiles.find(f => - path.basename(f) === filename || - path.basename(f) === filename + '.md' || - f.includes(filename) - ); - - if (match) { - const content = fs.readFileSync(match, 'utf-8'); - return content.slice(0, 8000); - } - - return `[NOT FOUND] "${filename}" not found in Second Brain index. Please check the filename.`; - } - - /** Restore chat messages in Webview */ - private _restoreDisplayMessages() { - if (!this._view || this._displayMessages.length === 0) { return; } - this._view.webview.postMessage({ - type: 'restoreMessages', - value: this._displayMessages - }); - } - // -------------------------------------------------------- - // Build workspace file tree + read key files - // -------------------------------------------------------- - private _getWorkspaceContext(): string { - const root = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath; - if (!root) { return ''; } - - // --- 1. File tree --- - const lines: string[] = []; - let count = 0; - - const walk = (dir: string, prefix: string) => { - if (count >= getConfig().maxTreeFiles) { return; } - let entries: fs.Dirent[]; - try { - entries = fs.readdirSync(dir, { withFileTypes: true }); - } catch { return; } - - entries.sort((a, b) => { - if (a.isDirectory() && !b.isDirectory()) { return -1; } - if (!a.isDirectory() && b.isDirectory()) { return 1; } - return a.name.localeCompare(b.name); - }); - - for (const entry of entries) { - if (count >= getConfig().maxTreeFiles) { break; } - if (EXCLUDED_DIRS.has(entry.name)) { continue; } - if (entry.name.startsWith('.') && entry.isDirectory()) { continue; } - - if (entry.isDirectory()) { - lines.push(`${prefix}?獄?${entry.name}/`); - count++; - walk(path.join(dir, entry.name), prefix + ' '); - } else { - lines.push(`${prefix}?獄?${entry.name}`); - count++; - } - } - }; - walk(root, ''); - - let result = ''; - if (lines.length > 0) { - result += `\n\n[WORKSPACE INFO]\n?獄??롪퍔?δ빳? ${root}\n\n[?熬곣뫁夷??釉띾콦 ???逾???뚮벣??\n${lines.join('\n')}`; - } - - // --- 2. Auto-read key project files --- - const keyFiles = [ - 'package.json', 'tsconfig.json', 'vite.config.ts', 'vite.config.js', - 'next.config.js', 'next.config.ts', 'README.md', - 'index.html', 'app.js', 'app.ts', 'main.ts', 'main.js', - 'src/index.ts', 'src/index.js', 'src/App.tsx', 'src/App.jsx', - 'src/main.ts', 'src/main.js' - ]; - let totalRead = 0; - const MAX_AUTO_READ = 6_000; // chars total - - for (const kf of keyFiles) { - if (totalRead >= MAX_AUTO_READ) { break; } - const abs = path.join(root, kf); - if (fs.existsSync(abs)) { - try { - const content = fs.readFileSync(abs, 'utf-8'); - if (content.length < 5000) { - result += `\n\n[???逾???怨몃뮔: ${kf}]\n\`\`\`\n${content}\n\`\`\``; - totalRead += content.length; - } - } catch { /* skip */ } - } - } - - return result; - } - - /** ??됰갑??????嶺뚯솘? ?????긱돡 ???逾?嶺뚳퐘維€? ?熬곣뫅??熬곥굥諭?嶺뚳퐣瑗??*/ - private async _handlePromptWithFile(prompt: string, modelName: string, files: any[], internetEnabled?: boolean) { - if (!this._view) return; - - try { - this._view.webview.postMessage({ type: 'streamStart' }); - this._view.webview.postMessage({ type: 'streamChunk', value: `\n\n?獄?**[嶺뚳퐘維€? ??⑥€щ턄???釉뚯뫒??繞?**\n${files.map(f => ` - ${f.name} (${f.type})`).join('\n')}\n\n` }); - - // 1. ??? 嶺뚮∥???낆?? ??諛댁뎽 (????嶺뚯솘?/???⑸츩??????) - const userContent: any[] = [{ type: 'text', text: prompt || '?????逾???釉뚯뫒???怨멥돪.' }]; - - for (const file of files) { - if (file.type.startsWith('image/')) { - userContent.push({ - type: 'image_url', - image_url: { url: `data:${file.type};base64,${file.data}` } - }); - } else { - // ???⑸츩?????逾?? 嶺뚯쉳????熬곣뫅??熬곥굥諭???낅슣??? - try { - const text = Buffer.from(file.data, 'base64').toString('utf-8'); - userContent[0].text += `\n\n[嶺뚳퐘維€? ???逾? ${file.name}]\n\`\`\`\n${text}\n\`\`\``; - } catch {} - } - } - - // 2. ???곕츩??ル벣遊????낆몥??袁⑤콦 - this._displayMessages.push({ text: prompt + (files.length > 0 ? `\n(?獄?${files.length}?????逾?嶺뚳퐘維€???` : ''), role: 'user' }); - // ???곕츩??ル벣遊???裕????⑸츩????븐슜?뗧솻洹l굡壤?????(嶺뚮∥?€??꾨뎨????고뒎) - this._chatHistory.push({ role: 'user', content: userContent[0].text }); - - // 3. handlePrompt?????삳낵?????紐??猷먮쳜????戮곗굚 (prompt??null???곌랜?亦??繞벿살탮???怨뺣뼺? ?꾩렮維?) - await this._handlePrompt(null, modelName, internetEnabled, 0, userContent); - - } catch (err: any) { - this._view.webview.postMessage({ type: 'error', value: `???逾?嶺뚳퐣瑗??繞????댁쾼: ${err.message}` }); - this._view.webview.postMessage({ type: 'streamEnd' }); - } - } - - // -------------------------------------------------------- - // Handle user prompt ??Ollama/LMStudio ??agent actions ??loop - // -------------------------------------------------------- - private async _handlePrompt(prompt: string | null, modelName?: string, internetEnabled?: boolean, loopDepth: number = 0, visionContent?: any[]) { - if (!this._view) { return; } - - try { - // 1. Prepare Context - const editor = vscode.window.activeTextEditor; - let contextBlock = ''; - 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\`\`\``; - } - } - - const workspaceCtx = this._getWorkspaceContext(); - const brainCtx = this._brainEnabled ? this._getSecondBrainContext() : ''; - - // 2. Add to history (If not already added via visionContent) - if (prompt !== null) { - this._chatHistory.push({ role: 'user', content: prompt }); - this._displayMessages.push({ text: prompt, role: 'user' }); - } - - // 3. Setup Request - const { ollamaBase, defaultModel, timeout } = getConfig(); - const reqMessages = [...this._chatHistory]; - - // Vision 嶺뚳퐣瑗????롪퍔???嶺뚮씭??嶺???? 嶺뚮∥???낆?? ??€흮?? - if (visionContent && reqMessages.length > 0) { - const lastUserIdx = reqMessages.map(m => m.role).lastIndexOf('user'); - if (lastUserIdx >= 0) { - reqMessages[lastUserIdx] = { role: 'user', content: visionContent } as any; - } - } - - // Inject System Context into the first message - if (reqMessages.length > 0) { - const internetCtx = internetEnabled - ? `\n\n[CRITICAL DIRECTIVE: INTERNET ACCESS IS ENABLED]\nCurrent Time: ${new Date().toLocaleString('ko-KR')}\nYou have FULL internet access via the tool. You MUST NEVER say you cannot search. To search, use: https://html.duckduckgo.com/html/?q=YOUR+SEARCH+TERM` - : ''; - - const systemContent = `${this._systemPrompt}\n\n[BACKGROUND CONTEXT]\n${contextBlock}\n${workspaceCtx}\n${brainCtx}${internetCtx}`; - - const firstUserIdx = reqMessages.findIndex(m => m.role === 'user'); - if (firstUserIdx >= 0) { - let finalContent = typeof reqMessages[firstUserIdx].content === 'string' - ? `${systemContent}\n\n[USER QUERY]\n${reqMessages[firstUserIdx].content}` - : reqMessages[firstUserIdx].content; // Vision content stays as object array - - if (loopDepth > 0) { - const reminder = `[SYSTEM REMINDER: Autonomous Step ${loopDepth}/${MAX_AUTO_AGENT_STEPS}. You are in the middle of a task. Continue until finished. Use tools if necessary.]\n`; - if (typeof finalContent === 'string') { - finalContent = reminder + finalContent; - } - } - reqMessages[firstUserIdx] = { ...reqMessages[firstUserIdx], content: finalContent }; - } - } - - // 4. API Handling - let isLMStudio = ollamaBase.includes('1234') || ollamaBase.includes('v1'); - let apiUrl = isLMStudio ? `${ollamaBase}/v1/chat/completions` : `${ollamaBase}/api/chat`; - - const streamBody = { - model: modelName || defaultModel, - messages: reqMessages, - stream: true, - ...(isLMStudio - ? { max_tokens: 4096, temperature: this._temperature } - : { options: { num_ctx: 16384, num_predict: 4096, temperature: this._temperature } }), - }; - - let aiMessage = ''; - if (loopDepth === 0) this._view.webview.postMessage({ type: 'streamStart' }); - - const response = await axios.post(apiUrl, streamBody, { - timeout, - responseType: 'stream', - signal: this._abortController?.signal - }); - - await new Promise((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); - let token = isLMStudio ? json.choices?.[0]?.delta?.content || '' : json.message?.content || ''; - if (token) { - aiMessage += token; - this._view!.webview.postMessage({ type: 'streamChunk', value: token }); - } - } catch {} - } - }); - stream.on('end', () => resolve()); - stream.on('error', (err: any) => reject(err)); - }); - - if (loopDepth === 0) this._view.webview.postMessage({ type: 'streamEnd' }); - this._chatHistory.push({ role: 'assistant', content: aiMessage }); - - // 5. Action Execution - const report = await this._executeActions(aiMessage); - - if (report.length > 0) { - const reportMsg = `\n\n---\n**[Agent Action Report] (${loopDepth + 1}/${MAX_AUTO_AGENT_STEPS})**\n${report.join("\n")}`; - this._view.webview.postMessage({ type: 'streamChunk', value: reportMsg }); - this._view.webview.postMessage({ type: 'streamEnd' }); - aiMessage += reportMsg; - } - - // 嶺뚮씭??嶺????嶺???戮?뻣 嶺뚮∥???낆????????(??蹂μ쟽 ??蹂ㅽ깴) - if (loopDepth === 0 || report.length === 0) { - this._displayMessages.push({ text: this._stripActionTags(aiMessage), role: 'ai' }); - } - - // 6. Loop Control (MAX 50) - if (report.length > 0 && loopDepth < MAX_AUTO_AGENT_STEPS) { - const currentActionStr = report.join('|'); - const lastActionStr = this._ctx.workspaceState.get('lastActionStr'); - - if (currentActionStr === lastActionStr) { - this._view.webview.postMessage({ type: "streamChunk", value: "\n\n**[Repetition Detected]** AI is repeating the same action. Providing a hint to diverge..." }); - this._chatHistory.push({ role: "user", content: "[System Reminder] You just performed the exact same action. Please try a different approach (e.g., read a different file, check a different directory, or summarize results) to avoid an infinite loop." }); - // 猷⑦봽 醫낅즺 ?€??怨꾩냽 吏꾪뻾 (HINT 二쇱엯 ?곹깭濡? - } - - await this._ctx.workspaceState.update('lastActionStr', currentActionStr); - this._view.webview.postMessage({ type: 'autoContinue', value: `Thinking... (${loopDepth + 1}/${MAX_AUTO_AGENT_STEPS})` }); - - // ??類ㅼ읉???? ?낅슣?섇젆?嶺뚮ㅄ維€?????ル∥六???關諭???μ쪚??듭물??? await new Promise(r => setTimeout(r, 500)); - await this._handlePrompt(null, modelName, internetEnabled, loopDepth + 1); - } - } catch (error: any) { - this._view.webview.postMessage({ type: "error", value: `[Error]: ${error.message}` }); - } - } - - - private async _executeActions(aiMessage: string): Promise { - const report: string[] = []; - const rootPath = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath; - if (!rootPath) return report; - - let brainModified = false; - let firstCreatedFile: string | undefined; - let match; - - // ACTION 1: Create File - const createRegex = /([\s\S]*?)<\/create_file>/gi; - while ((match = createRegex.exec(aiMessage)) !== null) { - const relPath = match[1].trim(); - const content = match[2].trim(); - const absPath = path.resolve(rootPath, relPath); - try { - fs.mkdirSync(path.dirname(absPath), { recursive: true }); - fs.writeFileSync(absPath, content, 'utf-8'); - if (absPath.startsWith(_getBrainDir())) brainModified = true; - report.push(`Created: ${relPath}`); - if (!firstCreatedFile) firstCreatedFile = absPath; - } catch (err: any) { report.push(`Error Creating: ${relPath} - ${err.message}`); } - } - - // ACTION 2: Edit File (Search & Replace Support) - const editRegex = /([\s\S]*?)<\/edit_file>/gi; - while ((match = editRegex.exec(aiMessage)) !== null) { - const relPath = match[1].trim(); - const editContent = match[2].trim(); - const absPath = path.resolve(rootPath, relPath); - try { - if (fs.existsSync(absPath)) { - let currentContent = fs.readFileSync(absPath, 'utf-8'); - const searchMatch = editContent.match(/([\s\S]*?)<\/search>\s*([\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 (S&R): ${relPath}`); - } else { - report.push(`Error: 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(`Error: File not found for editing: ${relPath}`); - } - } catch (err: any) { report.push(`Error Editing: ${relPath} - ${err.message}`); } - } - - // ACTION 3: Read File - const readRegex = /<(?:read_file|read)\s+(?:path|file)=['"]?([^'"\/\>]*)['"]?\s*\/?>(?:<\/(?:read_file|read)>)?/gi; - while ((match = readRegex.exec(aiMessage)) !== null) { - const relPath = match[1].trim(); - const absPath = path.resolve(rootPath, relPath); - try { - if (fs.existsSync(absPath)) { - const content = fs.readFileSync(absPath, 'utf-8'); - const preview = content.length > 5000 ? content.slice(0, 5000) + "\n... (truncated)" : content; - report.push(`Read: ${relPath} (${content.length} chars)`); - this._chatHistory.push({ role: 'user', content: `[Action: read_file result for ${relPath}]\n\`\`\`\n${preview}\n\`\`\`` }); - } else { - report.push(`Error: File not found for reading: ${relPath}`); - } - } catch (err: any) { report.push(`Error Reading: ${relPath} - ${err.message}`); } - } - - // ACTION 4: Delete File - const deleteRegex = /<(?:delete_file|delete)\s+path=['"]?([^'"]+)['"]?\s*\/?>(?:<\/(?:delete_file|delete)>)?/gi; - while ((match = deleteRegex.exec(aiMessage)) !== null) { - const relPath = match[1].trim(); - const absPath = path.resolve(rootPath, relPath); - try { - if (fs.existsSync(absPath)) { - fs.unlinkSync(absPath); - if (absPath.startsWith(_getBrainDir())) brainModified = true; - report.push(`Deleted: ${relPath}`); - } else { - report.push(`Error: File not found for deletion: ${relPath}`); - } - } catch (err: any) { report.push(`Error Deleting: ${relPath} - ${err.message}`); } - } - - // ACTION 5: List directory - const listRegex = /<(?:list_files|list_dir|ls)\s+(?:path|dir|name)=['"]?([^'"\/\>]*)['"]?\s*\/?>(?:<\/(?:list_files|list_dir|ls)>)?/gi; - while ((match = listRegex.exec(aiMessage)) !== null) { - const relDir = match[1].trim() || '.'; - const absDir = path.resolve(rootPath, relDir); - try { - if (fs.existsSync(absDir) && fs.statSync(absDir).isDirectory()) { - const entries = fs.readdirSync(absDir, { 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: ${relDir}/\n\`\`\`\n${listing}\n\`\`\``); - this._chatHistory.push({ role: 'user', content: `[Action: list_files result for ${relDir}]\n${listing}` }); - } else { - report.push(`Error: Directory not found: ${relDir}`); - } - } catch (err: any) { report.push(`Error Listing: ${relDir} - ${err.message}`); } - } - - // ACTION 6: Run commands - const cmdRegex = /<(?:run_command|command|bash|terminal)>([\s\S]*?)<\/(?:run_command|command|bash|terminal)>/gi; - while ((match = cmdRegex.exec(aiMessage)) !== null) { - let cmd = match[1].trim(); - if (cmd.startsWith('```')) { - const lines = cmd.split('\n'); - if (lines[0].startsWith('```')) lines.shift(); - if (lines.length > 0 && lines[lines.length - 1].startsWith('```')) lines.pop(); - cmd = lines.join('\n').trim(); - } - try { - if (!this._terminal || this._terminal.exitStatus !== undefined) { - this._terminal = vscode.window.createTerminal({ name: 'G1nation Terminal', cwd: rootPath }); - } - this._terminal.show(); - this._terminal.sendText(cmd); - report.push(`Executed Command: ${cmd}`); - } catch (err: any) { report.push(`Error Executing Command: ${cmd} - ${err.message}`); } - } - - // ACTION 7: Read URL - const urlRegex = /<(?:read_url|url|fetch_url)>([\s\S]*?)<\/(?:read_url|url|fetch_url)>/gi; - while ((match = urlRegex.exec(aiMessage)) !== null) { - const url = match[1].trim(); - try { - const { data } = await axios.get(url, { headers: { 'User-Agent': 'Mozilla/5.0' }, timeout: 10000 }); - let cleaned = data.toString() - .replace(/)<[^<]*)*<\/script>/gi, '') - .replace(/)<[^<]*)*<\/style>/gi, '') - .replace(/<[^>]+>/g, ' ') - .replace(/\s+/g, ' ') - .trim(); - report.push(`Fetched URL: ${url} (${cleaned.length} chars)`); - this._chatHistory.push({ role: 'user', content: `[Action: read_url result for ${url}]\n\`\`\`\n${cleaned.slice(0, 15000)}\n\`\`\`` }); - } catch (err: any) { - report.push(`Error Fetching URL: ${url} - ${err.message}`); - } - } - - // FALLBACK: Markdown code blocks - if (report.length === 0) { - const fallbackRegex = /```(?:[a-zA-Z]*)?\s*\n\/\/\s*(?:file|path):\s*([^\n]+)\n([\s\S]*?)```/gi; - while ((match = fallbackRegex.exec(aiMessage)) !== null) { - const relPath = match[1].trim(); - const content = match[2].trim(); - if (relPath && content && relPath.includes('.')) { - try { - const absPath = path.join(rootPath, relPath); - fs.mkdirSync(path.dirname(absPath), { recursive: true }); - fs.writeFileSync(absPath, content, 'utf-8'); - report.push(`Created: ${relPath}`); - if (!firstCreatedFile) firstCreatedFile = absPath; - } catch (err: any) { report.push(`Error: ${relPath} - ${err.message}`); } - } - } - } - - if (firstCreatedFile) { - vscode.window.showTextDocument(vscode.Uri.file(firstCreatedFile), { preview: false }); - } - - // Final Notifications - const successCount = report.filter(r => r.startsWith('Created') || r.startsWith('Updated') || r.startsWith('Deleted') || r.startsWith('Listed') || r.startsWith('Executed') || r.startsWith('Read') || r.startsWith('Fetched')).length; - if (successCount > 0) { - vscode.window.showInformationMessage(`G1nation: Successfully processed ${successCount} actions.`); - } - - if (brainModified && shouldAutoPushBrain() && getSecondBrainRepo()) { - try { - const brainDir = _getBrainDir(); const { execSync } = require('child_process'); execSync(`git add .`, { cwd: brainDir }); - execSync(`git commit -m "[P-Reinforce] Auto-synced knowledge"`, { cwd: brainDir }).catch(() => {}); - execSync(`git push`, { cwd: brainDir }).catch(() => {}); - } catch {} + execSync(`git commit -m "[G1-Sync] Manual knowledge update"`, { cwd: brainDir }); + execSync(`git push`, { cwd: brainDir }); + vscode.window.showInformationMessage("Second Brain synced successfully."); + } catch (err: any) { + vscode.window.showWarningMessage("Sync completed (or no changes to push)."); + } + }); + } + + private async _handlePrompt(data: any) { + if (!this._view) return; + + const { value, model, internet, files } = data; + this._view.webview.postMessage({ type: 'streamStart' }); + + try { + await this._agent.execute(value, { + model, + internet, + files, + onToken: (token) => { + this._view?.webview.postMessage({ type: 'streamChunk', value: token }); + } + }); + this._view.webview.postMessage({ type: 'streamEnd' }); + } catch (error: any) { + this._view.webview.postMessage({ type: 'error', value: error.message }); } - - return report; - } - // Strip raw XML action tags from display message - private _stripActionTags(text: string): string { - return text - .replace(/<(?:create_file|file)\s+[^>]*>[\s\S]*?<\/(?:create_file|file)>/gi, '') - .replace(/<(?:edit_file|edit)\s+[^>]*>[\s\S]*?<\/(?:edit_file|edit)>/gi, '') - .replace(/<(?:delete_file|delete)\s+[^>]*\s*\/?>(?:<\/(?:delete_file|delete)>)?/gi, '') - .replace(/<(?:read_file|read)\s+[^>]*\s*\/?>(?:<\/(?:read_file|read)>)?/gi, '') - .replace(/<(?:list_files|list_dir|ls)\s+[^>]*\s*\/?>(?:<\/(?:list_files|list_dir|ls)>)?/gi, '') - .replace(/<(?:run_command|command|bash|terminal)>[\s\S]*?<\/(?:run_command|command|bash|terminal)>/gi, '') - .replace(/<(?:read_brain)>[\s\S]*?<\/(?:read_brain)>/gi, '') - .trim(); } + private async _sendModels() { + if (!this._view) return; + try { + const config = getConfig(); + const url = config.ollamaUrl; + let models: string[] = []; - // ============================================================ - // Webview HTML ??CINEMATIC UI v3 (Content-Grade Visuals) - // ============================================================ - private _getHtml(): string { + if (url.includes('1234')) { + const res = await axios.get(`${url}/v1/models`, { timeout: 2000 }); + models = res.data.data.map((m: any) => m.id); + } else { + const res = await axios.get(`${url}/api/tags`, { timeout: 2000 }); + models = res.data.models.map((m: any) => m.name); + } + this._view.webview.postMessage({ type: 'modelsList', value: models }); + } catch (err) { + this._view.webview.postMessage({ type: 'modelsList', value: ['default'] }); + } + } + + private _getHtml(webview: vscode.Webview): string { return ` G1nation @@ -1951,13 +298,9 @@ body.vscode-light { --input-bg:rgba(255,255,255,.9);--code-bg:#f5f5f7; } html,body{height:100%;font-family:'SF Pro Display',-apple-system,BlinkMacSystemFont,'Segoe UI',system-ui,sans-serif;font-size:13px;background:var(--bg);color:var(--text);display:flex;flex-direction:column;overflow:hidden;min-height:0} - -/* AURORA BACKGROUND */ body::before{content:'';position:fixed;top:-50%;left:-50%;width:200%;height:200%;background:radial-gradient(ellipse at 20% 50%,rgba(124,106,255,.06) 0%,transparent 50%),radial-gradient(ellipse at 80% 20%,rgba(224,64,251,.04) 0%,transparent 50%),radial-gradient(ellipse at 50% 80%,rgba(0,229,255,.03) 0%,transparent 50%);animation:aurora 20s ease-in-out infinite;z-index:0;pointer-events:none} @keyframes aurora{0%,100%{transform:translate(0,0) rotate(0deg)}33%{transform:translate(2%,-1%) rotate(.5deg)}66%{transform:translate(-1%,2%) rotate(-.5deg)}} - -/* HEADER */ -.header{display:flex;align-items:center;justify-content:space-between;padding:10px 14px;background:rgba(10,10,12,.8);backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px);border-bottom:1px solid var(--border);flex-shrink:0;position:relative;z-index:10} +.header{display:flex;align-items:center;justify-content:space-between;padding:10px 14px;background:rgba(10,10,12,.8);backdrop-filter:blur(20px);border-bottom:1px solid var(--border);flex-shrink:0;position:relative;z-index:10} .header::after{content:'';position:absolute;bottom:0;left:0;right:0;height:1px;background:linear-gradient(90deg,transparent 5%,var(--accent) 30%,var(--accent2) 50%,var(--accent3) 70%,transparent 95%);opacity:.5;animation:headerGlow 4s ease-in-out infinite alternate} @keyframes headerGlow{0%{opacity:.3}100%{opacity:.6}} .thinking-bar{height:2px;background:transparent;position:relative;overflow:hidden;flex-shrink:0;z-index:10} @@ -1966,22 +309,15 @@ body::before{content:'';position:fixed;top:-50%;left:-50%;width:200%;height:200% @keyframes thinkSlide{0%{left:-40%}100%{left:100%}} .header-left{display:flex;align-items:center;gap:8px} .logo{width:26px;height:26px;border-radius:6px;background:#050505;border:1px solid rgba(0,255,65,.3);display:flex;align-items:center;justify-content:center;font-size:16px;color:var(--accent);box-shadow:0 0 15px rgba(0,255,65,.15);animation:logoPulse 3s ease-in-out infinite;position:relative;text-shadow:0 0 8px var(--accent)} -.logo::after{content:'';position:absolute;inset:-1px;border-radius:7px;background:var(--accent);opacity:.2;filter:blur(3px);animation:logoPulse 3s ease-in-out infinite} @keyframes logoPulse{0%,100%{box-shadow:0 0 10px rgba(0,255,65,.1)}50%{box-shadow:0 0 25px rgba(0,255,65,.3)}} .brand{font-weight:800;font-size:14px;color:var(--text-bright);letter-spacing:-.5px;background:linear-gradient(135deg,#fff 40%,var(--accent) 100%);-webkit-background-clip:text;-webkit-text-fill-color:transparent} .header-right{display:flex;align-items:center;gap:5px} -select{background:rgba(22,22,28,.9);color:var(--text-bright);border:1px solid var(--border2);padding:5px 8px;border-radius:8px;font-size:10px;font-family:inherit;cursor:pointer;outline:none;max-width:120px;transition:all .3s;backdrop-filter:blur(8px)} +select{background:rgba(22,22,28,.9);color:var(--text-bright);border:1px solid var(--border2);padding:5px 8px;border-radius:8px;font-size:10px;cursor:pointer;outline:none;max-width:120px;transition:all .3s;backdrop-filter:blur(8px)} select:hover,select:focus{border-color:var(--accent);box-shadow:0 0 12px var(--accent-glow)} .btn-icon{background:rgba(22,22,28,.7);border:1px solid var(--border2);color:var(--text-dim);width:28px;height:28px;border-radius:8px;font-size:13px;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all .3s;backdrop-filter:blur(8px);position:relative;overflow:hidden} -.btn-icon::before{content:'';position:absolute;inset:0;background:linear-gradient(135deg,var(--accent-glow),var(--accent2-glow));opacity:0;transition:opacity .3s} .btn-icon:hover{color:var(--text-bright);border-color:var(--accent);transform:translateY(-1px);box-shadow:0 4px 15px var(--accent-glow)} -.btn-icon:hover::before{opacity:1} - -/* CHAT */ .chat{flex:1;overflow-y:auto;padding:16px 14px;display:flex;flex-direction:column;gap:16px;position:relative;z-index:1;min-height:0} .chat::-webkit-scrollbar{width:2px}.chat::-webkit-scrollbar-track{background:transparent}.chat::-webkit-scrollbar-thumb{background:var(--accent);border-radius:2px;opacity:.5} - -/* MESSAGES */ .msg{display:flex;flex-direction:column;gap:5px;animation:msgIn .5s cubic-bezier(.16,1,.3,1)} .msg-head{display:flex;align-items:center;gap:7px;font-weight:600;font-size:11px;color:var(--text)} .msg-time{font-weight:400;font-size:9px;color:var(--text-dim);margin-left:auto;opacity:.6} @@ -1991,38 +327,17 @@ select:hover,select:focus{border-color:var(--accent);box-shadow:0 0 12px var(--a .msg-body{padding-left:29px;line-height:1.75;color:var(--text);white-space:pre-wrap;word-break:break-word;font-size:13px} .msg-user .msg-body{background:var(--surface);border:1px solid var(--border2);border-radius:14px;padding:10px 14px;margin-left:29px;color:var(--text-bright);backdrop-filter:blur(8px)} .msg-body pre{background:var(--code-bg);border:1px solid var(--border2);border-radius:10px;padding:14px 16px;overflow-x:auto;margin:8px 0;font-size:12px;line-height:1.6;color:#c9d1d9;position:relative} -.msg-body pre::-webkit-scrollbar{height:6px} -.msg-body pre::-webkit-scrollbar-track{background:rgba(0,0,0,.2);border-radius:4px} -.msg-body pre::-webkit-scrollbar-thumb{background:rgba(124,106,255,.3);border-radius:4px} -.msg-body pre::-webkit-scrollbar-thumb:hover{background:rgba(124,106,255,.6)} .msg-body code{font-family:'SF Mono','JetBrains Mono','Fira Code','Menlo',monospace;font-size:11.5px} .msg-body :not(pre)>code{background:rgba(124,106,255,.1);color:var(--accent);padding:2px 7px;border-radius:5px;border:1px solid rgba(124,106,255,.15)} -.msg-body a{color:var(--accent);text-decoration:none} -.msg-body a:hover{text-decoration:underline} .code-wrap{position:relative} .code-lang{position:absolute;top:0;left:14px;background:linear-gradient(135deg,var(--accent),var(--accent2));color:#fff;padding:2px 10px;border-radius:0 0 6px 6px;font-size:9px;font-family:'SF Mono',monospace;text-transform:uppercase;letter-spacing:.5px;font-weight:600} -.copy-btn{position:absolute;top:8px;right:8px;background:var(--surface2);border:1px solid var(--border2);color:var(--text-dim);padding:4px 12px;border-radius:6px;font-size:10px;cursor:pointer;opacity:0;transition:all .3s;font-family:inherit;z-index:1;backdrop-filter:blur(8px)} +.copy-btn{position:absolute;top:8px;right:8px;background:var(--surface2);border:1px solid var(--border2);color:var(--text-dim);padding:4px 12px;border-radius:6px;font-size:10px;cursor:pointer;opacity:0;transition:all .3s;z-index:1;backdrop-filter:blur(8px)} .code-wrap:hover .copy-btn{opacity:1}.copy-btn:hover{background:var(--accent);color:#fff;border-color:var(--accent);box-shadow:0 0 12px var(--accent-glow)} -.copy-btn.copied{background:var(--green);color:#fff;border-color:var(--green);opacity:1} - -/* BADGES */ -.file-badge{background:rgba(255,171,64,.05);border:1px solid rgba(255,171,64,.2);border-radius:10px 10px 0 0;border-bottom:none;padding:8px 14px;font-size:11px;font-weight:700;color:var(--yellow);display:flex;align-items:center;gap:6px;backdrop-filter:blur(8px)} -.edit-badge{background:rgba(0,229,255,.05);border:1px solid rgba(0,229,255,.2);border-radius:10px 10px 0 0;border-bottom:none;padding:8px 14px;font-size:11px;font-weight:700;color:var(--cyan);display:flex;align-items:center;gap:6px;backdrop-filter:blur(8px)} -.cmd-badge{background:rgba(124,106,255,.05);border:1px solid rgba(124,106,255,.25);border-radius:10px;padding:10px 14px;margin:8px 0;font-size:12px;color:var(--accent);font-family:'SF Mono','Menlo',monospace;display:flex;align-items:center;gap:8px;backdrop-filter:blur(8px)} -.msg-error .msg-body{color:var(--red);text-shadow:0 0 20px rgba(255,82,82,.2)} - -/* WELCOME */ .welcome{text-align:center;padding:0 20px 20px;position:relative} .welcome-logo{width:56px;height:56px;border-radius:16px;margin:0 auto 16px;background:#050505;border:1px solid rgba(0,255,65,.3);display:flex;align-items:center;justify-content:center;font-size:32px;color:var(--accent);box-shadow:inset 0 0 15px rgba(0,255,65,.1), 0 0 30px rgba(0,255,65,.2);animation:welcomeFloat 4s ease-in-out infinite;position:relative;text-shadow:0 0 15px var(--accent)} -.welcome-logo::before{content:'';position:absolute;inset:-2px;border-radius:18px;background:var(--accent);opacity:.15;filter:blur(8px);animation:pulseGlow 3s linear infinite} -@keyframes pulseGlow{0%,100%{opacity:.15;filter:blur(8px)}50%{opacity:.3;filter:blur(12px)}} -@keyframes spin{to{transform:rotate(360deg)}} @keyframes welcomeFloat{0%,100%{transform:translateY(0) scale(1)}50%{transform:translateY(-6px) scale(1.03)}} .welcome-title{font-size:22px;font-weight:900;letter-spacing:-1px;color:var(--text-bright);margin-bottom:8px} -@keyframes gradText{0%,100%{background-position:0% 50%}50%{background-position:100% 50%}} .welcome-sub{color:var(--text-dim);font-size:12px;line-height:1.7;margin-bottom:18px;letter-spacing:-.2px} - -/* LOADING */ .loading-wrap{padding-left:29px;padding-top:6px;display:flex;align-items:center;gap:10px} .loading-dots{display:flex;gap:4px} .loading-dots span{width:6px;height:6px;border-radius:50%;background:var(--accent);animation:dotBounce 1.4s ease-in-out infinite} @@ -2030,100 +345,60 @@ select:hover,select:focus{border-color:var(--accent);box-shadow:0 0 12px var(--a .loading-dots span:nth-child(3){animation-delay:.4s;background:var(--accent3)} @keyframes dotBounce{0%,80%,100%{transform:scale(.6);opacity:.4}40%{transform:scale(1.2);opacity:1}} .loading-text{font-size:11px;color:var(--text-dim);animation:pulse 2s ease-in-out infinite;letter-spacing:.3px} - -/* INPUT */ .input-wrap{padding:8px 14px 14px;flex-shrink:0;position:relative;z-index:1} .input-box{background:var(--input-bg);border:1px solid var(--border2);border-radius:14px;padding:12px 14px;display:flex;flex-direction:column;gap:8px;transition:all .3s;position:relative;backdrop-filter:blur(12px)} -.input-box:focus-within{border-color:var(--accent);box-shadow:0 0 24px rgba(0,255,65,.15);animation:focusPulse 3s infinite} -@keyframes focusPulse{0%,100%{box-shadow:0 0 20px rgba(0,255,65,.08)}50%{box-shadow:0 0 28px rgba(0,255,65,.2)}} +.input-box:focus-within{border-color:var(--accent);box-shadow:0 0 24px rgba(0,255,65,.15)} textarea{width:100%;background:transparent;border:none;color:var(--text-bright);font-family:inherit;font-size:13px;line-height:1.5;resize:none;outline:none;min-height:22px;max-height:150px} textarea::placeholder{color:var(--text-dim)} .input-footer{display:flex;align-items:center;justify-content:space-between} .input-hint{font-size:10px;color:var(--text-dim);opacity:.5} .input-btns{display:flex;gap:5px} .send-btn{background:linear-gradient(135deg,var(--accent),var(--accent2));border:none;color:#fff;width:32px;height:32px;border-radius:10px;cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:14px;transition:all .2s;box-shadow:0 2px 12px rgba(124,106,255,.35);position:relative;overflow:hidden} -.send-btn::after{content:'';position:absolute;inset:0;background:linear-gradient(135deg,transparent,rgba(255,255,255,.15));opacity:0;transition:opacity .3s} .send-btn:hover{transform:translateY(-2px) scale(1.05);box-shadow:0 6px 24px rgba(124,106,255,.45)} -.send-btn:hover::after{opacity:1} -.send-btn:active{transform:scale(.92)}.send-btn:disabled{opacity:.2;cursor:not-allowed;transform:none;box-shadow:none} +.send-btn:active{transform:scale(.92)}.send-btn:disabled{opacity:.2;cursor:not-allowed} .stop-btn{background:var(--red);border:none;color:#fff;width:32px;height:32px;border-radius:10px;cursor:pointer;display:none;align-items:center;justify-content:center;font-size:11px;box-shadow:0 0 12px rgba(255,82,82,.3)} .stop-btn.visible{display:flex} @keyframes msgIn{from{opacity:0;transform:translateY(12px) scale(.97)}to{opacity:1;transform:translateY(0) scale(1)}} @keyframes pulse{0%,100%{opacity:.4}50%{opacity:1}} -.stream-active{position:relative} -.stream-active::after{content:'';display:inline-block;width:2px;height:14px;background:var(--accent);margin-left:2px;animation:blink .6s step-end infinite;vertical-align:text-bottom;border-radius:1px;box-shadow:0 0 6px var(--accent)} +.stream-active::after{content:'';display:inline-block;width:2px;height:14px;background:var(--accent);margin-left:2px;animation:blink .6s step-end infinite;vertical-align:text-bottom;box-shadow:0 0 6px var(--accent)} @keyframes blink{0%,100%{opacity:1}50%{opacity:0}} -.stream-active .code-wrap:last-child { - border: 1px solid var(--accent); - animation: codePulse 2s infinite; -} -.stream-active .code-wrap:last-child pre { - box-shadow: inset 0 0 20px rgba(124,106,255,0.05); -} -@keyframes codePulse { - 0%, 100% { box-shadow: 0 0 15px var(--accent-glow); } - 50% { box-shadow: 0 0 35px var(--accent2-glow); border-color: var(--accent2); } -} .main-view{flex:1;display:flex;flex-direction:column;overflow:hidden;transition:all .5s cubic-bezier(.16,1,.3,1);min-height:0;max-height:100%} body.init .main-view{justify-content:center;margin-top:-6vh} body.init .chat{flex:0 0 auto;overflow:visible;padding-bottom:15px} -body.init .input-wrap{max-width:680px;width:100%;margin:0 auto;transform:none;transition:all .5s cubic-bezier(.16,1,.3,1)} - -/* ATTACHMENT */ +body.init .input-wrap{max-width:680px;width:100%;margin:0 auto} .attach-btn{background:transparent;border:1px solid var(--border2);color:var(--text-dim);width:32px;height:32px;border-radius:10px;cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:16px;transition:all .3s;flex-shrink:0} -.attach-btn:hover{color:var(--accent);border-color:var(--accent);box-shadow:0 0 12px var(--accent-glow);transform:translateY(-1px)} +.attach-btn:hover{color:var(--accent);border-color:var(--accent);box-shadow:0 0 12px var(--accent-glow)} .attach-preview{display:none;gap:6px;padding:0 0 6px;flex-wrap:wrap} .attach-preview.visible{display:flex} -.attach-chip{display:flex;align-items:center;gap:5px;background:var(--surface2);border:1px solid var(--border2);border-radius:8px;padding:4px 10px;font-size:10px;color:var(--text);animation:msgIn .3s ease} -.attach-chip .chip-icon{font-size:12px} -.attach-chip .chip-name{max-width:100px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} -.attach-chip .chip-remove{cursor:pointer;color:var(--text-dim);font-size:12px;margin-left:2px;transition:color .2s} +.attach-chip{display:flex;align-items:center;gap:5px;background:var(--surface2);border:1px solid var(--border2);border-radius:8px;padding:4px 10px;font-size:10px;color:var(--text)} +.attach-chip .chip-remove{cursor:pointer;color:var(--text-dim);font-size:12px;margin-left:2px} .attach-chip .chip-remove:hover{color:var(--red)} .attach-thumb{width:28px;height:28px;border-radius:5px;object-fit:cover;border:1px solid var(--border2)} - -/* REGENERATE BUTTON */ -.regen-btn{display:inline-flex;align-items:center;gap:4px;background:transparent;border:none;color:var(--text-dim);padding:4px 6px;border-radius:4px;font-size:11px;cursor:pointer;transition:color 0.2s;font-family:inherit;margin-top:6px;margin-left:29px;opacity:0.7} +.regen-btn{display:inline-flex;align-items:center;gap:4px;background:transparent;border:none;color:var(--text-dim);padding:4px 6px;border-radius:4px;font-size:11px;cursor:pointer;transition:color 0.2s;margin-top:6px;margin-left:29px;opacity:0.7} .regen-btn:hover{color:var(--text);opacity:1} - -/* SYNTAX HIGHLIGHTING */ -.msg-body pre .kw{color:#c792ea} -.msg-body pre .str{color:#c3e88d} -.msg-body pre .num{color:#f78c6c} -.msg-body pre .cm{color:#546e7a;font-style:italic} -.msg-body pre .fn{color:#82aaff} -.msg-body pre .tag{color:#f07178} -.msg-body pre .attr{color:#ffcb6b} -.msg-body pre .op{color:#89ddff} -.msg-body pre .type{color:#ffcb6b} -
G1nation
+
G1nation
- +
G1nation
-
\ubcf4\uc548 \u00b7 \ube44\uc6a9\ucd5c\uc801\ud654 \u00b7 \uc9c0\uc2dd\uc5f0\uacb0
\ud504\ub85c\uc81d\ud2b8\ub97c \uc774\ud574\ud558\uace0, \ucf54\ub4dc\ub97c \uc791\uc131\ud558\uace0, \uc2e4\ud589\ud569\ub2c8\ub2e4.
+
Security · Optimized · Knowledge Mesh
Understands your project, writes code, and executes tasks.
- -
+ +
`; } } \ No newline at end of file diff --git a/src/security.ts b/src/security.ts new file mode 100644 index 0000000..1271c30 --- /dev/null +++ b/src/security.ts @@ -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; +} diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000..89bd65c --- /dev/null +++ b/src/utils.ts @@ -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('ollamaUrl', 'http://127.0.0.1:11434'), + defaultModel: cfg.get('defaultModel', 'gemma4:e2b'), + maxTreeFiles: 200, + timeout: cfg.get('requestTimeout', 300) * 1000, + localBrainPath: cfg.get('localBrainPath', '') + }; +} + +export function shouldAutoPushBrain(): boolean { + const cfg = vscode.workspace.getConfiguration('g1nation'); + return cfg.get('autoPushBrain', false); +} + +export function getSecondBrainRepo(): string { + const cfg = vscode.workspace.getConfiguration('g1nation'); + return cfg.get('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] + +file content here + + + [ACTION 2: EDIT EXISTING FILES] + +exact text to find +replacement text + + + [ACTION 3: DELETE FILES] + + + [ACTION 4: READ FILES] + + + [ACTION 5: LIST DIRECTORY] + + + [ACTION 6: RUN TERMINAL COMMANDS] +npm install express + + [ACTION 7: READ USER'S SECOND BRAIN] +filename.md + + [ACTION 8: READ WEBSITES & SEARCH INTERNET] +https://html.duckduckgo.com/html/?q=YOUR+SEARCH+QUERY + +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.`;