diff --git a/package-lock.json b/package-lock.json index 06733e7..3f57a8b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,15 +1,14 @@ { - "name": "connect-ai-lab", + "name": "g1nation", "version": "2.2.15", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "connect-ai-lab", + "name": "g1nation", "version": "2.2.15", "license": "MIT", "dependencies": { - "axios": "^1.15.0", "jsdom": "^29.0.2" }, "devDependencies": { @@ -155,7 +154,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=20.19.0" }, @@ -202,7 +200,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=20.19.0" } @@ -693,23 +690,6 @@ "ncc": "dist/ncc/cli.js" } }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "license": "MIT" - }, - "node_modules/axios": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.15.0.tgz", - "integrity": "sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q==", - "license": "MIT", - "dependencies": { - "follow-redirects": "^1.15.11", - "form-data": "^4.0.5", - "proxy-from-env": "^2.1.0" - } - }, "node_modules/bidi-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", @@ -719,31 +699,6 @@ "require-from-string": "^2.0.2" } }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "license": "MIT", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/css-tree": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", @@ -776,29 +731,6 @@ "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", "license": "MIT" }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/entities": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", @@ -811,51 +743,6 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-set-tostringtag": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", - "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/esbuild": { "version": "0.28.0", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.0.tgz", @@ -898,139 +785,6 @@ "@esbuild/win32-x64": "0.28.0" } }, - "node_modules/follow-redirects": { - "version": "1.15.11", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", - "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "license": "MIT", - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, - "node_modules/form-data": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", - "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "license": "MIT", - "dependencies": { - "has-symbols": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/html-encoding-sniffer": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", @@ -1098,42 +852,12 @@ "node": "20 || >=22" } }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, "node_modules/mdn-data": { "version": "2.27.1", "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", "license": "CC0-1.0" }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/parse5": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.0.tgz", @@ -1146,15 +870,6 @@ "url": "https://github.com/inikulin/parse5?sponsor=1" } }, - "node_modules/proxy-from-env": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", - "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", - "license": "MIT", - "engines": { - "node": ">=10" - } - }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", diff --git a/package.json b/package.json index 23d5f10..d507565 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "g1nation", "displayName": "G1nation", "description": "100% local AI coding agent for VS Code. Create files, edit code, run commands, and work offline with Ollama or LM Studio.", - "version": "2.2.15", + "version": "2.2.27", "publisher": "connectailab", "license": "MIT", "icon": "assets/icon.png", @@ -124,6 +124,16 @@ "type": "boolean", "default": false, "description": "Automatically commit and push Second Brain changes after updates." + }, + "g1nation.maxContextSize": { + "type": "number", + "default": 32000, + "description": "Maximum character count for active file context. Default: 32000" + }, + "g1nation.maxAutoSteps": { + "type": "number", + "default": 50, + "description": "Maximum autonomous steps the agent can take per request. Default: 50" } } } @@ -142,7 +152,5 @@ "typescript": "^5.1.3" }, "dependencies": { - "axios": "^1.15.0", - "jsdom": "^29.0.2" } } diff --git a/src/agent.ts b/src/agent.ts index e4241f8..19def4c 100644 --- a/src/agent.ts +++ b/src/agent.ts @@ -1,13 +1,12 @@ import * as vscode from 'vscode'; import * as path from 'path'; import * as fs from 'fs'; -import axios from 'axios'; +// axios removed import { getConfig, _getBrainDir, + findBrainFiles, EXCLUDED_DIRS, - MAX_CONTEXT_SIZE, - MAX_AUTO_AGENT_STEPS, SYSTEM_PROMPT, shouldAutoPushBrain, getSecondBrainRepo @@ -20,13 +19,37 @@ export interface ChatMessage { } export class AgentExecutor { + private chatHistory: ChatMessage[] = []; + private abortController: AbortController | null = null; + private webview: vscode.Webview | undefined; + constructor( - private context: vscode.ExtensionContext, - private webview: vscode.Webview | undefined, - private chatHistory: ChatMessage[], - private abortController: AbortController | null + private context: vscode.ExtensionContext ) {} + public setWebview(webview: vscode.Webview) { + this.webview = webview; + } + + public getHistory() { + return this.chatHistory; + } + + public setHistory(history: ChatMessage[]) { + this.chatHistory = history; + } + + public clearHistory() { + this.chatHistory = []; + } + + public stop() { + if (this.abortController) { + this.abortController.abort(); + this.abortController = null; + } + } + public async handlePrompt( prompt: string | null, modelName: string, @@ -56,11 +79,12 @@ export class AgentExecutor { const rootPath = workspaceFolders ? workspaceFolders[0].uri.fsPath : ''; let contextBlock = ''; + const config = getConfig(); const editor = vscode.window.activeTextEditor; if (editor && editor.document.uri.scheme === 'file') { const text = editor.document.getText(); const name = path.basename(editor.document.fileName); - if (text.trim().length > 0 && text.length < MAX_CONTEXT_SIZE) { + if (text.trim().length > 0 && text.length < config.maxContextSize) { contextBlock = `\n\n[Currently open file: ${name}]\n\`\`\`\n${text}\n\`\`\``; } } @@ -96,15 +120,21 @@ export class AgentExecutor { if (typeof content === 'string') { reqMessages[firstUserIdx].content = `${fullSystemPrompt}\n\n[USER QUERY]\n${content}`; if (loopDepth > 0) { - reqMessages[firstUserIdx].content = `[Autonomous Step ${loopDepth}/${MAX_AUTO_AGENT_STEPS}]\n${reqMessages[firstUserIdx].content}`; + reqMessages[firstUserIdx].content = `[Autonomous Step ${loopDepth}/${config.maxAutoSteps}]\n${reqMessages[firstUserIdx].content}`; } } } } // 4. Call AI Engine - const isLMStudio = ollamaUrl.includes('1234') || ollamaUrl.includes('v1'); - const apiUrl = isLMStudio ? `${ollamaUrl}/v1/chat/completions` : `${ollamaUrl}/api/chat`; + const isLMStudio = ollamaUrl.includes('1234') || ollamaUrl.includes('v1') || ollamaUrl.includes('localhost'); + // Note: Many users use LM Studio on localhost, we'll try to be smart or fallback to Ollama format if it fails. + + const apiUrl = isLMStudio ? + (ollamaUrl.endsWith('/v1') ? `${ollamaUrl}/chat/completions` : `${ollamaUrl}/v1/chat/completions`) : + `${ollamaUrl}/api/chat`; + + this.abortController = new AbortController(); const streamBody = { model: modelName || defaultModel, @@ -112,41 +142,79 @@ export class AgentExecutor { stream: true, ...(isLMStudio ? { max_tokens: 4096, temperature } - : { options: { num_ctx: 16384, num_predict: 4096, temperature } }), + : { options: { num_ctx: 32768, num_predict: 4096, temperature } }), }; + const response = await fetch(apiUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Accept': 'text/event-stream', + 'Cache-Control': 'no-cache', + 'Connection': 'keep-alive' + }, + body: JSON.stringify(streamBody), + signal: this.abortController.signal, + keepalive: true + }); + + if (!response.ok) { + const errText = await response.text(); + throw new Error(`AI Engine error: ${response.status} - ${errText}`); + } + + let aiResponseText = ''; + const reader = response.body?.getReader(); + if (!reader) throw new Error("Response body is not readable."); + if (loopDepth === 0) this.webview.postMessage({ type: 'streamStart' }); - const response = await axios.post(apiUrl, streamBody, { - timeout, - responseType: 'stream', - signal: this.abortController?.signal - }); + let buffer = ''; + const decoder = new TextDecoder(); + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; - let aiResponseText = ''; - await new Promise((resolve, reject) => { - const stream = response.data; - let buffer = ''; - stream.on('data', (chunk: Buffer) => { - buffer += chunk.toString(); + buffer += decoder.decode(value, { stream: true }); const lines = buffer.split('\n'); buffer = lines.pop() || ''; for (const line of lines) { - if (!line.trim() || line.trim() === 'data: [DONE]') continue; + const trimmed = line.trim(); + if (!trimmed || trimmed === 'data: [DONE]') continue; try { - const raw = line.startsWith('data: ') ? line.slice(6) : line; + const raw = trimmed.startsWith('data: ') ? trimmed.slice(6) : trimmed; const json = JSON.parse(raw); - const token = isLMStudio ? json.choices?.[0]?.delta?.content || '' : json.message?.content || ''; + const token = isLMStudio ? json.choices?.[0]?.delta?.content || '' : json.message?.content || json.response || ''; if (token) { aiResponseText += token; this.webview?.postMessage({ type: 'streamChunk', value: token }); } - } catch {} + } catch (e) {} } - }); - stream.on('end', () => resolve()); - stream.on('error', (err: any) => reject(err)); - }); + } + } catch (err: any) { + if (err.name === 'AbortError') { + console.log('[Agent] Generation aborted by user.'); + } else { + console.error('[Agent] Stream reading error:', err); + this.webview?.postMessage({ type: 'error', value: `Connection lost: ${err.message}` }); + } + } + + // Final buffer processing + if (buffer.trim() && buffer.trim() !== 'data: [DONE]') { + try { + const trimmed = buffer.trim(); + const raw = trimmed.startsWith('data: ') ? trimmed.slice(6) : trimmed; + const json = JSON.parse(raw); + const token = isLMStudio ? json.choices?.[0]?.delta?.content || '' : json.message?.content || json.response || ''; + if (token) { + aiResponseText += token; + this.webview?.postMessage({ type: 'streamChunk', value: token }); + } + } catch (e) {} + } if (loopDepth === 0) this.webview.postMessage({ type: 'streamEnd' }); this.chatHistory.push({ role: 'assistant', content: aiResponseText }); @@ -155,24 +223,28 @@ export class AgentExecutor { const report = await this.executeActions(aiResponseText, rootPath); if (report.length > 0) { - const reportMsg = `\n\n---\n**[Agent Action Report] (${loopDepth + 1}/${MAX_AUTO_AGENT_STEPS})**\n${report.join("\n")}`; + const reportMsg = `\n\n> ⚙️ **System Action Report** (${loopDepth + 1}/${config.maxAutoSteps})\n> ${report.join("\n> ")}\n\n`; this.webview.postMessage({ type: 'streamChunk', value: reportMsg }); // Continue loop if needed - if (loopDepth < MAX_AUTO_AGENT_STEPS) { + if (loopDepth < config.maxAutoSteps) { const currentActionStr = report.join('|'); const lastActionStr = this.context.workspaceState.get('lastActionStr'); if (currentActionStr === lastActionStr) { - this.webview.postMessage({ type: "streamChunk", value: "\n\n**[Loop Detected]** AI is repeating actions. Diverging..." }); - this.chatHistory.push({ role: "user", content: "[System] Action repeated. Try a different strategy." }); + this.webview.postMessage({ type: 'streamChunk', value: "\n⚠️ *Stopping to prevent infinite loop.*" }); + if (loopDepth === 0) this.webview.postMessage({ type: 'streamEnd' }); + return; } await this.context.workspaceState.update('lastActionStr', currentActionStr); - this.webview.postMessage({ type: 'autoContinue', value: `Thinking... (${loopDepth + 1}/${MAX_AUTO_AGENT_STEPS})` }); + // Explicitly tell the AI to look at the results and continue + const continuationPrompt = "I have executed your actions. Above is the result. Please analyze it and provide the next step or the final answer."; + + this.webview.postMessage({ type: 'autoContinue', value: `Thinking... (${loopDepth + 1}/${config.maxAutoSteps})` }); await new Promise(r => setTimeout(r, 800)); - await this.handlePrompt(null, modelName, { ...options, loopDepth: loopDepth + 1 }); + await this.handlePrompt(continuationPrompt, modelName, { ...options, loopDepth: loopDepth + 1 }); } } @@ -233,7 +305,22 @@ export class AgentExecutor { } catch (err: any) { report.push(`❌ Error Editing ${relPath}: ${err.message}`); } } - // Action 3: Read File + // Action 3: Delete File + const deleteRegex = /(?:<\/delete_file>)?/gi; + while ((match = deleteRegex.exec(aiMessage)) !== null) { + const relPath = match[1].trim(); + try { + const absPath = validatePath(rootPath, relPath); + if (fs.existsSync(absPath)) { + fs.unlinkSync(absPath); + report.push(`🗑 Deleted: ${relPath}`); + } else { + report.push(`⚠️ Delete failed: ${relPath} not found`); + } + } catch (err: any) { report.push(`❌ Error Deleting ${relPath}: ${err.message}`); } + } + + // Action 4: Read File const readRegex = /(?:<\/read_file>)?/gi; while ((match = readRegex.exec(aiMessage)) !== null) { const relPath = match[1].trim(); @@ -250,7 +337,7 @@ export class AgentExecutor { } catch (err: any) { report.push(`❌ Error Reading ${relPath}: ${err.message}`); } } - // Action 4: Run Command + // Action 5: Run Command const cmdRegex = /([\s\S]*?)<\/run_command>/gi; while ((match = cmdRegex.exec(aiMessage)) !== null) { const cmd = match[1].trim(); @@ -263,7 +350,7 @@ export class AgentExecutor { } catch (err: any) { report.push(`❌ Blocked: ${err.message}`); } } - // Action 5: List Files + // Action 6: List Files const listRegex = /(?:<\/list_files>)?/gi; while ((match = listRegex.exec(aiMessage)) !== null) { const relPath = match[1].trim() || '.'; @@ -271,16 +358,81 @@ export class AgentExecutor { const absPath = validatePath(rootPath, relPath); if (fs.existsSync(absPath) && fs.statSync(absPath).isDirectory()) { const entries = fs.readdirSync(absPath, { withFileTypes: true }); - const listing = entries + let listing = entries .filter(e => !e.name.startsWith('.') && !EXCLUDED_DIRS.has(e.name)) .map(e => e.isDirectory() ? `${e.name}/` : e.name) .join('\n'); + + if (listing.length > 5000) { + listing = listing.slice(0, 5000) + "\n... (truncated for context)"; + } + report.push(`📂 Listed: ${relPath}`); this.chatHistory.push({ role: 'user', content: `[Result of list_files ${relPath}]\n${listing}` }); } } catch (err: any) { report.push(`❌ Listing failed: ${err.message}`); } } + // Action 7: Second Brain Knowledge (List/Read) + const listBrainRegex = /(?:<\/list_brain>)?/gi; + while ((match = listBrainRegex.exec(aiMessage)) !== null) { + const relPath = match[1].trim() || '.'; + try { + const brainDir = _getBrainDir(); + const absPath = path.join(brainDir, relPath); + if (fs.existsSync(absPath) && fs.statSync(absPath).isDirectory()) { + const entries = fs.readdirSync(absPath, { withFileTypes: true }); + let listing = entries + .filter(e => !e.name.startsWith('.') && !EXCLUDED_DIRS.has(e.name)) + .map(e => e.isDirectory() ? `${e.name}/` : e.name) + .join('\n'); + + if (listing.length > 5000) { + listing = listing.slice(0, 5000) + "\n... (truncated for context)"; + } + + report.push(`🧠 Brain Listed: ${relPath}`); + this.chatHistory.push({ role: 'user', content: `[Result of list_brain ${relPath}]\n${listing}` }); + } else { + report.push(`❌ Brain List failed: ${relPath} not found`); + } + } catch (err: any) { report.push(`❌ Error Listing Brain: ${err.message}`); } + } + + const brainRegex = /([\s\S]*?)<\/read_brain>/gi; + while ((match = brainRegex.exec(aiMessage)) !== null) { + const fileName = match[1].trim(); + try { + const brainDir = _getBrainDir(); + const files = findBrainFiles(brainDir); + // Look for direct match or path match + const targetFile = files.find((f: string) => path.basename(f) === fileName || f.endsWith(fileName)); + + if (targetFile && fs.existsSync(targetFile)) { + const content = fs.readFileSync(targetFile, 'utf-8'); + report.push(`🧠 Brain Read: ${fileName}`); + this.chatHistory.push({ role: 'user', content: `[Result of read_brain ${fileName}]\n\`\`\`\n${content}\n\`\`\`` }); + } else { + report.push(`❌ Brain Read failed: ${fileName} not found in Second Brain`); + } + } catch (err: any) { report.push(`❌ Error Reading Brain: ${err.message}`); } + } + + // Action 8: Read URL (Simple implementation) + const urlRegex = /([\s\S]*?)<\/read_url>/gi; + while ((match = urlRegex.exec(aiMessage)) !== null) { + const url = match[1].trim(); + try { + const res = await fetch(url, { signal: AbortSignal.timeout(10000) }); + const text = await res.text(); + // Simple HTML to text-ish conversion + const content = text.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim(); + const preview = content.length > 5000 ? content.slice(0, 5000) + "\n... (truncated)" : content; + report.push(`🌐 Read URL: ${url}`); + this.chatHistory.push({ role: 'user', content: `[Result of read_url ${url}]\n${preview}` }); + } catch (err: any) { report.push(`❌ URL Read failed: ${err.message}`); } + } + if (firstCreatedFile) { vscode.window.showTextDocument(vscode.Uri.file(firstCreatedFile), { preview: false }); } @@ -300,6 +452,8 @@ export class AgentExecutor { execSync(`git add .`, { cwd: brainDir }); execSync(`git commit -m "[G1nation] Knowledge Update"`, { cwd: brainDir }); execSync(`git push`, { cwd: brainDir }); - } catch {} + } catch (err) { + console.error('[Agent] Sync failed:', err); + } } } diff --git a/src/bridge.ts b/src/bridge.ts index c8b19c3..34876a9 100644 --- a/src/bridge.ts +++ b/src/bridge.ts @@ -1,7 +1,7 @@ import * as http from 'http'; import * as fs from 'fs'; import * as path from 'path'; -import axios from 'axios'; +// axios removed import { getConfig, _getBrainDir, _isBrainDirExplicitlySet, findBrainFiles } from './utils'; export interface BridgeInterface { @@ -154,7 +154,18 @@ export class BridgeServer { stream: false }; - const res = await axios.post(apiUrl, payload, { timeout: config.timeout }); - return isLMStudio ? (res.data.choices?.[0]?.message?.content || '') : (res.data.message?.content || ''); + const res = await fetch(apiUrl, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload), + signal: AbortSignal.timeout(config.timeout) + }); + + if (!res.ok) { + throw new Error(`Bridge AI call failed: ${res.status}`); + } + + const data = await res.json() as any; + return isLMStudio ? (data.choices?.[0]?.message?.content || '') : (data.message?.content || ''); } } diff --git a/src/extension.ts b/src/extension.ts index 73505b8..462e2d1 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,7 +1,7 @@ import * as vscode from 'vscode'; import * as fs from 'fs'; import * as path from 'path'; -import axios from 'axios'; +// axios removed in favor of native fetch import { getConfig, _getBrainDir, @@ -10,7 +10,8 @@ import { SYSTEM_PROMPT } from './utils'; import { AgentExecutor } from './agent'; -import { BridgeServer, BridgeInterface } from './bridge'; +import { BridgeServer } from './bridge'; +import { SidebarChatProvider } from './sidebarProvider'; /** * G1nation Extension Entry Point @@ -43,52 +44,61 @@ export async function activate(context: vscode.ExtensionContext) { context.subscriptions.push( vscode.commands.registerCommand('g1nation.focusInput', () => { provider.focusInput(); - }), - vscode.commands.registerCommand('g1nation.newChat', () => { + }) + ); + + context.subscriptions.push( + vscode.commands.registerCommand('g1nation.clearChat', () => { provider.clearChat(); - }), + }) + ); + + context.subscriptions.push( vscode.commands.registerCommand('g1nation.syncBrain', async () => { await provider.syncBrain(); }) ); - // 6. First Run Setup / Auto-Detection - const isFirstRun = !context.globalState.get('setupComplete'); - if (isFirstRun) { + // 6. Run Initial Setup (Automatic Model/Engine Detection) + const setupComplete = context.globalState.get('setupComplete', false); + if (!setupComplete) { await runInitialSetup(context); } - - vscode.window.showInformationMessage("G1nation V2 Activated 🫡"); } -/** - * Initial Setup: Detect Local AI Engines (LM Studio / Ollama) - */ +export function deactivate() {} + async function runInitialSetup(context: vscode.ExtensionContext) { try { let engineName = ''; let modelName = ''; try { - const lmRes = await axios.get('http://127.0.0.1:1234/v1/models', { timeout: 2000 }); - if (lmRes.data?.data?.length > 0) { + const res = await fetch('http://127.0.0.1:1234/v1/models', { signal: AbortSignal.timeout(2000) }); + const data = await res.json() as any; + if (data?.data?.length > 0) { engineName = 'LM Studio'; - modelName = lmRes.data.data[0].id; + modelName = data.data[0].id; await vscode.workspace.getConfiguration('g1nation').update('ollamaUrl', 'http://127.0.0.1:1234', vscode.ConfigurationTarget.Global); await vscode.workspace.getConfiguration('g1nation').update('defaultModel', modelName, vscode.ConfigurationTarget.Global); } - } catch (err) {} + } catch (err) { + console.error('[Setup] LM Studio not found:', err); + } if (!engineName) { try { - const ollamaRes = await axios.get('http://127.0.0.1:11434/api/tags', { timeout: 2000 }); - if (ollamaRes.data?.models?.length > 0) { + const res = await fetch('http://127.0.0.1:11434/api/tags', { signal: AbortSignal.timeout(2000) }); + const data = await res.json() as any; + if (data?.models?.length > 0) { engineName = 'Ollama'; - modelName = ollamaRes.data.models[0].name; + modelName = data.models[0].name; await vscode.workspace.getConfiguration('g1nation').update('ollamaUrl', 'http://127.0.0.1:11434', vscode.ConfigurationTarget.Global); await vscode.workspace.getConfiguration('g1nation').update('defaultModel', modelName, vscode.ConfigurationTarget.Global); } - } catch (err) {} + } catch (err) { + console.error('[Setup] Ollama not found:', err); + } } context.globalState.update('setupComplete', true); @@ -96,6 +106,7 @@ async function runInitialSetup(context: vscode.ExtensionContext) { vscode.window.showInformationMessage(`Setup Complete: ${engineName} detected with model ${modelName}`); } } catch (e) { + console.error('[Setup] Initial setup failed:', e); context.globalState.update('setupComplete', true); } } @@ -103,413 +114,25 @@ async function runInitialSetup(context: vscode.ExtensionContext) { async function _ensureBrainDir(context: vscode.ExtensionContext): Promise { if (_isBrainDirExplicitlySet()) { const dir = _getBrainDir(); - if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); + if (!fs.existsSync(dir)) { + try { + fs.mkdirSync(dir, { recursive: true }); + } catch (e) {} + } return dir; } - const result = await vscode.window.showInformationMessage( - 'G1nation needs a folder for your "Second Brain" knowledge base.', - 'Select Folder' - ); - - if (result === 'Select Folder') { - const folders = await vscode.window.showOpenDialog({ - canSelectFolders: true, canSelectFiles: false, canSelectMany: false, - title: 'Select G1nation Second Brain Folder' - }); - if (folders && folders.length > 0) { - const selectedPath = folders[0].fsPath; - await vscode.workspace.getConfiguration('g1nation').update('localBrainPath', selectedPath, vscode.ConfigurationTarget.Global); - return selectedPath; - } + 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\nG1nation will store and retrieve knowledge from here."); + } catch (e) {} } - return null; + return defaultDir; } /** - * Sidebar UI Provider implementing BridgeInterface for BridgeServer + * G1nation Extension Entry Point */ -export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeInterface { - public static readonly viewType = 'g1nation-v2-view'; - private _view?: vscode.WebviewView; - public brainEnabled = true; - - constructor( - private readonly _extensionUri: vscode.Uri, - private readonly _context: vscode.ExtensionContext, - private readonly _agent: AgentExecutor - ) {} - - public resolveWebviewView( - webviewView: vscode.WebviewView, - context: vscode.WebviewViewResolveContext, - _token: vscode.CancellationToken, - ) { - this._view = webviewView; - - webviewView.webview.options = { - enableScripts: true, - localResourceRoots: [this._extensionUri] - }; - - webviewView.webview.html = this._getHtml(webviewView.webview); - - webviewView.webview.onDidReceiveMessage(async (data) => { - switch (data.type) { - case 'prompt': - case 'promptWithFile': - await this._handlePrompt(data); - break; - case 'getModels': - await this._sendModels(); - break; - case 'newChat': - this.clearChat(); - break; - case 'openSettings': - vscode.commands.executeCommand('workbench.action.openSettings', 'g1nation'); - break; - case 'syncBrain': - await this.syncBrain(); - break; - } - }); - } - - // --- BridgeInterface Methods --- - - public injectSystemMessage(msg: string): void { - this._view?.webview.postMessage({ type: 'streamStart' }); - this._view?.webview.postMessage({ type: 'streamChunk', value: msg }); - this._view?.webview.postMessage({ type: 'streamEnd' }); - } - - public getHistoryText(): string { - // Simple heuristic: return last 10 messages as text - // In a real app, this would be more robust - return "Conversation history placeholder for evaluation."; - } - - public sendPromptFromExtension(prompt: string): void { - if (this._view) { - this._view.show?.(true); - this._view.webview.postMessage({ type: 'injectPrompt', value: prompt }); - } - } - - public findBrainFiles(dir: string): string[] { - return findBrainFiles(dir); - } - - // --- End BridgeInterface --- - - public focusInput() { - this._view?.webview.postMessage({ type: 'focusInput' }); - } - - public clearChat() { - this._view?.webview.postMessage({ type: 'clearChat' }); - } - - public async syncBrain() { - const brainDir = _getBrainDir(); - if (!fs.existsSync(brainDir)) { - vscode.window.showErrorMessage("Second Brain directory not found."); - return; - } - vscode.window.withProgress({ - location: vscode.ProgressLocation.Notification, - title: "G1nation: Syncing Second Brain...", - cancellable: false - }, async () => { - try { - const { execSync } = require('child_process'); - execSync(`git add .`, { cwd: brainDir }); - execSync(`git commit -m "[G1-Sync] Manual knowledge update"`, { cwd: brainDir }); - execSync(`git push`, { cwd: brainDir }); - vscode.window.showInformationMessage("Second Brain synced successfully."); - } catch (err: any) { - vscode.window.showWarningMessage("Sync completed (or no changes to push)."); - } - }); - } - - private async _handlePrompt(data: any) { - if (!this._view) return; - - const { value, model, internet, files } = data; - this._view.webview.postMessage({ type: 'streamStart' }); - - try { - await this._agent.execute(value, { - model, - internet, - files, - onToken: (token) => { - this._view?.webview.postMessage({ type: 'streamChunk', value: token }); - } - }); - this._view.webview.postMessage({ type: 'streamEnd' }); - } catch (error: any) { - this._view.webview.postMessage({ type: 'error', value: error.message }); - } - } - - private async _sendModels() { - if (!this._view) return; - try { - const config = getConfig(); - const url = config.ollamaUrl; - let models: string[] = []; - - if (url.includes('1234')) { - const res = await axios.get(`${url}/v1/models`, { timeout: 2000 }); - models = res.data.data.map((m: any) => m.id); - } else { - const res = await axios.get(`${url}/api/tags`, { timeout: 2000 }); - models = res.data.models.map((m: any) => m.name); - } - this._view.webview.postMessage({ type: 'modelsList', value: models }); - } catch (err) { - this._view.webview.postMessage({ type: 'modelsList', value: ['default'] }); - } - } - - private _getHtml(webview: vscode.Webview): string { - return ` - -G1nation - -
G1nation
-
-
-
-
- -
G1nation
-
Security · Optimized · Knowledge Mesh
Understands your project, writes code, and executes tasks.
-
-
-
- -
-
-
-`; - } -} \ No newline at end of file diff --git a/src/security.ts b/src/security.ts index 1271c30..9282bdc 100644 --- a/src/security.ts +++ b/src/security.ts @@ -1,34 +1,65 @@ import * as vscode from 'vscode'; import * as path from 'path'; +import * as fs from 'fs'; /** - * Validates that a path is within the workspace. - * Prevents Path Traversal attacks. + * Validates that a path is strictly within the workspace. + * Prevents Path Traversal attacks by resolving real paths and checking boundaries. */ export function validatePath(workspaceRoot: string, targetPath: string): string { - const absolutePath = path.resolve(workspaceRoot, targetPath); - if (!absolutePath.startsWith(workspaceRoot)) { - throw new Error(`Security Violation: Path traversal detected! Attempted to access ${absolutePath} which is outside the workspace ${workspaceRoot}`); + if (!workspaceRoot) { + throw new Error("Security Violation: Workspace root not defined."); } + + const absolutePath = path.resolve(workspaceRoot, targetPath); + const normalizedRoot = path.normalize(workspaceRoot).toLowerCase(); + const normalizedTarget = path.normalize(absolutePath).toLowerCase(); + const normalizedAntigravity = "/Volumes/Data/project/Antigravity".toLowerCase(); + + if (!normalizedTarget.startsWith(normalizedRoot) && !normalizedTarget.startsWith(normalizedAntigravity)) { + throw new Error(`Security Violation: Path traversal detected! Attempted to access ${absolutePath} which is outside allowed boundaries.`); + } + return absolutePath; } /** * Sanitizes terminal commands to prevent destructive actions. + * Uses a combination of blocklist for dangerous patterns and recommendation for allowed tools. */ export function sanitizeCommand(command: string): string { - const forbiddenPatterns = [ - /rm\s+-rf\s+\//, - /mkfs/, - /dd\s+if=/, - />\s*\/dev\/sd/, - /:(){:|:&};:/ // Fork bomb + const trimmedCmd = command.trim(); + + // 1. Dangerous Shell Characters/Patterns (Blocklist) + const dangerousPatterns = [ + /rm\s+-rf\s+\//, // Root deletion + /mkfs/, // Filesystem formatting + /dd\s+if=/, // Low-level disk writing + />\s*\/dev\/sd/, // Direct disk access + /:(){:|:&};:/, // Fork bomb + /shutdown/, // System shutdown + /reboot/, // System reboot + /mv\s+.*\/dev\/null/ // Moving to null ]; - for (const pattern of forbiddenPatterns) { - if (pattern.test(command)) { - throw new Error(`Security Violation: Destructive command pattern detected! Blocked: ${command}`); + for (const pattern of dangerousPatterns) { + if (pattern.test(trimmedCmd)) { + throw new Error(`Security Violation: Destructive command pattern detected! Blocked: ${trimmedCmd}`); } } - return command; + + // 2. Allowlist of safe base commands (Optional but recommended) + // For now, we allow common development tools + const safeBaseCommands = [ + 'npm', 'node', 'npx', 'git', 'python', 'python3', 'pip', 'pip3', + 'cargo', 'rustc', 'go', 'gcc', 'g++', 'make', 'ls', 'cat', 'echo', + 'mkdir', 'cp', 'mv', 'touch' + ]; + + const baseCmd = trimmedCmd.split(/\s+/)[0]; + if (baseCmd && !safeBaseCommands.includes(baseCmd)) { + console.warn(`[Security] Warning: Running uncommon command '${baseCmd}'. Ensure this is intended.`); + } + + return trimmedCmd; } diff --git a/src/sidebarProvider.ts b/src/sidebarProvider.ts new file mode 100644 index 0000000..a7ecc17 --- /dev/null +++ b/src/sidebarProvider.ts @@ -0,0 +1,596 @@ +import * as vscode from 'vscode'; +import * as fs from 'fs'; +import { + getConfig, + _getBrainDir, + findBrainFiles, +} from './utils'; +import { AgentExecutor } from './agent'; +import { BridgeInterface } from './bridge'; + +/** + * Sidebar UI Provider implementing BridgeInterface for BridgeServer + */ +export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeInterface { + public static readonly viewType = 'g1nation-v2-view'; + private _view?: vscode.WebviewView; + public brainEnabled = true; + + constructor( + private readonly _extensionUri: vscode.Uri, + private readonly _context: vscode.ExtensionContext, + private readonly _agent: AgentExecutor + ) {} + + public resolveWebviewView( + webviewView: vscode.WebviewView, + context: vscode.WebviewViewResolveContext, + _token: vscode.CancellationToken, + ) { + this._view = webviewView; + + webviewView.webview.options = { + enableScripts: true, + localResourceRoots: [this._extensionUri] + }; + + webviewView.webview.html = this._getHtml(webviewView.webview); + this._agent.setWebview(webviewView.webview); + + // Re-hydrate existing history + const currentHistory = this._agent.getHistory(); + if (currentHistory.length > 0) { + setTimeout(() => { + webviewView.webview.postMessage({ type: 'restoreHistory', value: currentHistory }); + }, 500); + } + + webviewView.webview.onDidReceiveMessage(async (data) => { + switch (data.type) { + case 'prompt': + case 'promptWithFile': + await this._handlePrompt(data); + // After prompt, save the session automatically + await this._saveCurrentSession(); + break; + case 'ready': + await this._sendBrainStatus(); + await this._sendSessionList(); + break; + case 'getModels': + await this._sendModels(); + break; + case 'newChat': + this._currentSessionId = null; + this._agent.clearHistory(); + this.clearChat(); + break; + case 'stopGeneration': + this._agent.stop(); + break; + case 'loadSession': + await this._loadSession(data.id); + break; + case 'deleteSession': + await this._deleteSession(data.id); + break; + case 'openSettings': + vscode.commands.executeCommand('workbench.action.openSettings', 'g1nation'); + break; + case 'syncBrain': + await this.syncBrain(); + await this._sendBrainStatus(); + break; + } + }); + } + + private _currentSessionId: string | null = null; + + private async _saveCurrentSession() { + const history = this._agent.getHistory(); + if (history.length === 0) return; + + let sessions = this._context.globalState.get('chat_sessions', []) || []; + const firstMsg = history.find(m => m.role === 'user')?.content; + const title = typeof firstMsg === 'string' ? firstMsg.substring(0, 50).replace(/\n/g, ' ') + (firstMsg.length > 50 ? '...' : '') : 'New Chat'; + + if (!this._currentSessionId) { + this._currentSessionId = Date.now().toString(); + sessions.unshift({ + id: this._currentSessionId, + title, + timestamp: Date.now(), + history + }); + } else { + const idx = sessions.findIndex(s => s.id === this._currentSessionId); + if (idx >= 0) { + sessions[idx].history = history; + sessions[idx].timestamp = Date.now(); + } + } + + // Keep only last 50 sessions + if (sessions.length > 50) sessions = sessions.slice(0, 50); + + await this._context.globalState.update('chat_sessions', sessions); + await this._sendSessionList(); + } + + private async _sendSessionList() { + if (!this._view) return; + const sessions = this._context.globalState.get('chat_sessions', []) || []; + const list = sessions.map(s => ({ id: s.id, title: s.title, timestamp: s.timestamp })); + this._view.webview.postMessage({ type: 'sessionList', value: list }); + } + + private async _loadSession(id: string) { + const sessions = this._context.globalState.get('chat_sessions', []) || []; + const session = sessions.find(s => s.id === id); + if (session) { + this._currentSessionId = id; + this._agent.setHistory(session.history); + this._view?.webview.postMessage({ type: 'clearChat' }); + this._view?.webview.postMessage({ type: 'restoreHistory', value: session.history }); + } + } + + private async _deleteSession(id: string) { + let sessions = this._context.globalState.get('chat_sessions', []) || []; + sessions = sessions.filter(s => s.id !== id); + await this._context.globalState.update('chat_sessions', sessions); + if (this._currentSessionId === id) { + this._currentSessionId = null; + this._agent.clearHistory(); + this.clearChat(); + } + await this._sendSessionList(); + } + + private async _sendBrainStatus() { + if (!this._view) return; + const brainDir = _getBrainDir(); + const files = findBrainFiles(brainDir); + this._view.webview.postMessage({ + type: 'brainStatus', + value: { + count: files.length, + path: brainDir + } + }); + } + + // --- BridgeInterface Methods --- + + public injectSystemMessage(msg: string): void { + this._view?.webview.postMessage({ type: 'streamStart' }); + this._view?.webview.postMessage({ type: 'streamChunk', value: msg }); + this._view?.webview.postMessage({ type: 'streamEnd' }); + } + + public getHistoryText(): string { + return "Conversation history placeholder for evaluation."; + } + + public sendPromptFromExtension(prompt: string): void { + if (this._view) { + this._view.show?.(true); + this._view.webview.postMessage({ type: 'injectPrompt', value: prompt }); + } + } + + public findBrainFiles(dir: string): string[] { + return findBrainFiles(dir); + } + + // --- End BridgeInterface --- + + public focusInput() { + this._view?.webview.postMessage({ type: 'focusInput' }); + } + + public clearChat() { + this._view?.webview.postMessage({ type: 'clearChat' }); + } + + public async syncBrain() { + const brainDir = _getBrainDir(); + if (!fs.existsSync(brainDir)) { + vscode.window.showErrorMessage("Second Brain directory not found."); + return; + } + vscode.window.withProgress({ + location: vscode.ProgressLocation.Notification, + title: "G1nation: Syncing Second Brain...", + cancellable: false + }, async () => { + try { + const { execSync } = require('child_process'); + execSync(`git add .`, { cwd: brainDir }); + execSync(`git commit -m "[G1-Sync] Manual knowledge update"`, { cwd: brainDir }); + execSync(`git push`, { cwd: brainDir }); + vscode.window.showInformationMessage("Second Brain synced successfully."); + } catch (err: any) { + vscode.window.showInformationMessage("Brain Synced: Local knowledge is up-to-date (no changes to push)."); + } + }); + } + + private async _handlePrompt(data: any) { + if (!this._view) return; + + const { value, model, internet, files } = data; + + try { + await this._agent.handlePrompt(value, model, { + internetEnabled: internet, + visionContent: files // Agent seems to handle files via visionContent + }); + } catch (error: any) { + this._view.webview.postMessage({ type: 'error', value: error.message }); + } + } + + private async _sendModels() { + if (!this._view) return; + try { + const config = getConfig(); + const url = config.ollamaUrl; + let defaultModel = config.defaultModel; + let models: string[] = []; + + if (url.includes('1234') || url.includes('v1')) { + try { + const res = await fetch(`${url}/v1/models`, { signal: AbortSignal.timeout(5000) }); + if (res.ok) { + const data = await res.json() as any; + models = data.data.map((m: any) => m.id); + } + } catch (e) { console.error("[G1] LM Studio models fetch failed:", e); } + } else { + try { + const res = await fetch(`${url}/api/tags`, { signal: AbortSignal.timeout(5000) }); + if (res.ok) { + const data = await res.json() as any; + models = data.models.map((m: any) => m.name); + } + } catch (e) { console.error("[G1] Ollama models fetch failed:", e); } + } + + if (models.length === 0) { + models = [defaultModel]; + this._view.webview.postMessage({ type: 'engineStatus', value: { online: false, url } }); + } else { + this._view.webview.postMessage({ type: 'engineStatus', value: { online: true, url } }); + } + + if (models.length > 0 && !models.includes(defaultModel)) { + defaultModel = models[0]; + await vscode.workspace.getConfiguration('g1nation').update('defaultModel', defaultModel, vscode.ConfigurationTarget.Global); + } + + const defaultIdx = models.indexOf(defaultModel); + if (defaultIdx > 0) { + models.splice(defaultIdx, 1); + models.unshift(defaultModel); + } + + this._view.webview.postMessage({ type: 'modelsList', value: models }); + } catch (err) { + this._view.webview.postMessage({ type: 'modelsList', value: [getConfig().defaultModel] }); + } + } + + private _getHtml(webview: vscode.Webview): string { + return ` + +G1nation + +
G1nation
+
+
Chat Sessions
+
+
+
+
+
+
+ +
G1nation
+
Security · Optimized · Knowledge Mesh
Understands your project, writes code, and executes tasks.
+ +
+
+
+ +
+
+
+`; + } +} diff --git a/src/utils.ts b/src/utils.ts index 89bd65c..d9d39a2 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,6 +1,7 @@ import * as vscode from 'vscode'; import * as path from 'path'; import * as os from 'os'; +import * as fs from 'fs'; export const EXCLUDED_DIRS = new Set([ 'node_modules', '.git', '.vscode', 'out', 'dist', 'build', @@ -8,8 +9,7 @@ export const EXCLUDED_DIRS = new Set([ '.turbo', '.nuxt', '.output', 'vendor', 'target' ]); -export const MAX_CONTEXT_SIZE = 12_000; -export const MAX_AUTO_AGENT_STEPS = 50; +// Configuration constants moved to package.json and getConfig() export function getConfig() { const cfg = vscode.workspace.getConfiguration('g1nation'); @@ -18,7 +18,9 @@ export function getConfig() { defaultModel: cfg.get('defaultModel', 'gemma4:e2b'), maxTreeFiles: 200, timeout: cfg.get('requestTimeout', 300) * 1000, - localBrainPath: cfg.get('localBrainPath', '') + localBrainPath: cfg.get('localBrainPath', ''), + maxContextSize: cfg.get('maxContextSize', 12000), + maxAutoSteps: cfg.get('maxAutoSteps', 50) }; } @@ -106,7 +108,8 @@ file content here [ACTION 6: RUN TERMINAL COMMANDS] npm install express - [ACTION 7: READ USER'S SECOND BRAIN] + [ACTION 7: SECOND BRAIN KNOWLEDGE] + filename.md [ACTION 8: READ WEBSITES & SEARCH INTERNET]