import * as vscode from 'vscode'; import * as os from 'os'; import * as path from 'path'; import * as fs from 'fs'; // ─── 브레인 프로필 인터페이스 ─── export interface BrainProfile { id: string; name: string; localBrainPath: string; secondBrainRepo?: string; description?: string; } // ─── 에이전트 설정 인터페이스 (통합 버전) ─── export interface IAgentConfig { ollamaUrl: string; defaultModel: string; maxTreeFiles: number; timeout: number; localBrainPath: string; secondBrainRepo: string; brainProfiles: BrainProfile[]; activeBrainId: string; maxContextSize: number; maxAutoSteps: number; dryRun: boolean; multiAgentEnabled: boolean; memoryEnabled: boolean; memoryShortTermMessages: number; memoryMediumTermSessions: number; memoryLongTermFiles: number; // ─── 컨텍스트 한계 관리 ─── contextLength: number; maxOutputTokens: number; contextSafetyMargin: number; contextOverflowPolicy: 'stopAtLimit' | 'truncateMiddle' | 'rollingWindow'; autoCompactHistory: boolean; /** 작은 모델(≤4B) 감지 시 예산 계산에 쓸 유효 context window 상한. 0 = 비활성화. */ smallModelContextCap: number; // ─── 응답 복구 (Thought Quarantine / Auto-Continuation) ─── /** 답변이 출력 토큰 한계에 걸리면 사용자 개입 없이 내부적으로 이어서 생성. */ autoContinueOnOutputLimit: boolean; /** 자동 이어쓰기 최대 횟수 (무한 반복 방지). 0 = 비활성화. */ maxAutoContinuations: number; /** 모델이 내부 사고만 출력하고 답변이 없으면 "최종 답변만" 지시로 1회 재생성. */ finalOnlyRetryOnThoughtLeak: boolean; } // ─── 경로 정규화 유틸리티 ─── function normalizePath(p: string): string { if (!p) return p; if (p.startsWith('~/')) { return path.join(os.homedir(), p.substring(2)); } return p.trim(); } function toBrainProfile(raw: Partial | undefined, fallbackIndex: number): BrainProfile | null { if (!raw) return null; const localBrainPath = normalizePath(raw.localBrainPath || ''); if (!localBrainPath) return null; return { id: (raw.id || `brain-${fallbackIndex + 1}`).trim(), name: (raw.name || path.basename(localBrainPath) || `Brain ${fallbackIndex + 1}`).trim(), localBrainPath, secondBrainRepo: (raw.secondBrainRepo || '').trim(), description: (raw.description || '').trim() }; } // ─── VS Code 설정에서 읽어오는 값 (통합 구현) ─── export function getConfig(): IAgentConfig { const cfg = vscode.workspace.getConfiguration('g1nation'); // 브레인 프로필 로직 (utils.ts에서 이관) const legacyBrainPath = cfg.get('localBrainPath', ''); const legacyBrainRepo = cfg.get('secondBrainRepo', ''); const configuredProfiles = cfg.get[]>('brainProfiles', []); const profiles = configuredProfiles .map((profile, index) => toBrainProfile(profile, index)) .filter((profile): profile is BrainProfile => !!profile); if (profiles.length === 0) { const fallbackPath = normalizePath(legacyBrainPath) || path.join(os.homedir(), '.g1nation-brain'); profiles.push({ id: 'default-brain', name: 'Local Brain', localBrainPath: fallbackPath, secondBrainRepo: legacyBrainRepo.trim(), description: legacyBrainPath ? 'Migrated from your existing localBrainPath setting' : 'Auto-created local knowledge folder.' }); } const activeBrainId = cfg.get('activeBrainId', profiles[0].id) || profiles[0].id; const activeBrain = profiles.find((profile) => profile.id === activeBrainId) || profiles[0]; return { ollamaUrl: cfg.get('ollamaUrl', 'http://127.0.0.1:11434') || 'http://127.0.0.1:11434', defaultModel: cfg.get('defaultModel', 'gemma4:e2b') || 'gemma4:e2b', maxTreeFiles: 200, timeout: cfg.get('requestTimeout', 300) * 1000, localBrainPath: activeBrain.localBrainPath, secondBrainRepo: activeBrain.secondBrainRepo || '', brainProfiles: profiles, activeBrainId: activeBrain.id, maxContextSize: cfg.get('maxContextSize', 12000), maxAutoSteps: cfg.get('maxAutoSteps', 50), dryRun: cfg.get('dryRun', false), multiAgentEnabled: cfg.get('multiAgentEnabled', false), memoryEnabled: cfg.get('memoryEnabled', true), memoryShortTermMessages: Math.max(0, cfg.get('memoryShortTermMessages', 8)), memoryMediumTermSessions: Math.max(0, cfg.get('memoryMediumTermSessions', 5)), memoryLongTermFiles: Math.max(0, cfg.get('memoryLongTermFiles', 6)), contextLength: Math.max(2048, cfg.get('contextLength', 32768)), maxOutputTokens: Math.max(256, cfg.get('maxOutputTokens', 4096)), contextSafetyMargin: Math.max(0, cfg.get('contextSafetyMargin', 2048)), contextOverflowPolicy: ((): IAgentConfig['contextOverflowPolicy'] => { const v = cfg.get('contextOverflowPolicy', 'stopAtLimit'); return v === 'truncateMiddle' || v === 'rollingWindow' ? v : 'stopAtLimit'; })(), autoCompactHistory: cfg.get('autoCompactHistory', true), smallModelContextCap: Math.max(0, cfg.get('smallModelContextCap', 0)), autoContinueOnOutputLimit: cfg.get('autoContinueOnOutputLimit', true), maxAutoContinuations: Math.max(0, Math.min(10, cfg.get('maxAutoContinuations', 4))), finalOnlyRetryOnThoughtLeak: cfg.get('finalOnlyRetryOnThoughtLeak', true) }; } /** * Config Validator: Validates the current configuration. */ export function validateConfig(): { valid: boolean; errors: string[] } { const config = getConfig(); const errors: string[] = []; // 1. Ollama URL Validation try { new URL(config.ollamaUrl); } catch (e) { errors.push(`Invalid Ollama URL: ${config.ollamaUrl}`); } // 2. Brain Path Validation if (config.localBrainPath && !fs.existsSync(config.localBrainPath)) { errors.push(`Brain path does not exist: ${config.localBrainPath}`); } return { valid: errors.length === 0, errors }; } // ─── 보안 정책 (Security Policy) ─── export const SECURITY_POLICY = { allowedCommandPrefixes: [ 'npm', 'yarn', 'pnpm', 'npx', 'node', 'ts-node', 'git', 'python', 'python3', 'pip', 'pip3', 'docker', 'docker-compose', 'ls', 'dir', 'cat', 'type', 'echo', 'print', 'cargo', 'go', 'rustc', 'java', 'javac', 'mvn', 'gradle', 'flutter', 'dart', 'pub', 'webpack', 'vite', 'esbuild', 'parcel', 'jest', 'mocha', 'vitest', 'cypress', 'tsc', 'vue-tsc', ], forbiddenCommands: [ 'rm -rf', 'rm-rf', 'del /f', 'format', 'mkfs', 'dd if=', ':(){ :|:& };:', 'wget http', 'curl http', 'sudo', 'chmod 777', 'chown root', ], sensitiveFilePatterns: [ '.env', '.env.*', 'id_rsa', 'id_ed25519', '.gitconfig', '.npmrc', '.pypirc', 'credentials.json', 'service-account.json', ], maxFileSize: 10 * 1024 * 1024, maxContextFiles: 200, }; export const EXCLUDED_DIRS = new Set([ 'node_modules', '.git', '.vscode', 'out', 'dist', 'build', '.next', '.cache', '__pycache__', '.DS_Store', 'coverage', '.turbo', '.nuxt', '.output', 'vendor', 'target', '.astra' ]);