+ 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);