0a97324f1b
R56–R59: agent.ts 2731→1529줄 god-file 분해 (25 modules) · attrParsers + LLM 메서드 8개 (callNonStreaming, streamChatOnce 등) · executeActions 415줄 → 8 handler 그룹 (file/run/list/brain/calendar/sheets/tasks) · handlePrompt 1100줄 → 7 phase 모듈 (system prompt + budget + autoContinue 등) R50–R55: extension.ts 1145→349줄 (telegram/settings/provider commands 분리) Stocks feature 신규: /stocks slash command (v2.2.152~158) · .astra/stocks.json 저장소 + Yahoo Finance 현재가 갱신 · 8 키워드 필터 (ROE/성장성/유동성/수익성/영업효율/기술력/안정성/PBR) · Naver 시가총액 페이지 JSON API (m.stock.naver.com) 발굴 · LLM Top 5 매력도 분석 + Telegram 자동 보고서 · KST 09:00/15:00 watcher 자동 모니터링 대화 연속성 (v2.2.150~157): · [PRIOR TURN CONCLUSION] block 으로 직전 결론 anchor · thin follow-up 분류 → boilerplate 헤더 suppression · slash 명령 결과 chatHistory mirror (capture wrapper) · echo/parrot 금지 system prompt rule 기타: /stocks 슬래시 자동완성 dropdown UI, Naver JSON API 전환 (cheerio 제거) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
419 lines
22 KiB
TypeScript
419 lines
22 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;
|
||
/** 채팅 응답 생성 temperature. 낮을수록 한국어 오타·깨진 토큰이 줄어든다. */
|
||
chatTemperature: 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;
|
||
// ─── Hybrid Semantic Search ───
|
||
/**
|
||
* Embedding model name as registered in LM Studio / Ollama. Empty disables
|
||
* semantic search and the retriever falls back to TF-IDF only. The user
|
||
* must load this model in the engine before enabling it here.
|
||
*/
|
||
embeddingModel: string;
|
||
/**
|
||
* Blend between TF-IDF (sparse) and embedding cosine (dense) scoring.
|
||
* 0 = TF-IDF only (status quo), 1 = embedding only.
|
||
* Default 0.5 = equal weight, a reasonable starting point.
|
||
*/
|
||
embeddingBlendAlpha: number;
|
||
/**
|
||
* Global Knowledge Mix weight (0–100). Controls how much the assistant leans on
|
||
* Second Brain evidence vs. model general knowledge when answering.
|
||
* 0 → Second Brain disabled; model knowledge only.
|
||
* 50 → Balanced (default).
|
||
* 100 → Second Brain is primary evidence; model knowledge only fills gaps.
|
||
* Per-agent overrides live in AgentKnowledgeEntry.secondBrainWeight and win.
|
||
*/
|
||
knowledgeMixSecondBrainWeight: number;
|
||
/**
|
||
* [Self-Reflection] Researcher와 Writer 사이에 메타인지 단계(Reflector)를 삽입할지 여부.
|
||
* true(기본): Reflector가 plan/research를 비판적으로 검토한 critique을 Writer에 주입.
|
||
* false: 기존 3단계(Planner→Researcher→Writer) 그대로 — 1 LLM 호출 절약 (저성능 모델/저지연 우선 시).
|
||
*/
|
||
/**
|
||
* Model id used by the 1인 기업 mode intent classifier (route message to
|
||
* pipeline vs casual chat). Empty → falls back to `defaultModel`. Recommended
|
||
* a fast small model (gemma e2b 등) so classification adds <1 s per send.
|
||
*/
|
||
companyIntentClassifierModel: string;
|
||
/**
|
||
* Bypass the intent classifier and always run the full pipeline. Legacy
|
||
* behaviour. Off by default because chat / question / thanks shouldn't
|
||
* dispatch all agents.
|
||
*/
|
||
companyDisableIntentClassifier: boolean;
|
||
/**
|
||
* 분류기가 추천한 파이프라인으로 *이번 turn만* 자동 전환할지. 켜면
|
||
* 사용자가 명시적으로 활성화해 둔 파이프라인보다 분류기 추천이 우선.
|
||
* 끄면 분류기 추천은 채팅 라벨에만 표시되고 dispatch는 사용자 활성
|
||
* 파이프라인 그대로. 처음 써 보는 사용자는 끈 채 추천만 보고 점차
|
||
* 신뢰 생기면 켜는 흐름을 권장 — 기본 false.
|
||
*/
|
||
companyAutoSelectPipeline: boolean;
|
||
/**
|
||
* Intent Alignment 모드. new_task 발생 시 사용자 의도를 C-G-C-F-Q로
|
||
* 정리하는 단계를 어떻게 다룰지.
|
||
* - 'off' : alignment 비활성. 분류기가 new_task 라고 하면 곧장 pipeline.
|
||
* - 'smart' : 기본값. confidence high면 자동 진행, medium/low면 사용자 확인.
|
||
* - 'strict' : confidence 무관 항상 사용자에게 contract 확인 카드 띄움.
|
||
*/
|
||
companyIntentAlignmentMode: 'off' | 'smart' | 'strict';
|
||
/** alignment 라운드 최대 횟수 (질문→답변 사이클). 1~5. */
|
||
companyIntentAlignmentMaxRounds: number;
|
||
/**
|
||
* Pixel Office 시각화 패널을 사이드바에 표시할지 여부. UI layer 전용 —
|
||
* 끈다고 Agent 행동이 바뀌지 않는다. Off면 webview는 패널을 숨기고
|
||
* 백엔드도 broadcast 자체를 skip해서 자원 절약.
|
||
*/
|
||
/**
|
||
* Self-Reflector Phase A — 모든 LLM 응답 끝에 [Self-Reflector Check]
|
||
* 자기검증 블록을 자동으로 붙이게 한다. 추가 LLM 콜 없음. 본질적으로
|
||
* 응답 품질 안전망이라 끌 이유는 적지만, 잡담 위주 환경에서 노이즈로
|
||
* 느껴진다면 꺼둘 수 있다.
|
||
*/
|
||
selfReflectorEnabled: boolean;
|
||
/**
|
||
* Self-Reflector Phase B — 회사 모드 specialist 응답 직후 *분리된 콘텍스트*
|
||
* 에서 LLM 한 번 더 호출해 외부 시각으로 검증. 실패 시 1회 retry. 비용
|
||
* 추가되므로 기본 OFF.
|
||
*/
|
||
selfReflectorExternalEnabled: boolean;
|
||
/**
|
||
* Self-Reflector Phase C — 코드 산출물에 한해 syntax/lint를 실제로 돌려
|
||
* 실행 기반 검증. Python: py_compile, JS: node --check, TS: tsc --noEmit.
|
||
* 실패 시 에러를 응답에 첨부. 기본 OFF — 사용자 환경에 toolchain이
|
||
* 깔려 있어야 의미가 있다.
|
||
*/
|
||
selfReflectorExecutionEnabled: boolean;
|
||
companyPixelOfficeEnabled: boolean;
|
||
/**
|
||
* Pixel Office의 캐릭터 말풍선 연출을 켤지. enabled가 true이고 이 값도
|
||
* true일 때만 말풍선이 생성된다. 시끄럽게 느껴지면 사용자가 끌 수 있게.
|
||
*/
|
||
companyPixelOfficeBubbles: boolean;
|
||
/**
|
||
* Multi-Agent 발동 모드:
|
||
* - 'auto' (기본): 작은 모델(≤4B) 감지 OR prompt가 컨텍스트의 큰 비중을 차지할 때만 자동 발동.
|
||
* - 'always': 인사·짧은 잡담을 제외한 모든 요청에 5단계 파이프라인 사용.
|
||
* - 'off': 기존 single-agent 동작 (수동 토글 / 키워드 매칭만 사용).
|
||
*/
|
||
workflowMultiAgentMode: 'auto' | 'always' | 'off';
|
||
/**
|
||
* 'auto' 모드에서 prompt + brain context 토큰이 contextLength 의 이 비율(0~1)을 넘으면 강제 5단계.
|
||
* 기본 0.30 — 작은 모델이 30% 이상을 input으로 먹기 시작하면 한 번에 끝내려는 시도가 위험.
|
||
*/
|
||
workflowAutoCtxFractionThreshold: number;
|
||
/**
|
||
* 절대 토큰 임계값 — 입력 prompt 가 이 값 *미만* 이면 Multi-Agent 파이프라인 발동
|
||
* 안 함 (키워드·길이 트리거 무시). 모델이 단일 호출로 처리.
|
||
*
|
||
* 의도: 사용자가 "요약/리뷰" 같은 키워드만 써도 chunked 가 강제로 발동해
|
||
* LLM 여러 번 호출되며 답변이 느려지는 문제 해결. 입력이 모델 윈도우 대비
|
||
* 충분히 작으면 한 번에 답하는 게 합리적.
|
||
*
|
||
* 기본 50000 — 대부분의 사용 환경에 적합. 매우 작은 컨텍스트 모델로 큰 입력을
|
||
* 자주 다룬다면 OOM 방지 차원에서 사용자가 직접 낮출 수 있음 (Astra Settings 패널).
|
||
*/
|
||
chunkedSwitchTokens: number;
|
||
/**
|
||
* Chunked 파이프라인 진입 시 outline 이 만들 수 있는 *최대 섹션 수*.
|
||
* 실제 LLM 호출 횟수 = 1(outline) + N(section) + 1(polish) = 2 + N.
|
||
* 따라서 이 값이 3이면 최대 5회, 4이면 최대 6회.
|
||
*
|
||
* 작을수록 답변 속도 빠름, 클수록 답변이 더 세분화. 기본 3 — 사용자
|
||
* 피드백("6회 이상은 과하다") 반영. 1~10 범위 clamp.
|
||
*/
|
||
chunkedMaxSections: number;
|
||
/**
|
||
* ChunkedWriter 의 polish persona 를 사용자가 override 할 텍스트. 비어 있으면
|
||
* 기본 polish persona (DEFAULT_POLISH_PERSONA) 사용. 입력이 있으면 그 텍스트가
|
||
* 그대로 system prompt 로 들어가 polish 단계의 톤·구조 룰을 정의.
|
||
*
|
||
* 의도: 사용자가 답변 톤을 (예: 격식체·반말·법률 문서·마케팅 카피) 도메인에
|
||
* 맞게 직접 조정 가능. 코드 변경 없이 Settings 패널 textarea 만으로.
|
||
*
|
||
* 빈 문자열 / 공백만이면 default 사용 — 잘못 입력해도 답변이 깨지진 않음.
|
||
*/
|
||
polishPersonaOverride: string;
|
||
// ─── Stream 표시 ───
|
||
/**
|
||
* 모델 토큰을 받는 즉시 채팅 버블에 흘려보낼지 여부.
|
||
* - false(기본): 토큰은 내부에서만 누적, sanitize 끝난 최종 답변만 한 번에 표시 → Harmony/think 마커 누설 원천 차단.
|
||
* - true: legacy 라이브 스트리밍. 모델 출력에 control token 이 섞여 나오면 잠깐 화면에 보일 수 있음.
|
||
*/
|
||
liveStreamTokens: boolean;
|
||
/**
|
||
* 최종 답변 포맷.
|
||
* - 'plain' (기본): 모델이 무심코 내보낸 `##`, `**`, `__`, `> `, `* ` 등의 마크다운 마커를 후처리로 모두 제거.
|
||
* 섹션 라벨 텍스트(예: "핵심 요약")는 유지되지만 헤더 마커는 사라져 깔끔한 plain text 로 표시.
|
||
* - 'markdown': legacy 동작. 모델 출력을 그대로 렌더러에 넘김.
|
||
*/
|
||
outputFormat: 'plain' | 'markdown';
|
||
/**
|
||
* 자동 기록 (project chronicle auto-record). true 면 매 prompt 후 의미 있는 turn 을
|
||
* Wiki/Chronicle 폴더에 자동으로 저장. false 면 자동 저장 OFF (수동 기록은 계속 가능).
|
||
* 사이드바 도구 드롭다운의 토글 항목으로 즉시 변경 가능.
|
||
*/
|
||
chronicleAutoRecord: boolean;
|
||
// ─── LM Studio sampling (applied to both SDK and REST paths) ───
|
||
/** LM Studio nucleus sampling cutoff (0~1). Lower tightens; 1 disables. */
|
||
lmStudioTopP: number;
|
||
/** LM Studio top-K cutoff (0 disables). */
|
||
lmStudioTopK: number;
|
||
/** LM Studio min-P floor (0~1, 0 disables). */
|
||
lmStudioMinP: number;
|
||
/** LM Studio repeat penalty (1 disables, 1.05–1.2 typical). */
|
||
lmStudioRepeatPenalty: number;
|
||
/** Render tok/s + TTFT from prediction stats into context-budget badge. */
|
||
lmStudioShowStatsInBudget: boolean;
|
||
/** LM Studio model key of a small draft model for speculative decoding ('' = disabled). */
|
||
lmStudioDraftModel: string;
|
||
/** Load-time options. Read once per load(); changing these after load needs a reload. */
|
||
lmStudioLoad: {
|
||
flashAttention: boolean;
|
||
/** "max" | "off" | number 0-1 */
|
||
gpuOffloadRatio: 'max' | 'off' | number;
|
||
offloadKVCacheToGpu: boolean;
|
||
keepModelInMemory: boolean;
|
||
useFp16ForKVCache: boolean;
|
||
/** 0 = engine default */
|
||
evalBatchSize: number;
|
||
};
|
||
}
|
||
|
||
// ─── 경로 정규화 유틸리티 ───
|
||
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),
|
||
chatTemperature: Math.min(2, Math.max(0, cfg.get<number>('chatTemperature', 0.3))),
|
||
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),
|
||
embeddingModel: (cfg.get<string>('embeddingModel', '') || '').trim(),
|
||
embeddingBlendAlpha: Math.max(0, Math.min(1, cfg.get<number>('embeddingBlendAlpha', 0.5))),
|
||
knowledgeMixSecondBrainWeight: Math.max(0, Math.min(100, Math.round(
|
||
cfg.get<number>('knowledgeMix.secondBrainWeight', 50)
|
||
))),
|
||
companyIntentClassifierModel: (cfg.get<string>('company.intentClassifierModel', '') || '').trim(),
|
||
companyDisableIntentClassifier: cfg.get<boolean>('company.disableIntentClassifier', false),
|
||
companyAutoSelectPipeline: cfg.get<boolean>('company.autoSelectPipeline', true),
|
||
companyIntentAlignmentMode: ((): 'off' | 'smart' | 'strict' => {
|
||
const v = (cfg.get<string>('company.intentAlignmentMode', 'smart') || 'smart').trim().toLowerCase();
|
||
return v === 'off' || v === 'strict' ? v : 'smart';
|
||
})(),
|
||
// ALIGNMENT_DEFAULT_MAX_ROUNDS 와 일치 — `intentAlignment` 모듈을 직접 import 하지 않은
|
||
// 이유는 config 가 features/ 아래 모듈을 의존하면 의도치 않은 순환 import 가 생기기 때문.
|
||
// 둘이 어긋나면 안 되므로 변경 시 양쪽 같이 갱신.
|
||
companyIntentAlignmentMaxRounds: Math.max(1, Math.min(5, cfg.get<number>('company.intentAlignmentMaxRounds', 3))),
|
||
selfReflectorEnabled: cfg.get<boolean>('selfReflector.enabled', false),
|
||
selfReflectorExternalEnabled: cfg.get<boolean>('selfReflector.externalVerification', false),
|
||
selfReflectorExecutionEnabled: cfg.get<boolean>('selfReflector.executionVerification', false),
|
||
companyPixelOfficeEnabled: cfg.get<boolean>('company.pixelOffice.enabled', true),
|
||
companyPixelOfficeBubbles: cfg.get<boolean>('company.pixelOffice.bubbles', true),
|
||
workflowMultiAgentMode: ((): 'auto' | 'always' | 'off' => {
|
||
const v = (cfg.get<string>('workflow.multiAgentMode', 'auto') || 'auto').trim().toLowerCase();
|
||
return v === 'always' || v === 'off' ? v : 'auto';
|
||
})(),
|
||
workflowAutoCtxFractionThreshold: Math.max(0.05, Math.min(0.95,
|
||
cfg.get<number>('workflow.autoCtxFractionThreshold', 0.30)
|
||
)),
|
||
chunkedSwitchTokens: Math.max(1000, cfg.get<number>('chunkedSwitchTokens', 50000)),
|
||
chunkedMaxSections: Math.max(1, Math.min(10, cfg.get<number>('chunkedMaxSections', 3))),
|
||
polishPersonaOverride: (cfg.get<string>('polishPersonaOverride', '') || '').trim(),
|
||
liveStreamTokens: cfg.get<boolean>('liveStreamTokens', true),
|
||
outputFormat: ((): 'plain' | 'markdown' => {
|
||
const v = (cfg.get<string>('outputFormat', 'plain') || 'plain').trim().toLowerCase();
|
||
return v === 'markdown' ? 'markdown' : 'plain';
|
||
})(),
|
||
chronicleAutoRecord: cfg.get<boolean>('chronicleAutoRecord', true),
|
||
lmStudioTopP: Math.max(0, Math.min(1, cfg.get<number>('lmStudio.sampling.topP', 0.9))),
|
||
lmStudioTopK: Math.max(0, cfg.get<number>('lmStudio.sampling.topK', 20)),
|
||
lmStudioMinP: Math.max(0, Math.min(1, cfg.get<number>('lmStudio.sampling.minP', 0.05))),
|
||
lmStudioRepeatPenalty: Math.max(1, Math.min(2, cfg.get<number>('lmStudio.sampling.repeatPenalty', 1.1))),
|
||
lmStudioShowStatsInBudget: cfg.get<boolean>('lmStudio.statsInBudget', true),
|
||
lmStudioDraftModel: (cfg.get<string>('lmStudio.draftModel', '') || '').trim(),
|
||
lmStudioLoad: {
|
||
flashAttention: cfg.get<boolean>('lmStudio.load.flashAttention', true),
|
||
gpuOffloadRatio: ((): 'max' | 'off' | number => {
|
||
const raw = (cfg.get<string>('lmStudio.load.gpuOffloadRatio', 'max') || 'max').trim().toLowerCase();
|
||
if (raw === 'max' || raw === 'off') return raw;
|
||
const n = Number(raw);
|
||
if (Number.isFinite(n)) return Math.max(0, Math.min(1, n));
|
||
return 'max';
|
||
})(),
|
||
offloadKVCacheToGpu: cfg.get<boolean>('lmStudio.load.offloadKVCacheToGpu', true),
|
||
keepModelInMemory: cfg.get<boolean>('lmStudio.load.keepModelInMemory', true),
|
||
useFp16ForKVCache: cfg.get<boolean>('lmStudio.load.useFp16ForKVCache', false),
|
||
evalBatchSize: Math.max(0, cfg.get<number>('lmStudio.load.evalBatchSize', 0)),
|
||
},
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 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'
|
||
]);
|