183 lines
7.6 KiB
TypeScript
183 lines
7.6 KiB
TypeScript
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<BrainProfile> | 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<string>('localBrainPath', '');
|
|
const legacyBrainRepo = cfg.get<string>('secondBrainRepo', '');
|
|
const configuredProfiles = cfg.get<Partial<BrainProfile>[]>('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<string>('activeBrainId', profiles[0].id) || profiles[0].id;
|
|
const activeBrain = profiles.find((profile) => profile.id === activeBrainId) || profiles[0];
|
|
|
|
return {
|
|
ollamaUrl: cfg.get<string>('ollamaUrl', 'http://127.0.0.1:11434') || 'http://127.0.0.1:11434',
|
|
defaultModel: cfg.get<string>('defaultModel', 'gemma4:e2b') || 'gemma4:e2b',
|
|
maxTreeFiles: 200,
|
|
timeout: cfg.get<number>('requestTimeout', 300) * 1000,
|
|
localBrainPath: activeBrain.localBrainPath,
|
|
secondBrainRepo: activeBrain.secondBrainRepo || '',
|
|
brainProfiles: profiles,
|
|
activeBrainId: activeBrain.id,
|
|
maxContextSize: cfg.get<number>('maxContextSize', 12000),
|
|
maxAutoSteps: cfg.get<number>('maxAutoSteps', 50),
|
|
dryRun: cfg.get<boolean>('dryRun', false),
|
|
multiAgentEnabled: cfg.get<boolean>('multiAgentEnabled', false),
|
|
memoryEnabled: cfg.get<boolean>('memoryEnabled', true),
|
|
memoryShortTermMessages: Math.max(0, cfg.get<number>('memoryShortTermMessages', 8)),
|
|
memoryMediumTermSessions: Math.max(0, cfg.get<number>('memoryMediumTermSessions', 5)),
|
|
memoryLongTermFiles: Math.max(0, cfg.get<number>('memoryLongTermFiles', 6)),
|
|
contextLength: Math.max(2048, cfg.get<number>('contextLength', 32768)),
|
|
maxOutputTokens: Math.max(256, cfg.get<number>('maxOutputTokens', 4096)),
|
|
contextSafetyMargin: Math.max(0, cfg.get<number>('contextSafetyMargin', 2048)),
|
|
contextOverflowPolicy: ((): IAgentConfig['contextOverflowPolicy'] => {
|
|
const v = cfg.get<string>('contextOverflowPolicy', 'stopAtLimit');
|
|
return v === 'truncateMiddle' || v === 'rollingWindow' ? v : 'stopAtLimit';
|
|
})(),
|
|
autoCompactHistory: cfg.get<boolean>('autoCompactHistory', true),
|
|
smallModelContextCap: Math.max(0, cfg.get<number>('smallModelContextCap', 0)),
|
|
autoContinueOnOutputLimit: cfg.get<boolean>('autoContinueOnOutputLimit', true),
|
|
maxAutoContinuations: Math.max(0, Math.min(10, cfg.get<number>('maxAutoContinuations', 4))),
|
|
finalOnlyRetryOnThoughtLeak: cfg.get<boolean>('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'
|
|
]);
|