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/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 };