import * as vscode from 'vscode'; import * as fs from 'fs'; import * as path from 'path'; // TeamOps + System + Datacollect handler 자기 등록 — side-effect import. slashRouter // 가 이미 로드된 후 등록되도록 entry point 에서. (v2.2.196~201 도메인별 파일 분리) import './features/teamops/handlers'; import './features/system/handlers'; import './features/datacollect/handlers'; // axios removed in favor of native fetch import { _getBrainDir, _isBrainDirExplicitlySet, findBrainFiles, SYSTEM_PROMPT, buildApiUrl, logError, logInfo, resolveEngine, openInEditorGroup } from './utils'; import { getConfig, validateConfig } from './config'; import { AgentExecutor } from './agent'; import { BridgeServer } from './bridge'; import { SidebarChatProvider } from './sidebarProvider'; import { HealthCheckMonitor } from './core/health'; import { initAstraPathResolver } from './core/astraPath'; import { LMStudioClient } from './lmstudio/client'; import { ActivityTracker } from './lmstudio/activityTracker'; import { ModelLifecycleManager } from './lmstudio/lifecycleManager'; import { LMStudioStreamer } from './lmstudio/streamer'; import { NodeSystemSpecsProvider, HeuristicModelMemoryEstimator } from './system/specs'; import { ApprovalQueue } from './features/approval/approvalQueue'; import { ApprovalPanelProvider } from './features/approval/approvalPanelProvider'; import { ApprovalStatusBar } from './features/approval/approvalStatusBar'; import { TelegramHttpClient } from './integrations/telegram/telegramClient'; import { TelegramBot } from './integrations/telegram/telegramBot'; import { getBrainTokenIndex, clearBrainTokenIndex } from './retrieval'; import { lessonTemplate, lessonSlug, parseLessonFrontmatter, normalizeLessonTitle, bumpLessonOccurrences } from './retrieval/lessonHelpers'; import { runConnectGoogleCalendarIcal, runConnectGoogleCalendarOAuth } from './extension/calendarSetup'; import { runInitialSetup } from './extension/initialSetup'; import { startStocksWatcher } from './features/stocks'; import { registerProviderCommands } from './extension/providerCommands'; import { registerScaffoldCommand } from './extension/scaffoldCommand'; import { registerLessonCommands } from './extension/lessonCommands'; import { registerEvalCommands } from './extension/evalCommands'; import { registerTelegramCommands, TELEGRAM_TOKEN_SECRET_KEY, type TelegramTokenStore } from './extension/telegramCommands'; import { setupSettingsPanel } from './extension/settingsSetup'; import { createTelegramBot } from './integrations/telegram/telegramSetup'; let _lifecycleManager: ModelLifecycleManager | undefined; let _telegramBot: TelegramBot | undefined; /** * Astra Extension Entry Point */ export async function activate(context: vscode.ExtensionContext) { // Activation 시점 popup + DevTools console — 사용자 환경(Antigravity 등 VS Code 변종) // 에서 우리 vsix가 실제로 활성화됐는지 결정적으로 가시화. console.error는 사용자가 // F12 DevTools console에서 다른 모든 출력과 함께 그대로 보인다 (logInfo의 OutputChannel // 과 별개 채널 — popup도 OutputChannel도 못 보는 경우의 마지막 안전망). const ext = vscode.extensions.getExtension('g1nation.astra'); const version = ext?.packageJSON?.version || '(unknown)'; console.log(`[ASTRA-DEBUG] activate v${version} pid=${process.pid}`); void vscode.window.showInformationMessage(`📡 Astra v${version} activated (PID=${process.pid})`); logInfo(`Astra activating... version=${version} pid=${process.pid}`); // Initialize Astra Path Resolver (.astra → ConnectAI/.astra/) initAstraPathResolver(context); // Start Environment Health Monitoring HealthCheckMonitor.runAllChecks(); HealthCheckMonitor.startInterval(600000); // Check every 10 mins // 0. Validate Configuration const validation = validateConfig(); if (!validation.valid) { vscode.window.showErrorMessage(`Astra Configuration Error: ${validation.errors.join(' ')}`); logError('Configuration validation failed.', { errors: validation.errors }); } // 1. Ensure Brain Directory await _ensureBrainDir(context); // 2. Initialize LM Studio Lifecycle Subsystem let provider: SidebarChatProvider | undefined; const initialUrl = getConfig().ollamaUrl; const activityTracker = new ActivityTracker(); const lmStudioClient = new LMStudioClient(initialUrl); const systemSpecs = new NodeSystemSpecsProvider(); const memoryEstimator = new HeuristicModelMemoryEstimator(); logInfo('System specs detected.', { summary: systemSpecs.get().summary }); const lifecycle = new ModelLifecycleManager({ client: lmStudioClient, activity: activityTracker, getConfig: () => { // Read from getConfig() so we share the same setting parsers (incl. gpuOffloadRatio coercion) // with the rest of the codebase instead of duplicating the logic here. const ag = getConfig(); const cfg = vscode.workspace.getConfiguration('g1nation'); return { idleTimeoutMs: cfg.get('lmStudio.idleTimeoutMs', 300000), autoLoadOnSelect: cfg.get('lmStudio.autoLoadOnSelect', true), loadConfig: { flashAttention: ag.lmStudioLoad.flashAttention, gpuOffloadRatio: ag.lmStudioLoad.gpuOffloadRatio, offloadKVCacheToGpu: ag.lmStudioLoad.offloadKVCacheToGpu, keepModelInMemory: ag.lmStudioLoad.keepModelInMemory, useFp16ForKVCache: ag.lmStudioLoad.useFp16ForKVCache, evalBatchSize: ag.lmStudioLoad.evalBatchSize, }, draftModel: ag.lmStudioDraftModel || undefined, }; }, notifyError: (msg) => provider?.postLmStudioError(msg), initialEngine: resolveEngine(initialUrl), systemSpecs, memoryEstimator, }); _lifecycleManager = lifecycle; context.subscriptions.push({ dispose: () => activityTracker.dispose() }); context.subscriptions.push({ dispose: () => lifecycle.dispose() }); // React to engine URL changes — re-target the SDK and reset state. context.subscriptions.push( vscode.workspace.onDidChangeConfiguration((e) => { if (!e.affectsConfiguration('g1nation.ollamaUrl')) return; const newUrl = vscode.workspace.getConfiguration('g1nation').get('ollamaUrl', ''); lmStudioClient.setBaseUrl(newUrl); lifecycle.setEngine(resolveEngine(newUrl)); }) ); // Keep the sidebar's model dropdown in sync when defaultModel / ollamaUrl is // changed from elsewhere (Settings panel, raw settings.json, …). Without // this the user sees a desync: Settings shows the new model, sidebar still // shows the old one until a manual refresh. context.subscriptions.push( vscode.workspace.onDidChangeConfiguration((e) => { const touchedModel = e.affectsConfiguration('g1nation.defaultModel'); const touchedUrl = e.affectsConfiguration('g1nation.ollamaUrl'); if (!touchedModel && !touchedUrl) return; // _sendModels is best-effort; the provider may not have a webview // attached yet during very early activation. void provider?._sendModels(touchedUrl); }) ); // 3. Initialize Approval subsystem (queue + panel webview + status bar badge) // Astra 2.81: sidebar view container is gone; all webviews open in editor // column 3 instead. We don't register a WebviewViewProvider — panels are // created on-demand via openAsPanel(). const approvalQueue = new ApprovalQueue(); const approvalPanel = new ApprovalPanelProvider(context.extensionUri, approvalQueue); const approvalStatusBar = new ApprovalStatusBar(approvalQueue); context.subscriptions.push( approvalStatusBar, { dispose: () => approvalQueue.dispose() }, vscode.commands.registerCommand(ApprovalStatusBar.focusCommand, () => approvalPanel.focus()), ); // 4. Initialize Agent Executor (with stream lifecycle hooks + LM Studio SDK streamer + approval queue) const lmStudioStreamer = new LMStudioStreamer(lmStudioClient); const agent = new AgentExecutor(context, { onStreamLifecycle: { start: () => lifecycle.onStreamStart(), end: () => lifecycle.onStreamEnd(), }, lmStudioStreamer, approvalQueue, }); // 4. Initialize Chat Provider (renders into an editor column, not a sidebar view) provider = new SidebarChatProvider(context.extensionUri, context, agent, { lifecycle, activity: activityTracker, loadedModels: () => lmStudioClient.listLoadedCached(), downloadedModels: () => lmStudioClient.listDownloadedCached(), }); // One-time repair: rewrite any chronicle projects that were saved with the // workspace parent as their `projectRoot` (a side-effect of the old // pre-multi-subproject activation code). Idempotent and silent when there's // nothing to fix. void provider._repairCorruptedChronicleProjectRoots().catch((e: any) => { logError('architecture: chronicle repair failed.', { error: e?.message ?? String(e) }); }); context.subscriptions.push( vscode.commands.registerCommand('g1nation.openChat', () => { provider!.openAsPanel(vscode.ViewColumn.Three); }) ); // Datacollect Python 의존성 자동 점검·설치 — `Astra: Setup Datacollect Dependencies`. // 슬래시 명령(/youtube /research /wikify 등)이 yt-dlp / youtube-transcript-api 같은 // Python 패키지를 필요로 하는데, Astra 확장만 깔면 그게 자동으로 따라오지 않아 // 사용자가 매번 수동 pip install 해야 했음. 이 명령으로 한 번에 처리. context.subscriptions.push( vscode.commands.registerCommand('g1nation.setupDatacollect', async () => { const { runDatacollectSetup } = await import('./features/setup/datacollectSetup'); await runDatacollectSetup(); }) ); // ── Activity Bar launcher view ──────────────────────────────────────── // Adds a sparkle (✦) icon to VS Code's left activity bar. Clicking it // opens a small sidebar with action buttons (Open Chat / New Chat / // Settings / Company / Architecture). Solves the user-reported pain // point: when the Astra Chat editor tab is accidentally closed, there // was no one-click way to reopen it short of restarting the extension. // // The view itself has no items — VS Code renders the `viewsWelcome` // content from package.json instead, which is just a list of command // links. Cheap, theme-aware, no webview to maintain. const astraLauncherProvider: vscode.TreeDataProvider = { getTreeItem: () => new vscode.TreeItem(''), getChildren: () => [], }; context.subscriptions.push( vscode.window.registerTreeDataProvider('g1nation-astra-launcher', astraLauncherProvider), ); // 4. Initialize Bridge Server (Port 4825) const bridge = new BridgeServer(provider); try { bridge.start(); logInfo('Bridge server started on port 4825.'); } catch (err) { logError('Failed to start bridge server.', err); } // 5. Register Core Commands context.subscriptions.push( vscode.commands.registerCommand('g1nation.focusInput', () => { provider.focusInput(); }) ); context.subscriptions.push( vscode.commands.registerCommand('g1nation.clearChat', () => { provider.clearChat(); }) ); context.subscriptions.push( vscode.commands.registerCommand('g1nation.syncBrain', async () => { await provider!.syncBrain(); }) ); // Telegram Bot integration — opt-in (g1nation.telegram.enabled), token in SecretStorage. // `tokenStore` 는 telegramCommands 모듈과 공유 — 모듈이 secrets change 시 // `tokenStore.current` 를 갱신하면 client 가 다음 호출에서 새 값을 본다. const tokenStore: TelegramTokenStore = { current: (await context.secrets.get(TELEGRAM_TOKEN_SECRET_KEY)) || '', }; const telegramClient = new TelegramHttpClient({ getToken: () => tokenStore.current, }); // buildTelegramSystemPrompt / looksLikeWorkOrder / buildTelegramCompanyContext / // latestCompanySessionDir / chunkTelegramMessage → `src/integrations/telegram/promptBuilders.ts` // handler 콜백 + AIService → `src/integrations/telegram/telegramSetup.ts` const telegramBot = createTelegramBot(context, { telegramClient, getProvider: () => provider }); _telegramBot = telegramBot; context.subscriptions.push( // refresh + dispose + listeners + setBotToken/clearBotToken/testConnection → `src/extension/telegramCommands.ts` ...registerTelegramCommands(context, { telegramBot, telegramClient, tokenStore }), // knowledge map + lesson cards → `src/extension/lessonCommands.ts` ...registerLessonCommands({ getAgent: () => agent }), // 검색 평가 하니스 (recall@k / MRR) → `src/extension/evalCommands.ts` ...registerEvalCommands(), // architecture / company / calendar / devil commands → `src/extension/providerCommands.ts` ...registerProviderCommands(context, { getProvider: () => provider }), ); // listLessonFiles / createLessonCard / manageLessons → `src/extension/lessons.ts` // Astra Settings webview — single entry point for user-facing config (Phase 5-A: Telegram only). // panel 인스턴스 + listener/command disposables → `src/extension/settingsSetup.ts` const { settingsPanel, disposables: settingsDisposables } = setupSettingsPanel(context, { telegramClient, telegramBot, // 모델 dropdown 이 보유 모델 전부를 보이도록 SDK 다운로드 목록을 전달. lmStudioDownloaded: () => lmStudioClient.listDownloadedCached(), }); context.subscriptions.push(...settingsDisposables); // g1nation.scaffoldProject → `src/extension/scaffoldCommand.ts` context.subscriptions.push(registerScaffoldCommand()); // 6. Run Initial Setup (Automatic Model/Engine Detection) const setupComplete = context.globalState.get('setupComplete', false); if (!setupComplete) { await runInitialSetup(context); } // Stocks watcher — VS Code 시작 시 자동 활성화. KST 09:00 / 15:00 자동 트리거. // disposable 은 subscriptions 에 푸시해 종료 시 timer cleanup. context.subscriptions.push(startStocksWatcher(context)); // 7. Auto-open all three Astra webviews as tabs in editor column 3. // The sidebar/activity-bar entry point was removed in 2.81 — all three views // (Chat, Approvals, Settings) now stack as tabs in the third editor column. // Order matters: Chat opens last so it ends up as the active tab. try { approvalPanel.openAsPanel(vscode.ViewColumn.Three); await settingsPanel.openAsPanel(vscode.ViewColumn.Three); provider!.openAsPanel(vscode.ViewColumn.Three); } catch (e) { logError('Failed to auto-open Astra panels.', e); } } export async function deactivate() { HealthCheckMonitor.dispose(); // Release the in-memory brain token index (and any pending debounced disk // write timer) — the `_states` Map is otherwise never cleared for the // process lifetime. clearBrainTokenIndex(); if (_telegramBot) { try { await _telegramBot.stop(); } catch (e) { logError('Telegram bot stop during deactivate failed.', e); } _telegramBot = undefined; } if (_lifecycleManager) { try { await _lifecycleManager.disposeAndUnload(2000); } catch (e) { logError('Lifecycle dispose during deactivate failed.', e); } _lifecycleManager = undefined; } } // runConnectGoogleCalendarIcal / runConnectGoogleCalendarOAuth → `src/extension/calendarSetup.ts` // runInitialSetup → `src/extension/initialSetup.ts` async function _ensureBrainDir(context: vscode.ExtensionContext): Promise { if (_isBrainDirExplicitlySet()) { const dir = _getBrainDir(); if (!fs.existsSync(dir)) { try { fs.mkdirSync(dir, { recursive: true }); } catch (e) { logError('Failed to create brain directory.', e); } } return dir; } const defaultDir = _getBrainDir(); if (!fs.existsSync(defaultDir)) { try { fs.mkdirSync(defaultDir, { recursive: true }); // Create a welcome file fs.writeFileSync(path.join(defaultDir, 'Welcome.md'), "# Welcome to your Second Brain\n\nAstra will store and retrieve knowledge from here."); } catch (e) { logError('Failed to initialize default brain directory.', e); } } return defaultDir; }