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 +21 -0
- package/README.md +140 -0
- package/bin/vibe-agent.js +36 -0
- package/out/agent-controller.js +449 -0
- package/out/agent-controller.js.map +1 -0
- package/out/extension.js +1037 -0
- package/out/extension.js.map +1 -0
- package/out/test/agent-controller.test.js +103 -0
- package/out/test/agent-controller.test.js.map +1 -0
- package/out/test/eval-runner.js +163 -0
- package/out/test/eval-runner.js.map +1 -0
- package/out/test/gold_standard_harness.js +23 -0
- package/out/test/gold_standard_harness.js.map +1 -0
- package/out/test/runTest.js +22 -0
- package/out/test/runTest.js.map +1 -0
- package/out/test/suite/extension.test.js +20 -0
- package/out/test/suite/extension.test.js.map +1 -0
- package/out/test/suite/index.js +38 -0
- package/out/test/suite/index.js.map +1 -0
- package/out/test-connection.js +32 -0
- package/out/test-connection.js.map +1 -0
- package/package.json +168 -0
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
|