diff --git a/index.html b/index.html new file mode 100644 index 0000000..e25248f --- /dev/null +++ b/index.html @@ -0,0 +1,455 @@ + + + + + + Codex Agent Runner — Powered by Ollama + + + + + +
+

⚡ Codex Agent Runner

+ checking… + All requests → Ollama (local) +
+ + +
+ + + + +
+ + +
+
+ Type a message. Prefix with @ollama, @copilot, + @lucidia, or @blackboxprogramming — all route to your local Ollama. +
+
+ + +
+ Try: @ollama explain quantum entanglement  ·  + @copilot write a Python hello world  ·  + @blackboxprogramming list sorting algorithms  ·  Shift+Enter for new line +
+
+ + +
+ + + + + diff --git a/ollama.js b/ollama.js new file mode 100644 index 0000000..374ab27 --- /dev/null +++ b/ollama.js @@ -0,0 +1,97 @@ +/** + * Ollama Client — all agent handles route here. + * + * Supported handles (case-insensitive): + * @ollama, @copilot, @lucidia, @blackboxprogramming + * + * No external AI provider is used. Every request goes directly to the + * local Ollama HTTP API (default: http://localhost:11434). + */ + +const OLLAMA_HANDLES = ['ollama', 'copilot', 'lucidia', 'blackboxprogramming']; + +/** + * Strip leading @handle from the user message and return the clean prompt. + * If no recognised handle is found the original text is returned unchanged. + * + * @param {string} text + * @returns {{ handle: string|null, prompt: string }} + */ +function parseHandle(text) { + const trimmed = text.trim(); + const match = trimmed.match(/^@([\w.]+)\s*/i); + if (match) { + const handle = match[1].replace(/\.$/, '').toLowerCase(); + if (OLLAMA_HANDLES.includes(handle)) { + return { handle, prompt: trimmed.slice(match[0].length) }; + } + } + return { handle: null, prompt: trimmed }; +} + +/** + * Send a chat message to the local Ollama instance and stream the response. + * + * @param {object} options + * @param {string} options.baseUrl - Ollama base URL (default: http://localhost:11434) + * @param {string} options.model - Model name (default: "llama3") + * @param {Array} options.messages - OpenAI-style message array + * @param {Function} options.onChunk - Called with each streamed text chunk + * @param {Function} options.onDone - Called when the stream is complete + * @param {Function} options.onError - Called with Error on failure + * @returns {Promise} + */ +async function ollamaChat({ baseUrl = 'http://localhost:11434', model = 'llama3', messages, onChunk, onDone, onError }) { + const url = `${baseUrl}/api/chat`; + let response; + try { + response = await fetch(url, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ model, messages, stream: true }), + }); + } catch (err) { + onError(new Error(`Cannot reach Ollama at ${baseUrl}. Is it running? (${err.message})`)); + return; + } + + if (!response.ok) { + onError(new Error(`Ollama returned HTTP ${response.status}: ${await response.text()}`)); + return; + } + + const reader = response.body.getReader(); + const decoder = new TextDecoder(); + let buffer = ''; + + try { + while (true) { + const { value, done } = await reader.read(); + if (done) break; + buffer += decoder.decode(value, { stream: true }); + const lines = buffer.split('\n'); + buffer = lines.pop(); // keep incomplete line + for (const line of lines) { + if (!line.trim()) continue; + let parsed; + try { parsed = JSON.parse(line); } catch (parseErr) { + console.debug('ollama: skipping non-JSON line:', parseErr.message, line); + continue; + } + if (parsed.message?.content) onChunk(parsed.message.content); + if (parsed.done) { onDone(); return; } + } + } + if (buffer.trim()) { + try { + const parsed = JSON.parse(buffer); + if (parsed.message?.content) onChunk(parsed.message.content); + } catch { /* ignore */ } + } + onDone(); + } catch (err) { + onError(new Error(`Stream error: ${err.message}`)); + } +} + +export { OLLAMA_HANDLES, parseHandle, ollamaChat }; diff --git a/ollama.test.js b/ollama.test.js new file mode 100644 index 0000000..5d1babb --- /dev/null +++ b/ollama.test.js @@ -0,0 +1,105 @@ +/** + * Tests for ollama.js – parseHandle() + * Run with: node --experimental-vm-modules ollama.test.js + * (No test framework needed – uses Node's built-in assert) + */ +import assert from 'node:assert/strict'; +import { OLLAMA_HANDLES, parseHandle } from './ollama.js'; + +let passed = 0; +let failed = 0; + +function test(name, fn) { + try { + fn(); + console.log(` ✅ ${name}`); + passed++; + } catch (err) { + console.error(` ❌ ${name}\n ${err.message}`); + failed++; + } +} + +// ── OLLAMA_HANDLES ──────────────────────────────────────────────────────────── +test('OLLAMA_HANDLES contains ollama', () => assert.ok(OLLAMA_HANDLES.includes('ollama'))); +test('OLLAMA_HANDLES contains copilot', () => assert.ok(OLLAMA_HANDLES.includes('copilot'))); +test('OLLAMA_HANDLES contains lucidia', () => assert.ok(OLLAMA_HANDLES.includes('lucidia'))); +test('OLLAMA_HANDLES contains blackboxprogramming', () => assert.ok(OLLAMA_HANDLES.includes('blackboxprogramming'))); + +// ── parseHandle – recognised handles ───────────────────────────────────────── +test('@ollama strips handle', () => { + const r = parseHandle('@ollama tell me a joke'); + assert.equal(r.handle, 'ollama'); + assert.equal(r.prompt, 'tell me a joke'); +}); + +test('@copilot strips handle', () => { + const r = parseHandle('@copilot write a function'); + assert.equal(r.handle, 'copilot'); + assert.equal(r.prompt, 'write a function'); +}); + +test('@lucidia strips handle', () => { + const r = parseHandle('@lucidia summarise this'); + assert.equal(r.handle, 'lucidia'); + assert.equal(r.prompt, 'summarise this'); +}); + +test('@blackboxprogramming strips handle', () => { + const r = parseHandle('@blackboxprogramming list algorithms'); + assert.equal(r.handle, 'blackboxprogramming'); + assert.equal(r.prompt, 'list algorithms'); +}); + +// ── Trailing dot variants (@copilot. / @blackboxprogramming.) ──────────────── +test('@copilot. trailing dot is stripped', () => { + const r = parseHandle('@copilot. hello'); + assert.equal(r.handle, 'copilot'); + assert.equal(r.prompt, 'hello'); +}); + +test('@blackboxprogramming. trailing dot is stripped', () => { + const r = parseHandle('@blackboxprogramming. sort this list'); + assert.equal(r.handle, 'blackboxprogramming'); + assert.equal(r.prompt, 'sort this list'); +}); + +// ── Case-insensitive ────────────────────────────────────────────────────────── +test('@OLLAMA is case-insensitive', () => { + const r = parseHandle('@OLLAMA hello'); + assert.equal(r.handle, 'ollama'); +}); + +test('@Copilot is case-insensitive', () => { + const r = parseHandle('@Copilot hello'); + assert.equal(r.handle, 'copilot'); +}); + +// ── Unknown / no handle ─────────────────────────────────────────────────────── +test('unknown handle returns null handle', () => { + const r = parseHandle('@gpt4 hello'); + assert.equal(r.handle, null); + assert.equal(r.prompt, '@gpt4 hello'); +}); + +test('no handle returns null handle', () => { + const r = parseHandle('plain message'); + assert.equal(r.handle, null); + assert.equal(r.prompt, 'plain message'); +}); + +test('empty string returns null handle', () => { + const r = parseHandle(''); + assert.equal(r.handle, null); + assert.equal(r.prompt, ''); +}); + +test('@ollama with no prompt returns empty string', () => { + const r = parseHandle('@ollama'); + assert.equal(r.handle, 'ollama'); + assert.equal(r.prompt, ''); +}); + +// ── Summary ─────────────────────────────────────────────────────────────────── +console.log(`\nResults: ${passed} passed, ${failed} failed`); +if (failed > 0) process.exit(1);