indirecttek-vibe-engine 1.6.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 IndirectTek
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,140 @@
1
+ # IndirectTek Vibe Engine 🤖⚡ (v1.6.29)
2
+
3
+ > **🚀 PRODUCTION STABLE RELEASE**
4
+ > **"Autonomous Local AI Agent. Maximum Badassery."**
5
+
6
+ **IndirectTek Vibe Engine** is a local-first autonomous VS Code extension designed for developers who want code, not conversation. Powered by your local **Ollama** instance, it turns your IDE into a self-healing, self-scaffolding autonomous agent.
7
+
8
+ No API keys. No data leaks. Pure local compute.
9
+
10
+ ---
11
+
12
+ ## 🚨 CRITICAL CONFIGURATION
13
+
14
+ **Quick Start (Best Way)**
15
+ You don't need to clone this repo to run the Agent Controller. Just use `npx`:
16
+
17
+ ```bash
18
+ # 1. Start the Agent Controller (Requires Node.js 18+)
19
+ npx indirecttek-vibe-engine
20
+
21
+ # 2. It will print a Token. Copy it.
22
+
23
+ # 3. Configure VS Code Extension:
24
+ # - Setting: IndirectTek AI -> Controller Token
25
+ # - Paste the token.
26
+ ```
27
+
28
+ ## 🛠️ Development Setup
29
+
30
+ If you want to contribute to the engine:
31
+
32
+ 1. Clone this repo.
33
+ 2. Run `npm install`.
34
+ 3. Run `npm run compile`.
35
+ 4. Run `./start-agent.sh` (Auto-configures your local VS Code).
36
+
37
+ ---
38
+
39
+ ## 🚨 CONFIGURATION Details
40
+
41
+ **User Settings vs. Workspace Settings**
42
+
43
+ VS Code has two places to store settings:
44
+ 1. **User Settings** (Global, applies to all windows).
45
+ 2. **Workspace Settings** (Local, applies ONLY to the current folder).
46
+
47
+ **⚠️ IF YOU ARE DEVELOPING LOCALLY OR OPENING THIS REPO:**
48
+ Check your **Workspace Settings** tab!
49
+ Local `.vscode/settings.json` files **OVERRIDE** your global User Settings.
50
+ Ensure `Partner Bot: Use Controller` is checked ✅ in the **Workspace** tab if you are seeing "Direct Mode" behavior or XML tags.
51
+
52
+ ---
53
+
54
+ ## ✨ Features (v1.6.28)
55
+
56
+ * **🔒 100% Local**: Works with **Ollama** (Qwen 2.5, Llama 3). Your IP, your rules.
57
+ * **🧠 Hybrid Architecture**:
58
+ * **Extension**: Handles UI, file I/O, and command execution.
59
+ * **Agent Controller**: A standalone Node.js brain process. It manages context, parses actions (JSON), and talks to Ollama.
60
+ * **⚡ Structured Editing**: Uses `create_file` and `edit_file` JSON actions for surgical precision.
61
+ * **🛑 Loop Protection (New)**:
62
+ * **Terminal Actions**: High-cost actions (like `generate_image`) force the agent to STOP and wait for user approval. No more runaway loops.
63
+ * **Hard Kill Switch**: The "Stop" button is now a physical kill switch that severs the connection immediately.
64
+ * **🌉 Protocol Agnostic**: The Controller can now translate both JSON and legacy XML responses.
65
+ * **🛑 Auto-Fresh Terminals**: Blocking commands (like starting servers) now strictly use a managed `Vibe Engine (Auto)` terminal. The agent kills and recreates this terminal for every new blocking task, ensuring no "shell hijacking" or stuck processes.
66
+
67
+ ---
68
+
69
+ ## 🛠️ Setup & Configuration
70
+
71
+ ### 1. Install Ollama
72
+ [Download Ollama](https://ollama.com) and pull a coding model:
73
+ ```bash
74
+ ollama pull qwen2.5-coder:32b
75
+ ```
76
+
77
+ ### 2. Install the Extension
78
+ Install `indirecttek-vibe-engine` from the VS Code Marketplace.
79
+
80
+ ### 3. Start the Agent Controller (Securely)
81
+ The brain lives outside the editor for security and stability.
82
+
83
+ ```bash
84
+ # 1. Pick a secret token (e.g. "my-secret-key")
85
+ export VIBE_TOKEN=my-secret-key
86
+
87
+ # 2. Run the controller (from source or installed package)
88
+ npm run agent-controller
89
+ ```
90
+ *Output: `[VibeAgentController] Listening on http://127.0.0.1:43110 (Secured)`*
91
+
92
+ ### 4. Configure VS Code
93
+ In Settings -> `IndirectTek AI`:
94
+ * **Ollama Url**: `http://localhost:11434`
95
+ * **Model**: `qwen2.5-coder:32b`
96
+ * **Use Controller**: ✅ **Check this box** (CRITICAL)
97
+ * **Controller Token**: Enter your secret (e.g. `my-secret-key`)
98
+ * **Controller Port**: `43110` (Default)
99
+
100
+ ### 5. (Optional) Local Image Gen
101
+ Set `Partner Bot: Fooocus Url` to your local Fooocus instance/Docker container to enable `generate_image` capability.
102
+
103
+ ### 6. 🎨 Vibe Studio (Web Client)
104
+ This project includes a dedicated web interface for image generation called **Vibe Studio**. It connects to your local GPU (Fooocus API) and provides a user-friendly way to create images.
105
+
106
+ **Features:**
107
+ - 🖥️ **Wife-Friendly UI:** Clean, dark-mode interface.
108
+ - ⏬ **Instant Downloads:** Save creates as PNGs directly to your computer.
109
+ - 📏 **Custom Sizes:** Choose from presets or enter custom dimensions.
110
+ - 🖼️ **Image Manipulation:** Supports Style Transfer and Face Swap.
111
+
112
+ **How to Run:**
113
+ Simply run the included script:
114
+ ```bash
115
+ ./start-studio.sh
116
+ ```
117
+ Or manually:
118
+ ```bash
119
+ cd vibe-studio
120
+ npm install # First time only
121
+ npm run dev
122
+ ```
123
+ Open `http://localhost:5173` in your browser.
124
+
125
+ ---
126
+
127
+ ## 🛡️ Stability & Safety
128
+ * **Localhost Only**: The controller only listens on `127.0.0.1`.
129
+ * **Deep Abort**: Clicking "Stop" propagates a signal through the entire web stack, cancelling generic requests and image downloads instantly.
130
+ * **Single-Shot Actions**: Image generation is strictly limited to 1 per turn to prevent resource exhaustion.
131
+
132
+ ---
133
+
134
+ ## 📜 License
135
+
136
+ **MIT License**. Built with ❤️ and ☕ by **[IndirectTek](https://indirecttek.com)**.
137
+
138
+ Visit usage docs at [indirecttek.com](https://indirecttek.com).
139
+
140
+ *Verified to run on Apple Silicon & Linux.*
@@ -0,0 +1,36 @@
1
+ #!/usr/bin/env node
2
+
3
+ const path = require('path');
4
+ const fs = require('fs');
5
+ const { execSync } = require('child_process');
6
+
7
+ console.log("🚀 Starting IndirectTek Vibe Agent (Standalone Mode)...");
8
+
9
+ // Ensure we are running from the package root relative to this script
10
+ // When installed via npm, this script is in node_modules/package/bin
11
+ // The compiled output is in node_modules/package/out
12
+
13
+ const compiledPath = path.join(__dirname, '../out/agent-controller.js');
14
+
15
+ if (!fs.existsSync(compiledPath)) {
16
+ console.error(`❌ Error: Could not find compiled agent controller at: ${compiledPath}`);
17
+ console.error("If you are running from source, please run 'npm run compile' first.");
18
+ process.exit(1);
19
+ }
20
+
21
+ // Generate Token if missing
22
+ if (!process.env.VIBE_TOKEN) {
23
+ const crypto = require('crypto');
24
+ const token = 'vibe-' + crypto.randomBytes(2).toString('hex');
25
+ process.env.VIBE_TOKEN = token;
26
+
27
+ console.log("================================================================");
28
+ console.log(`🔑 GENERATED TOKEN: ${token}`);
29
+ console.log("👉 Copy this token to VS Code Settings -> IndirectTek AI");
30
+ console.log("================================================================");
31
+ } else {
32
+ console.log(`🔑 Using Token: ${process.env.VIBE_TOKEN}`);
33
+ }
34
+
35
+ // Pass control to the main controller
36
+ require(compiledPath);
@@ -0,0 +1,449 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parseActionsFromReply = parseActionsFromReply;
4
+ const http = require("http");
5
+ const node_fetch_1 = require("node-fetch");
6
+ const DEFAULT_PORT = Number(process.env.VIBE_AGENT_PORT ?? 43110);
7
+ function readJsonBody(req) {
8
+ return new Promise((resolve, reject) => {
9
+ const chunks = [];
10
+ req.on('data', (c) => chunks.push(Buffer.isBuffer(c) ? c : Buffer.from(c)));
11
+ req.on('end', () => {
12
+ const raw = Buffer.concat(chunks).toString('utf8');
13
+ try {
14
+ const parsed = raw ? JSON.parse(raw) : {};
15
+ resolve(parsed);
16
+ }
17
+ catch (e) {
18
+ reject(new Error('Invalid JSON body'));
19
+ }
20
+ });
21
+ req.on('error', (err) => reject(err));
22
+ });
23
+ }
24
+ const fs = require('fs');
25
+ const path = require('path');
26
+ function detectContextFromSummary(summary) {
27
+ // If no summary provided, fall back to "Generic" safest defaults
28
+ if (!summary)
29
+ return `*** TECHNOLOGY STACK (GENERIC) ***\n- **Runtime**: Node.js (assumed)\n- **Constraints**: Use 'npx' for all tools.`;
30
+ let techStack = ['- **Package Manager**: npm']; // Default
31
+ let constraints = ['- **NO Global Installs**: Do not assume tools are in PATH. Use `npx`.'];
32
+ let detected = [];
33
+ // 1. Cloudflare Detection (Look for wrangler.toml in the text summary)
34
+ if (summary.includes('wrangler.toml') || summary.includes('wrangler.json')) {
35
+ detected.push('Cloudflare');
36
+ techStack.push('- **Platform**: Cloudflare Workers / Pages');
37
+ techStack.push('- **Database**: Cloudflare D1 (SQLite). Usage: `npx wrangler d1 execute <db> --command "..."`');
38
+ techStack.push('- **Storage**: Cloudflare R2.');
39
+ techStack.push(' - CREATE Bucket: `npx wrangler r2 bucket create <name>`');
40
+ techStack.push(' - LIST Buckets: `npx wrangler r2 bucket list`');
41
+ techStack.push('- **Tools**: `npx wrangler`');
42
+ constraints.push('- **NO AWS**: Do not suggest S3, Lambda, DynamoDB, or EC2. Use Cloudflare equivalents (D1, R2).');
43
+ }
44
+ // 2. AWS Detection (CDK / SST)
45
+ else if (summary.includes('cdk.json') || summary.includes('sst.config.ts')) {
46
+ detected.push('AWS');
47
+ techStack.push('- **Platform**: AWS');
48
+ techStack.push('- **IaC**: AWS CDK / SST');
49
+ constraints.push('- **NO Cloudflare**: Do not use `wrangler`.');
50
+ }
51
+ // 3. Framework Detection
52
+ if (summary.includes('astro.config.mjs') || summary.includes('astro.config.ts')) {
53
+ detected.push('Astro');
54
+ techStack.push('- **Framework**: Astro');
55
+ }
56
+ else if (summary.includes('next.config.js') || summary.includes('next.config.mjs')) {
57
+ detected.push('Next.js');
58
+ techStack.push('- **Framework**: Next.js');
59
+ }
60
+ // 4. Fallback if empty (Generic Node)
61
+ if (detected.length === 0 && summary.includes('package.json')) {
62
+ detected.push('Node.js');
63
+ techStack.push('- **Runtime**: Node.js');
64
+ }
65
+ console.log(`[Context Detection] Detected: ${detected.join(', ') || 'Unknown'} (from Workspace Summary)`);
66
+ // Return the formatted block
67
+ return `*** STRICT TECHNOLOGY STACK (DETECTED) ***
68
+ ${techStack.join('\n')}
69
+
70
+ *** NEGATIVE CONSTRAINTS ***
71
+ ${constraints.join('\n')}`;
72
+ }
73
+ async function callOllama(request, signal) {
74
+ const { ollama, mode, userMessage, history, workspaceSummary, visibleContext } = request;
75
+ // Use request-provided summary for detection
76
+ const dynamicContext = detectContextFromSummary(workspaceSummary);
77
+ const systemPrompt = mode === 'plan'
78
+ ? `You are a senior software architect.\nCreate a clear, phased implementation plan.\nDo NOT execute commands or modify files.\nRespond in Markdown.`
79
+ : mode === 'chat'
80
+ ? `You are a helpful senior engineer.\nExplain concepts and answer questions.\nYou may reference code context but DO NOT propose tool calls.\nRespond in Markdown.`
81
+ : `You are an AUTONOMOUS CODING AGENT.
82
+ Your goal is to SOLVE the user's problem by directly editing the open project.
83
+
84
+ ${dynamicContext}
85
+
86
+ *** TRANSPARENCY PROTOCOL ***
87
+ You MUST start your "Thought" section by explicitly reporting the status of the PREVIOUS action.
88
+ - Did the last command succeed?
89
+ - Did it fail? Why?
90
+ - What are you doing next based that result?
91
+
92
+ *** TOOLS AVAILABLE ***
93
+ - read_file(path): Read file content.
94
+ - create_file(path, content): Create/Overwrite file.
95
+ - search_replace(path, target, replacement): Targeted edit.
96
+ - run_command(cmd): Run shell command.
97
+ - list_files(dir): List directory.
98
+
99
+ *** TOOL USAGE TIPS ***
100
+ - **search_replace**:
101
+ - The \`target\` must MATCH EXACTLY (including whitespace).
102
+ - If it fails, try a smaller \`target\` or read the file again.
103
+ - **FALLBACK**: If \`search_replace\` keeps failing for a small file (< 200 lines), just use \`create_file\` to overwrite the whole thing with the corrected content.
104
+ - **run_command**:
105
+ - Use \`npx\` for local tools.
106
+ - If a command is stuck, use \`Ctrl+C\` (not available here, so just stop).
107
+
108
+ *** RESPONSE FORMAT ***
109
+ Return a JSON object with this structure:
110
+ {
111
+ "thought": "Report status of previous action. Then analyze current state and plan next step.",
112
+ "action": {
113
+ "tool": "tool_name",
114
+ "args": { ... }
115
+ }
116
+ **AVAILABLE TOOLS:**
117
+ 1. create_file(path, content): Create new files.
118
+ 2. edit_file(path, search, replace): Edit existing files.
119
+ 3. run_command(command): Run shell commands.
120
+ 4. generate_image(prompt, output_path): Generate generic images using the host's AI.
121
+
122
+ **RULES:**
123
+ - NO XML tags (<CREATE_FILE>, etc.). ONLY use the JSON block.
124
+ - The JSON block must be valid and parsable.
125
+ - **IMAGE GENERATION**:
126
+ - You possess a NATIVE tool called 'generate_image'.
127
+ - If the user needs an image, **DO NOT** try to install Python, Stable Diffusion, or DALL-E scripts.
128
+ - **DO NOT** execute 'pip install'.
129
+ - ALWAYS simply output the 'generate_image' JSON action.
130
+ - Example: { "type": "generate_image", "prompt": "...", "output_path": "..." }.
131
+ - **CLI TOOLS**:
132
+ - For tools like 'wrangler', 'eslint', 'tsc', 'vite', etc., **ALWAYS** use 'npx <command>'.
133
+ - Example: 'npx wrangler login', 'npx eslint .'`;
134
+ const messages = [];
135
+ messages.push({ role: 'system', content: systemPrompt });
136
+ if (workspaceSummary || visibleContext) {
137
+ const ctxPieces = [];
138
+ if (workspaceSummary)
139
+ ctxPieces.push(`[Workspace Summary]\n${workspaceSummary}`);
140
+ if (visibleContext)
141
+ ctxPieces.push(`[Visible Context]\n${visibleContext}`);
142
+ messages.push({
143
+ role: 'system',
144
+ content: ctxPieces.join('\n\n')
145
+ });
146
+ }
147
+ if (history && history.length > 0) {
148
+ messages.push(...history);
149
+ }
150
+ messages.push({ role: 'user', content: userMessage });
151
+ const endpoint = ollama.url.endsWith('/api/chat')
152
+ ? ollama.url
153
+ : `${ollama.url.replace(/\/$/, '')}/api/chat`;
154
+ const response = await (0, node_fetch_1.default)(endpoint, {
155
+ method: 'POST',
156
+ headers: { 'Content-Type': 'application/json' },
157
+ body: JSON.stringify({
158
+ model: ollama.model,
159
+ messages,
160
+ stream: false
161
+ })
162
+ });
163
+ if (!response.ok) {
164
+ throw new Error(`Ollama error: ${response.status} ${response.statusText}`);
165
+ }
166
+ const text = await response.text();
167
+ // Ollama can return NDJSON; aggregate message.content fields.
168
+ const lines = text.split('\n').filter((l) => l.trim() !== '');
169
+ let combined = '';
170
+ for (const line of lines) {
171
+ try {
172
+ const json = JSON.parse(line);
173
+ if (json.message && json.message.content) {
174
+ combined += json.message.content;
175
+ }
176
+ }
177
+ catch {
178
+ // ignore non-JSON lines
179
+ }
180
+ }
181
+ return combined || text;
182
+ }
183
+ async function callOllamaWithMessages(body, signal) {
184
+ const { ollama, messages } = body;
185
+ if (!ollama || !ollama.url || !ollama.model || !Array.isArray(messages)) {
186
+ throw new Error('Missing ollama configuration or messages array');
187
+ }
188
+ const endpoint = ollama.url.endsWith('/api/chat')
189
+ ? ollama.url
190
+ : `${ollama.url.replace(/\/$/, '')}/api/chat`;
191
+ const response = await (0, node_fetch_1.default)(endpoint, {
192
+ method: 'POST',
193
+ headers: { 'Content-Type': 'application/json' },
194
+ signal: signal,
195
+ body: JSON.stringify({
196
+ model: ollama.model,
197
+ messages,
198
+ stream: false
199
+ })
200
+ });
201
+ if (!response.ok) {
202
+ throw new Error(`Ollama error: ${response.status} ${response.statusText}`);
203
+ }
204
+ const text = await response.text();
205
+ const lines = text.split('\n').filter((l) => l.trim() !== '');
206
+ let combined = '';
207
+ for (const line of lines) {
208
+ try {
209
+ const json = JSON.parse(line);
210
+ if (json.message && json.message.content) {
211
+ combined += json.message.content;
212
+ }
213
+ }
214
+ catch {
215
+ // ignore
216
+ }
217
+ }
218
+ return combined || text;
219
+ }
220
+ function parseActionsFromReply(rawInput, body) {
221
+ let raw = rawInput;
222
+ let actions;
223
+ if (body.mode === 'agent') {
224
+ // Look for an ```actions ... ``` block and parse it as JSON.
225
+ // Look for an ```actions ... ```, ```json ... ```, or ``` ... ``` block and parse it as JSON.
226
+ // We use a broader regex to catch models that ignore the specific 'actions' language tag.
227
+ const actionsRegex = /```(?:actions|json|jsonc)?\s*(\[[\s\S]*?\]|\{[\s\S]*?\})\s*```/i;
228
+ const match = actionsRegex.exec(raw);
229
+ if (match && match[1]) {
230
+ try {
231
+ const parsed = JSON.parse(match[1]);
232
+ let arr = [];
233
+ if (Array.isArray(parsed)) {
234
+ arr = parsed;
235
+ }
236
+ else if (parsed && typeof parsed === 'object' && Array.isArray(parsed.actions)) {
237
+ // Wrapped form: { "actions": [ ... ] }
238
+ arr = parsed.actions;
239
+ }
240
+ const limits = body.limits || {};
241
+ const maxActions = typeof limits.maxActions === 'number' ? limits.maxActions : 3;
242
+ const filtered = [];
243
+ for (const rawItem of arr) {
244
+ if (!rawItem || typeof rawItem !== 'object')
245
+ continue;
246
+ // Normalise fields: allow "file" as alias for "path".
247
+ const path = typeof rawItem.path === 'string'
248
+ ? rawItem.path
249
+ : typeof rawItem.file === 'string'
250
+ ? rawItem.file
251
+ : undefined;
252
+ const search = typeof rawItem.search === 'string' ? rawItem.search : undefined;
253
+ const replace = typeof rawItem.replace === 'string' ? rawItem.replace : undefined;
254
+ const content = typeof rawItem.content === 'string' ? rawItem.content : undefined;
255
+ const command = typeof rawItem.command === 'string' ? rawItem.command : undefined;
256
+ const prompt = typeof rawItem.prompt === 'string' ? rawItem.prompt : undefined;
257
+ const output_path = typeof rawItem.output_path === 'string' ? rawItem.output_path : undefined;
258
+ const type = typeof rawItem.type === 'string'
259
+ ? rawItem.type
260
+ : content && path
261
+ ? 'create_file'
262
+ : path && search && replace
263
+ ? 'edit_file'
264
+ : undefined;
265
+ if (type === 'create_file' && path && content) {
266
+ filtered.push({
267
+ type: 'create_file',
268
+ path,
269
+ content
270
+ });
271
+ }
272
+ else if (type === 'edit_file' && path && search && replace) {
273
+ filtered.push({
274
+ type: 'edit_file',
275
+ path,
276
+ search,
277
+ replace
278
+ });
279
+ }
280
+ else if (type === 'run_command' && command) {
281
+ const maxRuns = typeof limits.maxRunCommands === 'number' ? limits.maxRunCommands : 1;
282
+ if (filtered.filter(a => a.type === 'run_command').length < maxRuns) {
283
+ filtered.push({
284
+ type: 'run_command',
285
+ command
286
+ });
287
+ }
288
+ }
289
+ else if (type === 'read_file' && path) {
290
+ filtered.push({
291
+ type: 'read_file',
292
+ path
293
+ });
294
+ }
295
+ else if (type === 'generate_image' && prompt) {
296
+ filtered.push({
297
+ type: 'generate_image',
298
+ prompt,
299
+ output_path
300
+ });
301
+ }
302
+ if (filtered.length >= maxActions)
303
+ break;
304
+ }
305
+ actions = filtered;
306
+ }
307
+ catch (e) {
308
+ // Invalid JSON block
309
+ }
310
+ if (actions && actions.length > 0) {
311
+ raw = raw.replace(match[0], '').trim();
312
+ }
313
+ }
314
+ // FAILSAFE: XML Fallback
315
+ if (!actions || actions.length === 0) {
316
+ const xmlActions = [];
317
+ const imgRegex = /<GENERATE_IMAGE>[\s\S]*?<PROMPT>([\s\S]*?)<\/PROMPT>[\s\S]*?<OUTPUT>([\s\S]*?)<\/OUTPUT>[\s\S]*?<\/GENERATE_IMAGE>/gi;
318
+ let xmlMatch;
319
+ while ((xmlMatch = imgRegex.exec(raw)) !== null) {
320
+ const prompt = xmlMatch[1].trim();
321
+ const output_path = xmlMatch[2].trim();
322
+ xmlActions.push({
323
+ type: 'generate_image',
324
+ prompt,
325
+ output_path
326
+ });
327
+ // Strip from markdown
328
+ raw = raw.replace(xmlMatch[0], '').trim();
329
+ }
330
+ if (xmlActions.length > 0)
331
+ actions = xmlActions;
332
+ }
333
+ }
334
+ return { replyMarkdown: raw, actions: actions || [] };
335
+ }
336
+ const AUTH_TOKEN = process.env.VIBE_TOKEN || process.argv.find(arg => arg.startsWith('--token='))?.split('=')[1];
337
+ // --- Global Abort Controller ---
338
+ let activeAbortController = null;
339
+ function createServer() {
340
+ const server = http.createServer(async (req, res) => {
341
+ // Token Auth
342
+ if (AUTH_TOKEN) {
343
+ const headerToken = req.headers['x-vibe-token'];
344
+ if (!headerToken || headerToken !== AUTH_TOKEN) {
345
+ // eslint-disable-next-line no-console
346
+ console.log(`[VibeAgentController] Blocked unauthorized request. Source: ${req.socket.remoteAddress}`);
347
+ res.writeHead(403, { 'Content-Type': 'application/json' });
348
+ res.end(JSON.stringify({ error: 'Unauthorized: Invalid x-vibe-token' }));
349
+ return;
350
+ }
351
+ }
352
+ try {
353
+ // --- ABORT ENDPOINT ---
354
+ if (req.method === 'POST' && req.url === '/abort') {
355
+ debugLog('Received /abort request');
356
+ if (activeAbortController) {
357
+ activeAbortController.abort();
358
+ activeAbortController = null;
359
+ console.log('🛑 [Deep Abort] Cancelled active Ollama request.');
360
+ res.writeHead(200, { 'Content-Type': 'application/json' });
361
+ res.end(JSON.stringify({ status: 'aborted' }));
362
+ }
363
+ else {
364
+ res.writeHead(200, { 'Content-Type': 'application/json' });
365
+ res.end(JSON.stringify({ status: 'no_active_request' }));
366
+ }
367
+ return;
368
+ }
369
+ // ---------------------
370
+ if (req.method === 'POST' && req.url === '/agent') {
371
+ debugLog('Received /agent request');
372
+ const body = await readJsonBody(req);
373
+ if (!body || !body.mode || !body.ollama) {
374
+ res.writeHead(400, { 'Content-Type': 'application/json' });
375
+ res.end(JSON.stringify({ error: 'Missing required fields' }));
376
+ return;
377
+ }
378
+ // Reset Abort Controller for new request
379
+ if (activeAbortController) {
380
+ activeAbortController.abort(); // Kill any zombie requests
381
+ }
382
+ activeAbortController = new AbortController();
383
+ let raw;
384
+ try {
385
+ if (Array.isArray(body.messages)) {
386
+ // Proxy mode: extension sends full message history.
387
+ debugLog(`Calling Ollama (Proxy Mode) with model: ${body.ollama.model}`);
388
+ raw = await callOllamaWithMessages(body, activeAbortController.signal);
389
+ }
390
+ else {
391
+ // Structured request mode (future use).
392
+ debugLog(`Calling Ollama (Structured Mode) with model: ${body.ollama.model}`);
393
+ raw = await callOllama(body, activeAbortController.signal);
394
+ }
395
+ }
396
+ catch (err) {
397
+ if (err.name === 'AbortError') {
398
+ debugLog('Ollama request aborted.');
399
+ res.writeHead(499, { 'Content-Type': 'application/json' }); // Client Closed Request
400
+ res.end(JSON.stringify({ error: 'Aborted by user' }));
401
+ return;
402
+ }
403
+ throw err;
404
+ }
405
+ finally {
406
+ activeAbortController = null;
407
+ }
408
+ debugLog('Ollama response received');
409
+ const { replyMarkdown, actions } = parseActionsFromReply(raw, body);
410
+ const reply = {
411
+ mode: body.mode,
412
+ replyMarkdown,
413
+ actions,
414
+ usedModel: body.ollama.model
415
+ };
416
+ // Agent mode will eventually parse actions from the model output.
417
+ // For the initial version we only return markdown and let the extension
418
+ // continue to handle tool execution as today.
419
+ res.writeHead(200, { 'Content-Type': 'application/json' });
420
+ res.end(JSON.stringify(reply));
421
+ return;
422
+ }
423
+ if (req.method === 'GET' && req.url === '/health') {
424
+ res.writeHead(200, { 'Content-Type': 'application/json' });
425
+ res.end(JSON.stringify({ status: 'ok', secured: !!AUTH_TOKEN }));
426
+ return;
427
+ }
428
+ res.writeHead(404, { 'Content-Type': 'application/json' });
429
+ res.end(JSON.stringify({ error: 'Not found' }));
430
+ }
431
+ catch (error) {
432
+ res.writeHead(500, { 'Content-Type': 'application/json' });
433
+ res.end(JSON.stringify({ error: error.message || 'Internal error' }));
434
+ }
435
+ });
436
+ server.listen(DEFAULT_PORT, '127.0.0.1', () => {
437
+ // eslint-disable-next-line no-console
438
+ console.log(`[VibeAgentController] Listening on http://127.0.0.1:${DEFAULT_PORT} ${AUTH_TOKEN ? '(Secured)' : '(No Auth Token provided)'}`);
439
+ });
440
+ }
441
+ // Add simple logging helper since we are debugging
442
+ function debugLog(msg) {
443
+ const ts = new Date().toISOString();
444
+ console.log(`[${ts}] ${msg}`);
445
+ }
446
+ if (require.main === module) {
447
+ createServer();
448
+ }
449
+ //# sourceMappingURL=agent-controller.js.map