hybard-agent 0.1.0
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 +41 -0
- package/bin/.cmd +0 -0
- package/bin/agent.js +673 -0
- package/bin/cli.js +326 -0
- package/bin/hybard.cmd +2 -0
- package/knowledge/BENGALI_GUIDE.md +436 -0
- package/knowledge/CAPABILITIES.md +448 -0
- package/knowledge/INDEX.md +204 -0
- package/knowledge/KNOWLEDGE_BASE.md +1174 -0
- package/knowledge/README.md +97 -0
- package/knowledge/SYSTEM_PROMPT.md +310 -0
- package/lib/agent.js +730 -0
- package/lib/analyzer.js +330 -0
- package/lib/coding-agent.js +87 -0
- package/lib/coding-model.js +585 -0
- package/lib/engine.js +591 -0
- package/lib/hybard-agent.js +1063 -0
- package/lib/main.dart +32 -0
- package/lib/models.js +357 -0
- package/lib/online.js +654 -0
- package/lib/server.js +278 -0
- package/lib/ui.js +96 -0
- package/package.json +50 -0
package/lib/online.js
ADDED
|
@@ -0,0 +1,654 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hybard AI Online Client — Connects to AI APIs (OpenAI-compatible).
|
|
3
|
+
*
|
|
4
|
+
* Supports:
|
|
5
|
+
* - OpenAI API (GPT-4, GPT-3.5)
|
|
6
|
+
* - Groq (free, fast inference — llama, mixtral)
|
|
7
|
+
* - OpenRouter (many models — Claude, Gemini, Llama, etc.)
|
|
8
|
+
* - Ollama (local models — llama3.2, codellama, etc.)
|
|
9
|
+
* - Any OpenAI-compatible endpoint (LM Studio, etc.)
|
|
10
|
+
*
|
|
11
|
+
* Provider presets (auto-configured):
|
|
12
|
+
* groq — api.groq.com/openai/v1 (free tier available)
|
|
13
|
+
* openrouter — openrouter.ai/api/v1
|
|
14
|
+
* ollama — localhost:11434/v1 (local)
|
|
15
|
+
* openai — api.openai.com/v1
|
|
16
|
+
*
|
|
17
|
+
* Usage:
|
|
18
|
+
* node -e "const {HybardAI} = require('./lib/online'); const ai = new HybardAI(); ai.chat('hello').then(console.log)"
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const https = require('https');
|
|
22
|
+
const http = require('http');
|
|
23
|
+
const fs = require('fs');
|
|
24
|
+
const path = require('path');
|
|
25
|
+
|
|
26
|
+
// Load .env file if present
|
|
27
|
+
const envPath = path.join(__dirname, '..', '.env');
|
|
28
|
+
if (fs.existsSync(envPath)) {
|
|
29
|
+
const envContent = fs.readFileSync(envPath, 'utf8');
|
|
30
|
+
for (const line of envContent.split('\n')) {
|
|
31
|
+
const trimmed = line.trim();
|
|
32
|
+
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
33
|
+
const eqIdx = trimmed.indexOf('=');
|
|
34
|
+
if (eqIdx > 0) {
|
|
35
|
+
const key = trimmed.slice(0, eqIdx).trim();
|
|
36
|
+
const val = trimmed.slice(eqIdx + 1).trim();
|
|
37
|
+
if (!process.env[key]) process.env[key] = val;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const os = require('os');
|
|
43
|
+
const CONFIG_DIR = path.join(os.homedir(), '.hybard');
|
|
44
|
+
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
|
|
45
|
+
const NETWORK_FILE = path.join(CONFIG_DIR, 'network.json');
|
|
46
|
+
|
|
47
|
+
// ─── Auto-detect local Wi-Fi/LAN IP ─────────────────────
|
|
48
|
+
function detectLocalIP() {
|
|
49
|
+
try {
|
|
50
|
+
const interfaces = os.networkInterfaces();
|
|
51
|
+
for (const name of Object.keys(interfaces)) {
|
|
52
|
+
for (const iface of interfaces[name]) {
|
|
53
|
+
if (iface.family === 'IPv4' && !iface.internal) {
|
|
54
|
+
return iface.address;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
} catch (e) {}
|
|
59
|
+
return '127.0.0.1';
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ─── Probe a URL to see if it's reachable ────────────────
|
|
63
|
+
function probeUrl(url, timeoutMs = 2000) {
|
|
64
|
+
return new Promise((resolve) => {
|
|
65
|
+
const mod = url.startsWith('https') ? https : http;
|
|
66
|
+
const req = mod.get(url, { timeout: timeoutMs }, (res) => {
|
|
67
|
+
let data = '';
|
|
68
|
+
res.on('data', (c) => (data += c));
|
|
69
|
+
res.on('end', () => resolve({ ok: true, data }));
|
|
70
|
+
});
|
|
71
|
+
req.on('error', () => resolve({ ok: false }));
|
|
72
|
+
req.on('timeout', () => { req.destroy(); resolve({ ok: false }); });
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ─── Find the Hybard server on the network ──────────────
|
|
77
|
+
async function discoverHybardServer() {
|
|
78
|
+
const localIP = detectLocalIP();
|
|
79
|
+
const candidates = [
|
|
80
|
+
`http://127.0.0.1:8000/health`,
|
|
81
|
+
`http://localhost:8000/health`,
|
|
82
|
+
`http://${localIP}:8000/health`,
|
|
83
|
+
];
|
|
84
|
+
for (const url of candidates) {
|
|
85
|
+
const result = await probeUrl(url, 1500);
|
|
86
|
+
if (result.ok) {
|
|
87
|
+
const base = url.replace('/health', '');
|
|
88
|
+
return { url: base, ip: new URL(base).hostname };
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Provider presets — auto-configure baseUrl and default model
|
|
95
|
+
const PROVIDER_PRESETS = {
|
|
96
|
+
hybard: {
|
|
97
|
+
baseUrl: 'http://localhost:8000/v1',
|
|
98
|
+
defaultModel: 'hybard-custom-llm',
|
|
99
|
+
defaultApiKey: 'hybard-local',
|
|
100
|
+
local: true,
|
|
101
|
+
},
|
|
102
|
+
groq: {
|
|
103
|
+
baseUrl: 'https://api.groq.com/openai/v1',
|
|
104
|
+
defaultModel: 'llama-3.3-70b-versatile',
|
|
105
|
+
defaultApiKey: process.env.GROQ_API_KEY || '',
|
|
106
|
+
},
|
|
107
|
+
openrouter: {
|
|
108
|
+
baseUrl: 'https://openrouter.ai/api/v1',
|
|
109
|
+
defaultModel: 'google/gemma-2-9b-it:free',
|
|
110
|
+
defaultApiKey: process.env.OPENROUTER_API_KEY || '',
|
|
111
|
+
},
|
|
112
|
+
ollama: {
|
|
113
|
+
baseUrl: 'http://localhost:11434/v1',
|
|
114
|
+
defaultModel: 'llama3.2',
|
|
115
|
+
defaultApiKey: 'ollama',
|
|
116
|
+
},
|
|
117
|
+
openai: {
|
|
118
|
+
baseUrl: 'https://api.openai.com/v1',
|
|
119
|
+
defaultModel: 'gpt-4o',
|
|
120
|
+
defaultApiKey: process.env.OPENAI_API_KEY || '',
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const DEFAULT_CONFIG = {
|
|
125
|
+
provider: 'hybard',
|
|
126
|
+
apiKey: 'hybard-local',
|
|
127
|
+
baseUrl: 'http://localhost:8000/v1',
|
|
128
|
+
model: 'hybard-custom-llm',
|
|
129
|
+
maxTokens: 2048,
|
|
130
|
+
temperature: 0.7,
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
class HybardAI {
|
|
134
|
+
constructor(options = {}) {
|
|
135
|
+
this.config = { ...DEFAULT_CONFIG, ...this._loadConfig(), ...options };
|
|
136
|
+
this.localServerUrl = null;
|
|
137
|
+
this.systemPrompt = `You are Hybard AI, an expert coding assistant. You help with:
|
|
138
|
+
- Writing code in any programming language
|
|
139
|
+
- Explaining code and concepts
|
|
140
|
+
- Debugging and fixing errors
|
|
141
|
+
- Creating project scaffolds
|
|
142
|
+
- Best practices and architecture advice
|
|
143
|
+
|
|
144
|
+
Always respond with clear, helpful code and explanations. When writing code, use proper formatting with markdown code blocks.`;
|
|
145
|
+
|
|
146
|
+
// Auto-discover local server in background
|
|
147
|
+
this._discoverLocalServer();
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async _discoverLocalServer() {
|
|
151
|
+
try {
|
|
152
|
+
const server = await discoverHybardServer();
|
|
153
|
+
if (server) {
|
|
154
|
+
this.localServerUrl = server.url;
|
|
155
|
+
PROVIDER_PRESETS.hybard.baseUrl = server.url + '/v1';
|
|
156
|
+
console.log(`[HybardAI] Local LLM server found at ${server.url}`);
|
|
157
|
+
}
|
|
158
|
+
} catch (e) {}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
_loadConfig() {
|
|
162
|
+
try {
|
|
163
|
+
if (fs.existsSync(CONFIG_FILE)) {
|
|
164
|
+
return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
|
|
165
|
+
}
|
|
166
|
+
} catch (e) {}
|
|
167
|
+
return {};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
saveConfig(updates = {}) {
|
|
171
|
+
this.config = { ...this.config, ...updates };
|
|
172
|
+
if (!fs.existsSync(CONFIG_DIR)) fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
173
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(this.config, null, 2));
|
|
174
|
+
return this.config;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
getApiKey() {
|
|
178
|
+
return this.config.apiKey || process.env.HYBARD_API_KEY || process.env.OPENAI_API_KEY || '';
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
getBaseUrl() {
|
|
182
|
+
return this.config.baseUrl || 'https://api.openai.com/v1';
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Switch to a preset provider (groq, openrouter, ollama, openai).
|
|
187
|
+
* Automatically sets baseUrl, model, and apiKey.
|
|
188
|
+
*/
|
|
189
|
+
setProvider(providerName) {
|
|
190
|
+
const preset = PROVIDER_PRESETS[providerName];
|
|
191
|
+
if (!preset) {
|
|
192
|
+
throw new Error(`Unknown provider: ${providerName}. Available: ${Object.keys(PROVIDER_PRESETS).join(', ')}`);
|
|
193
|
+
}
|
|
194
|
+
this.config.provider = providerName;
|
|
195
|
+
this.config.baseUrl = preset.baseUrl;
|
|
196
|
+
this.config.model = preset.defaultModel;
|
|
197
|
+
if (preset.defaultApiKey) {
|
|
198
|
+
this.config.apiKey = preset.defaultApiKey;
|
|
199
|
+
}
|
|
200
|
+
this.saveConfig(this.config);
|
|
201
|
+
return this.config;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* List available providers and their status.
|
|
206
|
+
*/
|
|
207
|
+
listProviders() {
|
|
208
|
+
return Object.entries(PROVIDER_PRESETS).map(([name, preset]) => ({
|
|
209
|
+
name,
|
|
210
|
+
baseUrl: preset.baseUrl,
|
|
211
|
+
model: preset.defaultModel,
|
|
212
|
+
hasApiKey: !!(preset.defaultApiKey || this.getApiKey()),
|
|
213
|
+
isCurrent: this.config.provider === name,
|
|
214
|
+
}));
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Send a chat completion request to the AI API with automatic fallback.
|
|
219
|
+
* Tries current provider first, then falls back to others on failure.
|
|
220
|
+
*/
|
|
221
|
+
async chat(message, options = {}) {
|
|
222
|
+
const apiKey = this.getApiKey();
|
|
223
|
+
if (!apiKey) {
|
|
224
|
+
return this._fallbackResponse(message);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Try current provider first
|
|
228
|
+
try {
|
|
229
|
+
return await this._chatProvider(this.config.provider, message, options);
|
|
230
|
+
} catch (err) {
|
|
231
|
+
console.log(`[Fallback] ${this.config.provider} failed: ${err.message}`);
|
|
232
|
+
|
|
233
|
+
// Try fallback providers: hybard local first, then others
|
|
234
|
+
const fallbacks = ['hybard', 'groq', 'ollama', 'openrouter', 'openai'].filter(p => p !== this.config.provider);
|
|
235
|
+
|
|
236
|
+
for (const provider of fallbacks) {
|
|
237
|
+
try {
|
|
238
|
+
const preset = PROVIDER_PRESETS[provider];
|
|
239
|
+
if (!preset) continue;
|
|
240
|
+
if (!preset.local && !preset.defaultApiKey && provider !== 'ollama') continue;
|
|
241
|
+
|
|
242
|
+
console.log(`[Fallback] Trying ${provider}...`);
|
|
243
|
+
return await this._chatProvider(provider, message, options);
|
|
244
|
+
} catch (e2) {
|
|
245
|
+
console.log(`[Fallback] ${provider} also failed: ${e2.message}`);
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
throw err;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Chat with a specific provider.
|
|
256
|
+
*/
|
|
257
|
+
async _chatProvider(provider, message, options = {}) {
|
|
258
|
+
const preset = PROVIDER_PRESETS[provider];
|
|
259
|
+
if (!preset) throw new Error(`Unknown provider: ${provider}`);
|
|
260
|
+
|
|
261
|
+
// Local providers don't need API keys
|
|
262
|
+
const isLocal = preset.local || provider === 'ollama' || provider === 'hybard';
|
|
263
|
+
const apiKey = isLocal ? (preset.defaultApiKey || 'local') : (preset.defaultApiKey || this.getApiKey());
|
|
264
|
+
|
|
265
|
+
if (!isLocal && !apiKey) {
|
|
266
|
+
throw new Error(`No API key for ${provider}`);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Use Ollama native API for better reliability
|
|
270
|
+
if (provider === 'ollama') {
|
|
271
|
+
return this._chatOllama(message, options);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const messages = [
|
|
275
|
+
{ role: 'system', content: options.system || this.systemPrompt },
|
|
276
|
+
{ role: 'user', content: message },
|
|
277
|
+
];
|
|
278
|
+
|
|
279
|
+
const body = JSON.stringify({
|
|
280
|
+
model: options.model || preset.defaultModel,
|
|
281
|
+
messages,
|
|
282
|
+
max_tokens: options.maxTokens || this.config.maxTokens,
|
|
283
|
+
temperature: options.temperature ?? this.config.temperature,
|
|
284
|
+
stream: false,
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
const baseUrl = new URL(preset.baseUrl);
|
|
288
|
+
const isHttps = baseUrl.protocol === 'https:';
|
|
289
|
+
const transport = isHttps ? https : http;
|
|
290
|
+
|
|
291
|
+
return new Promise((resolve, reject) => {
|
|
292
|
+
const req = transport.request(
|
|
293
|
+
{
|
|
294
|
+
hostname: baseUrl.hostname,
|
|
295
|
+
port: baseUrl.port || (isHttps ? 443 : 80),
|
|
296
|
+
path: `${baseUrl.pathname}/chat/completions`.replace(/\/+/g, '/'),
|
|
297
|
+
method: 'POST',
|
|
298
|
+
headers: {
|
|
299
|
+
'Content-Type': 'application/json',
|
|
300
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
301
|
+
'Content-Length': Buffer.byteLength(body),
|
|
302
|
+
},
|
|
303
|
+
timeout: 60000,
|
|
304
|
+
},
|
|
305
|
+
(res) => {
|
|
306
|
+
let data = '';
|
|
307
|
+
res.on('data', (chunk) => (data += chunk));
|
|
308
|
+
res.on('end', () => {
|
|
309
|
+
try {
|
|
310
|
+
const json = JSON.parse(data);
|
|
311
|
+
if (json.error) {
|
|
312
|
+
reject(new Error(json.error.message || JSON.stringify(json.error)));
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
const content = json.choices?.[0]?.message?.content || '';
|
|
316
|
+
resolve(content);
|
|
317
|
+
} catch (e) {
|
|
318
|
+
reject(new Error(`Failed to parse response: ${data.slice(0, 200)}`));
|
|
319
|
+
}
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
);
|
|
323
|
+
|
|
324
|
+
req.on('error', reject);
|
|
325
|
+
req.on('timeout', () => {
|
|
326
|
+
req.destroy();
|
|
327
|
+
reject(new Error('Request timed out'));
|
|
328
|
+
});
|
|
329
|
+
req.write(body);
|
|
330
|
+
req.end();
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Chat using Ollama's native /api/generate endpoint (more reliable).
|
|
336
|
+
*/
|
|
337
|
+
async _chatOllama(message, options = {}) {
|
|
338
|
+
const baseUrl = new URL(this.getBaseUrl());
|
|
339
|
+
// Ollama native endpoint: /api/generate (not /v1/chat/completions)
|
|
340
|
+
const ollamaUrl = `http://${baseUrl.hostname}:${baseUrl.port || 11434}`;
|
|
341
|
+
|
|
342
|
+
const body = JSON.stringify({
|
|
343
|
+
model: options.model || this.config.model,
|
|
344
|
+
prompt: message,
|
|
345
|
+
stream: false,
|
|
346
|
+
options: {
|
|
347
|
+
num_predict: options.maxTokens || this.config.maxTokens,
|
|
348
|
+
temperature: options.temperature ?? this.config.temperature,
|
|
349
|
+
},
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
return new Promise((resolve, reject) => {
|
|
353
|
+
const req = http.request(
|
|
354
|
+
{
|
|
355
|
+
hostname: baseUrl.hostname,
|
|
356
|
+
port: baseUrl.port || 11434,
|
|
357
|
+
path: '/api/generate',
|
|
358
|
+
method: 'POST',
|
|
359
|
+
headers: {
|
|
360
|
+
'Content-Type': 'application/json',
|
|
361
|
+
'Content-Length': Buffer.byteLength(body),
|
|
362
|
+
},
|
|
363
|
+
timeout: 300000, // 5 minutes for local model
|
|
364
|
+
},
|
|
365
|
+
(res) => {
|
|
366
|
+
let data = '';
|
|
367
|
+
res.on('data', (chunk) => (data += chunk));
|
|
368
|
+
res.on('end', () => {
|
|
369
|
+
try {
|
|
370
|
+
const json = JSON.parse(data);
|
|
371
|
+
if (json.error) {
|
|
372
|
+
reject(new Error(json.error));
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
resolve(json.response || '');
|
|
376
|
+
} catch (e) {
|
|
377
|
+
reject(new Error(`Failed to parse Ollama response: ${data.slice(0, 200)}`));
|
|
378
|
+
}
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
);
|
|
382
|
+
|
|
383
|
+
req.on('error', reject);
|
|
384
|
+
req.on('timeout', () => {
|
|
385
|
+
req.destroy();
|
|
386
|
+
reject(new Error('Ollama request timed out. The model may be loading — this can take 1-5 minutes for the first request. Try again and wait longer.'));
|
|
387
|
+
});
|
|
388
|
+
req.write(body);
|
|
389
|
+
req.end();
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Stream chat completion (yields chunks).
|
|
395
|
+
*/
|
|
396
|
+
async *chatStream(message, options = {}) {
|
|
397
|
+
const apiKey = this.getApiKey();
|
|
398
|
+
if (!apiKey) {
|
|
399
|
+
yield this._fallbackResponse(message);
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
const messages = [
|
|
404
|
+
{ role: 'system', content: options.system || this.systemPrompt },
|
|
405
|
+
{ role: 'user', content: message },
|
|
406
|
+
];
|
|
407
|
+
|
|
408
|
+
const body = JSON.stringify({
|
|
409
|
+
model: options.model || this.config.model,
|
|
410
|
+
messages,
|
|
411
|
+
max_tokens: options.maxTokens || this.config.maxTokens,
|
|
412
|
+
temperature: options.temperature ?? this.config.temperature,
|
|
413
|
+
stream: true,
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
const baseUrl = new URL(options.baseUrl || this.getBaseUrl());
|
|
417
|
+
const isHttps = baseUrl.protocol === 'https:';
|
|
418
|
+
const transport = isHttps ? https : http;
|
|
419
|
+
|
|
420
|
+
yield* new Promise((resolve, reject) => {
|
|
421
|
+
const chunks = [];
|
|
422
|
+
const req = transport.request(
|
|
423
|
+
{
|
|
424
|
+
hostname: baseUrl.hostname,
|
|
425
|
+
port: baseUrl.port || (isHttps ? 443 : 80),
|
|
426
|
+
path: `${baseUrl.pathname}/chat/completions`.replace(/\/+/g, '/'),
|
|
427
|
+
method: 'POST',
|
|
428
|
+
headers: {
|
|
429
|
+
'Content-Type': 'application/json',
|
|
430
|
+
Authorization: `Bearer ${apiKey}`,
|
|
431
|
+
},
|
|
432
|
+
},
|
|
433
|
+
(res) => {
|
|
434
|
+
let buffer = '';
|
|
435
|
+
res.on('data', (chunk) => {
|
|
436
|
+
buffer += chunk.toString();
|
|
437
|
+
const lines = buffer.split('\n');
|
|
438
|
+
buffer = lines.pop() || '';
|
|
439
|
+
for (const line of lines) {
|
|
440
|
+
if (line.startsWith('data: ')) {
|
|
441
|
+
const data = line.slice(6).trim();
|
|
442
|
+
if (data === '[DONE]') continue;
|
|
443
|
+
try {
|
|
444
|
+
const json = JSON.parse(data);
|
|
445
|
+
const delta = json.choices?.[0]?.delta?.content;
|
|
446
|
+
if (delta) chunks.push(delta);
|
|
447
|
+
} catch (e) {}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
});
|
|
451
|
+
res.on('end', () => resolve(chunks));
|
|
452
|
+
}
|
|
453
|
+
);
|
|
454
|
+
req.on('error', reject);
|
|
455
|
+
req.on('timeout', () => {
|
|
456
|
+
req.destroy();
|
|
457
|
+
reject(new Error('Request timed out'));
|
|
458
|
+
});
|
|
459
|
+
req.write(body);
|
|
460
|
+
req.end();
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
for (const chunk of chunks) yield chunk;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* Generate code from a prompt.
|
|
468
|
+
*/
|
|
469
|
+
async generateCode(prompt, language = 'python') {
|
|
470
|
+
const sysPrompt = `You are an expert ${language} programmer. Generate clean, working code.
|
|
471
|
+
Respond ONLY with the code in a markdown code block. No explanations unless asked.`;
|
|
472
|
+
return this.chat(prompt, { system: sysPrompt });
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* Explain code.
|
|
477
|
+
*/
|
|
478
|
+
async explainCode(code) {
|
|
479
|
+
const sysPrompt = `You are a code analysis expert. Explain the given code clearly and concisely.
|
|
480
|
+
Cover: what it does, how it works, key patterns, and any potential issues.`;
|
|
481
|
+
return this.chat(`Explain this code:\n\n\`\`\`\n${code}\n\`\`\``, { system: sysPrompt });
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* Fix code with an error.
|
|
486
|
+
*/
|
|
487
|
+
async fixCode(code, error = '') {
|
|
488
|
+
const sysPrompt = `You are a debugging expert. Fix the given code.
|
|
489
|
+
Return ONLY the corrected code in a markdown code block.`;
|
|
490
|
+
const msg = error
|
|
491
|
+
? `Fix this code. Error: ${error}\n\n\`\`\`\n${code}\n\`\`\``
|
|
492
|
+
: `Fix any issues in this code:\n\n\`\`\`\n${code}\n\`\`\``;
|
|
493
|
+
return this.chat(msg, { system: sysPrompt });
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* Create project scaffold.
|
|
498
|
+
*/
|
|
499
|
+
async scaffold(goal) {
|
|
500
|
+
const sysPrompt = `You are a project scaffolding expert. Create project files for the given goal.
|
|
501
|
+
Return each file in this format:
|
|
502
|
+
## filename.ext
|
|
503
|
+
\`\`\`language
|
|
504
|
+
file content here
|
|
505
|
+
\`\`\`
|
|
506
|
+
Include all necessary files (main code, config, tests, README).`;
|
|
507
|
+
return this.chat(`Create a project scaffold for: ${goal}`, { system: sysPrompt });
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* Fallback when no API key is configured.
|
|
512
|
+
*/
|
|
513
|
+
_fallbackResponse(message) {
|
|
514
|
+
return `[Hybard AI — Offline Mode]
|
|
515
|
+
|
|
516
|
+
No API key configured. To enable online AI:
|
|
517
|
+
1. hybard config set apiKey YOUR_API_KEY
|
|
518
|
+
2. hybard config set model gpt-4
|
|
519
|
+
|
|
520
|
+
Or set environment variable:
|
|
521
|
+
set HYBARD_API_KEY=your-key
|
|
522
|
+
|
|
523
|
+
Current config:
|
|
524
|
+
Provider: ${this.config.provider}
|
|
525
|
+
Model: ${this.config.model}
|
|
526
|
+
Base URL: ${this.config.baseUrl}
|
|
527
|
+
|
|
528
|
+
You can still use offline features:
|
|
529
|
+
hybard create <project> — scaffold projects
|
|
530
|
+
hybard fix <file> — repair syntax errors
|
|
531
|
+
hybard inspect — scan workspace
|
|
532
|
+
hybard help — show all commands
|
|
533
|
+
|
|
534
|
+
Message received: "${message.slice(0, 100)}"`;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* Interactive chat REPL with streaming output.
|
|
540
|
+
*/
|
|
541
|
+
async function startChat(options = {}) {
|
|
542
|
+
const readline = require('readline');
|
|
543
|
+
const ai = new HybardAI(options);
|
|
544
|
+
|
|
545
|
+
console.log('\n╔════════════════════════════════════════════════════════════╗');
|
|
546
|
+
console.log('║ Hybard AI — Online Coding Assistant ║');
|
|
547
|
+
console.log('║ ║');
|
|
548
|
+
console.log('║ Commands: ║');
|
|
549
|
+
console.log('║ /config — Show/set API configuration ║');
|
|
550
|
+
console.log('║ /model — Change AI model ║');
|
|
551
|
+
console.log('║ /clear — Clear chat history ║');
|
|
552
|
+
console.log('║ /help — Show help ║');
|
|
553
|
+
console.log('║ exit — Exit ║');
|
|
554
|
+
console.log('╚════════════════════════════════════════════════════════════╝\n');
|
|
555
|
+
|
|
556
|
+
if (!ai.getApiKey()) {
|
|
557
|
+
console.log('⚠ No API key detected. Running in offline mode.');
|
|
558
|
+
console.log(' Set your key: hybard config set apiKey YOUR_KEY\n');
|
|
559
|
+
} else {
|
|
560
|
+
console.log(`✓ Connected to ${ai.config.provider} (${ai.config.model})\n`);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
const rl = readline.createInterface({
|
|
564
|
+
input: process.stdin,
|
|
565
|
+
output: process.stdout,
|
|
566
|
+
prompt: 'You > ',
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
rl.prompt();
|
|
570
|
+
|
|
571
|
+
rl.on('line', async (line) => {
|
|
572
|
+
const text = line.trim();
|
|
573
|
+
if (!text) { rl.prompt(); return; }
|
|
574
|
+
|
|
575
|
+
if (text.toLowerCase() === 'exit' || text.toLowerCase() === 'quit') {
|
|
576
|
+
console.log('\nGoodbye! 👋\n');
|
|
577
|
+
rl.close();
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
if (text === '/config') {
|
|
582
|
+
console.log('\nCurrent configuration:');
|
|
583
|
+
console.log(` Provider: ${ai.config.provider}`);
|
|
584
|
+
console.log(` Model: ${ai.config.model}`);
|
|
585
|
+
console.log(` Base URL: ${ai.config.baseUrl}`);
|
|
586
|
+
console.log(` API Key: ${ai.getApiKey() ? '***' + ai.getApiKey().slice(-4) : '(not set)'}`);
|
|
587
|
+
console.log(` Max Tokens: ${ai.config.maxTokens}`);
|
|
588
|
+
console.log(` Temperature: ${ai.config.temperature}`);
|
|
589
|
+
console.log('\nSet a value: /config set <key> <value>');
|
|
590
|
+
rl.prompt();
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
if (text.startsWith('/config set ')) {
|
|
595
|
+
const parts = text.slice(12).split(' ');
|
|
596
|
+
const key = parts[0];
|
|
597
|
+
const value = parts.slice(1).join(' ');
|
|
598
|
+
if (key && value) {
|
|
599
|
+
const numVal = parseFloat(value);
|
|
600
|
+
ai.saveConfig({ [key]: isNaN(numVal) ? value : numVal });
|
|
601
|
+
console.log(`✓ Set ${key} = ${value}`);
|
|
602
|
+
} else {
|
|
603
|
+
console.log('Usage: /config set <key> <value>');
|
|
604
|
+
}
|
|
605
|
+
rl.prompt();
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
if (text === '/model' || text.startsWith('/model ')) {
|
|
610
|
+
if (text.startsWith('/model ')) {
|
|
611
|
+
const model = text.slice(7).trim();
|
|
612
|
+
ai.saveConfig({ model });
|
|
613
|
+
console.log(`✓ Model changed to: ${model}`);
|
|
614
|
+
} else {
|
|
615
|
+
console.log(`Current model: ${ai.config.model}`);
|
|
616
|
+
console.log('Change: /model gpt-4, /model gpt-3.5-turbo, /model claude-3-opus');
|
|
617
|
+
}
|
|
618
|
+
rl.prompt();
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
if (text === '/clear') {
|
|
623
|
+
console.clear();
|
|
624
|
+
rl.prompt();
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
if (text === '/help') {
|
|
629
|
+
console.log('\nHybard AI Chat Commands:');
|
|
630
|
+
console.log(' /config — Show configuration');
|
|
631
|
+
console.log(' /config set X Y — Set a config value');
|
|
632
|
+
console.log(' /model <name> — Change AI model');
|
|
633
|
+
console.log(' /clear — Clear screen');
|
|
634
|
+
console.log(' exit — Exit chat');
|
|
635
|
+
console.log('\nJust type your coding question or request!\n');
|
|
636
|
+
rl.prompt();
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// Normal message — send to AI
|
|
641
|
+
process.stdout.write('\nHybard > ');
|
|
642
|
+
try {
|
|
643
|
+
const response = await ai.chat(text);
|
|
644
|
+
console.log('\n' + response + '\n');
|
|
645
|
+
} catch (err) {
|
|
646
|
+
console.error(`\n[Error] ${err.message}\n`);
|
|
647
|
+
}
|
|
648
|
+
rl.prompt();
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
rl.on('close', () => process.exit(0));
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
module.exports = { HybardAI, startChat, discoverHybardServer, detectLocalIP };
|