feat: Add custom local brain path support with full 2-way sync

This commit is contained in:
Jay
2026-04-21 12:25:41 +09:00
parent 2b10736f8e
commit c156a14cf6
2 changed files with 31 additions and 14 deletions
+5
View File
@@ -120,6 +120,11 @@
"type": "string", "type": "string",
"default": "", "default": "",
"description": "🧠 Second Brain — 지식 저장소 GitHub URL (예: https://github.com/user/my-knowledge). 여기에 입력한 깃허브의 마크다운(.md) 파일들이 AI의 지식 기반이 됩니다." "description": "🧠 Second Brain — 지식 저장소 GitHub URL (예: https://github.com/user/my-knowledge). 여기에 입력한 깃허브의 마크다운(.md) 파일들이 AI의 지식 기반이 됩니다."
},
"connectAiLab.localBrainPath": {
"type": "string",
"default": "",
"description": "📁 로컬 동기화 폴더 경로 — (선택) 숨김 폴더 대신, 내 PC의 특정 폴더(예: /Users/jay/Desktop/MyBrain)를 지정하면 해당 폴더가 깃허브와 완벽히 양방향 동기화(Auto Pull & Push) 됩니다."
} }
} }
} }
+26 -14
View File
@@ -23,9 +23,21 @@ function getConfig() {
maxTreeFiles: cfg.get<number>('maxContextFiles', 200), maxTreeFiles: cfg.get<number>('maxContextFiles', 200),
timeout: cfg.get<number>('requestTimeout', 300) * 1000, timeout: cfg.get<number>('requestTimeout', 300) * 1000,
secondBrainRepo: cfg.get<string>('secondBrainRepo', ''), secondBrainRepo: cfg.get<string>('secondBrainRepo', ''),
localBrainPath: cfg.get<string>('localBrainPath', '')
}; };
} }
function _getBrainDir(): string {
const { localBrainPath } = getConfig();
if (localBrainPath && localBrainPath.trim() !== '') {
if (localBrainPath.startsWith('~/')) {
return path.join(os.homedir(), localBrainPath.substring(2));
}
return localBrainPath.trim();
}
return path.join(os.homedir(), '.connect-ai-brain');
}
const EXCLUDED_DIRS = new Set([ const EXCLUDED_DIRS = new Set([
'node_modules', '.git', '.vscode', 'out', 'dist', 'build', 'node_modules', '.git', '.vscode', 'out', 'dist', 'build',
'.next', '.cache', '__pycache__', '.DS_Store', 'coverage', '.next', '.cache', '__pycache__', '.DS_Store', 'coverage',
@@ -141,7 +153,7 @@ export function activate(context: vscode.ExtensionContext) {
} }
// Step 2: 두뇌 폴더 자동 생성 // Step 2: 두뇌 폴더 자동 생성
const brainDir = path.join(os.homedir(), '.connect-ai-brain'); const brainDir = _getBrainDir();
if (!fs.existsSync(brainDir)) { if (!fs.existsSync(brainDir)) {
fs.mkdirSync(brainDir, { recursive: true }); fs.mkdirSync(brainDir, { recursive: true });
} }
@@ -177,7 +189,7 @@ export function activate(context: vscode.ExtensionContext) {
} }
if (req.method === 'GET' && req.url === '/ping') { if (req.method === 'GET' && req.url === '/ping') {
const brainDir = path.join(os.homedir(), '.connect-ai-brain'); const brainDir = _getBrainDir();
const brainCount = fs.existsSync(brainDir) ? provider._findBrainFiles(brainDir).length : 0; const brainCount = fs.existsSync(brainDir) ? provider._findBrainFiles(brainDir).length : 0;
res.writeHead(200, { 'Content-Type': 'application/json' }); res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ status: 'ok', msg: 'Connect AI Bridge Ready', config: getConfig(), brain: { fileCount: brainCount, enabled: provider._brainEnabled } })); res.end(JSON.stringify({ status: 'ok', msg: 'Connect AI Bridge Ready', config: getConfig(), brain: { fileCount: brainCount, enabled: provider._brainEnabled } }));
@@ -339,7 +351,7 @@ export function activate(context: vscode.ExtensionContext) {
req.on('end', async () => { req.on('end', async () => {
try { try {
const parsed = JSON.parse(body); const parsed = JSON.parse(body);
const brainDir = path.join(os.homedir(), '.connect-ai-brain'); const brainDir = _getBrainDir();
if (!fs.existsSync(brainDir)) { if (!fs.existsSync(brainDir)) {
fs.mkdirSync(brainDir, { recursive: true }); fs.mkdirSync(brainDir, { recursive: true });
} }
@@ -464,7 +476,7 @@ async function showBrainNetwork(context: vscode.ExtensionContext) {
); );
// Scan real Second Brain files locally instead of current workspace // Scan real Second Brain files locally instead of current workspace
const brainDir = path.join(os.homedir(), '.connect-ai-brain'); const brainDir = _getBrainDir();
const realClusters: Record<string, string[]> = {}; const realClusters: Record<string, string[]> = {};
let filesFound = 0; let filesFound = 0;
@@ -479,7 +491,7 @@ async function showBrainNetwork(context: vscode.ExtensionContext) {
walkDir(fullPath); walkDir(fullPath);
} else if (entry.isFile() && fullPath.endsWith('.md')) { } else if (entry.isFile() && fullPath.endsWith('.md')) {
const folderName = path.basename(dir); const folderName = path.basename(dir);
const groupName = folderName === '.connect-ai-brain' ? 'Brain Root' : folderName; const groupName = folderName === path.basename(_getBrainDir()) ? 'Brain Root' : folderName;
if (!realClusters[groupName]) realClusters[groupName] = []; if (!realClusters[groupName]) realClusters[groupName] = [];
realClusters[groupName].push(entry.name.replace('.md', '')); realClusters[groupName].push(entry.name.replace('.md', ''));
filesFound++; filesFound++;
@@ -887,7 +899,7 @@ class SidebarChatProvider implements vscode.WebviewViewProvider {
private async _handleInjectLocalBrain(files: any[]) { private async _handleInjectLocalBrain(files: any[]) {
if (!this._view) return; if (!this._view) return;
const brainDir = path.join(os.homedir(), '.connect-ai-brain'); const brainDir = _getBrainDir();
if (!fs.existsSync(brainDir)) { if (!fs.existsSync(brainDir)) {
vscode.window.showErrorMessage("Second Brain이 연동되지 않았습니다. 채팅창 ⚙버튼이나 헤더에서 🧠버튼을 누른 후 깃허브 레포지토리를 먼저 연동해주세요."); vscode.window.showErrorMessage("Second Brain이 연동되지 않았습니다. 채팅창 ⚙버튼이나 헤더에서 🧠버튼을 누른 후 깃허브 레포지토리를 먼저 연동해주세요.");
return; return;
@@ -995,7 +1007,7 @@ class SidebarChatProvider implements vscode.WebviewViewProvider {
private async _handleBrainMenu() { private async _handleBrainMenu() {
if (!this._view) { return; } if (!this._view) { return; }
const brainDir = path.join(os.homedir(), '.connect-ai-brain'); const brainDir = _getBrainDir();
const isSynced = fs.existsSync(brainDir); const isSynced = fs.existsSync(brainDir);
const { secondBrainRepo } = getConfig(); const { secondBrainRepo } = getConfig();
const statusLabel = this._brainEnabled ? '🟢 ON' : '🔴 OFF'; const statusLabel = this._brainEnabled ? '🟢 ON' : '🔴 OFF';
@@ -1112,7 +1124,7 @@ class SidebarChatProvider implements vscode.WebviewViewProvider {
} }
this._isSyncingBrain = true; this._isSyncingBrain = true;
const brainDir = path.join(os.homedir(), '.connect-ai-brain'); const brainDir = _getBrainDir();
try { try {
this._view.webview.postMessage({ type: 'response', value: '🧠 **Second Brain 동기화 시작 중... 깃허브에서 지식을 복제합니다.**' }); this._view.webview.postMessage({ type: 'response', value: '🧠 **Second Brain 동기화 시작 중... 깃허브에서 지식을 복제합니다.**' });
@@ -1211,7 +1223,7 @@ class SidebarChatProvider implements vscode.WebviewViewProvider {
// 목차(인덱스)만 생성 — 내용은 AI가 <read_brain>으로 직접 열람 // 목차(인덱스)만 생성 — 내용은 AI가 <read_brain>으로 직접 열람
private _getSecondBrainContext(): string { private _getSecondBrainContext(): string {
const brainDir = path.join(os.homedir(), '.connect-ai-brain'); const brainDir = _getBrainDir();
if (!fs.existsSync(brainDir)) return ''; if (!fs.existsSync(brainDir)) return '';
const files = this._findBrainFiles(brainDir); const files = this._findBrainFiles(brainDir);
@@ -1246,7 +1258,7 @@ class SidebarChatProvider implements vscode.WebviewViewProvider {
// AI가 <read_brain>태그로 요청한 파일의 실제 내용을 읽어서 반환 // AI가 <read_brain>태그로 요청한 파일의 실제 내용을 읽어서 반환
private _readBrainFile(filename: string): string { private _readBrainFile(filename: string): string {
const brainDir = path.join(os.homedir(), '.connect-ai-brain'); const brainDir = _getBrainDir();
if (!fs.existsSync(brainDir)) return '[ERROR] Second Brain이 동기화되지 않았습니다. 🧠 버튼을 먼저 눌러주세요.'; if (!fs.existsSync(brainDir)) return '[ERROR] Second Brain이 동기화되지 않았습니다. 🧠 버튼을 먼저 눌러주세요.';
// 정확한 경로 매칭 시도 // 정확한 경로 매칭 시도
@@ -1858,7 +1870,7 @@ class SidebarChatProvider implements vscode.WebviewViewProvider {
fs.mkdirSync(dir, { recursive: true }); fs.mkdirSync(dir, { recursive: true });
} }
fs.writeFileSync(absPath, content, 'utf-8'); fs.writeFileSync(absPath, content, 'utf-8');
if (absPath.includes('.connect-ai-brain')) brainModified = true; if (absPath.startsWith(_getBrainDir())) brainModified = true;
report.push(`✅ 생성: ${relPath}`); report.push(`✅ 생성: ${relPath}`);
if (!firstCreatedFile) { firstCreatedFile = absPath; } if (!firstCreatedFile) { firstCreatedFile = absPath; }
} catch (err: any) { } catch (err: any) {
@@ -1902,7 +1914,7 @@ class SidebarChatProvider implements vscode.WebviewViewProvider {
if (editCount > 0) { if (editCount > 0) {
fs.writeFileSync(absPath, fileContent, 'utf-8'); fs.writeFileSync(absPath, fileContent, 'utf-8');
if (absPath.includes('.connect-ai-brain')) brainModified = true; if (absPath.startsWith(_getBrainDir())) brainModified = true;
report.push(`✏️ 편집 완료: ${relPath} (${editCount}건 수정)`); report.push(`✏️ 편집 완료: ${relPath} (${editCount}건 수정)`);
// Open edited file // Open edited file
vscode.window.showTextDocument(vscode.Uri.file(absPath), { preview: false }); vscode.window.showTextDocument(vscode.Uri.file(absPath), { preview: false });
@@ -1925,7 +1937,7 @@ class SidebarChatProvider implements vscode.WebviewViewProvider {
} else { } else {
fs.unlinkSync(absPath); fs.unlinkSync(absPath);
} }
if (absPath.includes('.connect-ai-brain')) brainModified = true; if (absPath.startsWith(_getBrainDir())) brainModified = true;
report.push(`🗑️ 삭제: ${relPath}`); report.push(`🗑️ 삭제: ${relPath}`);
} else { } else {
report.push(`⚠️ 삭제 스킵: ${relPath} — 파일이 존재하지 않습니다.`); report.push(`⚠️ 삭제 스킵: ${relPath} — 파일이 존재하지 않습니다.`);
@@ -2061,7 +2073,7 @@ class SidebarChatProvider implements vscode.WebviewViewProvider {
// Auto-Push Second Brain changes to Cloud // Auto-Push Second Brain changes to Cloud
if (brainModified) { if (brainModified) {
try { try {
const brainDir = path.join(os.homedir(), '.connect-ai-brain'); const brainDir = _getBrainDir();
const { execSync } = require('child_process'); const { execSync } = require('child_process');
execSync(`git add .`, { cwd: brainDir }); execSync(`git add .`, { cwd: brainDir });
execSync(`git commit -m "[P-Reinforce] Auto-synced structured knowledge"`, { cwd: brainDir }); execSync(`git commit -m "[P-Reinforce] Auto-synced structured knowledge"`, { cwd: brainDir });