Files
connectai/src/extension.ts
T
koriweb 7e96e56381 feat(astra): Project Astra 이메일 자산화 Phase 1+2 (v2.2.206)
- Gmail 읽기전용 수집(/email-sync) — gmail.readonly 스코프(공유 토큰),
  본문/메타/스레드를 로컬 인덱스에 저장. 본문 로컬 only(프라이버시).
- RAG 'email' 소스 — 검색 파이프라인 자동 합류 + 원문 메일 링크 출처.
- 하이브리드(TF-IDF+임베딩) 검색, brain 과 동일 공식.
- /email-status — 미회신/놓친 요청 추적(스레드 SENT 라벨 휴리스틱).
- 백그라운드 자동 동기화(g1nation.email.autoSync) — 슬래시와 동일 코어 공유.

신규 features/email/{gmailApi,emailStore,emailSync,autoSync,handlers}.ts
+ retrieval 'email' 소스 통합. 타입체크·407 테스트 통과.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 18:34:42 +09:00

365 lines
17 KiB
TypeScript

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';
import './features/email/handlers'; // Project Astra — /email-sync
import { startEmailAutoSync } from './features/email/autoSync';
// 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 { 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<number>('lmStudio.idleTimeoutMs', 300000),
autoLoadOnSelect: cfg.get<boolean>('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() });
// Project Astra — 이메일 백그라운드 자동 동기화 (g1nation.email.autoSync 켜져 있을 때만).
startEmailAutoSync(context);
// 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<string>('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<never> = {
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 }),
// 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,
});
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<boolean>('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<string | null> {
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;
}