diff --git a/README.md b/README.md index 423d756..607075f 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@

- Regolith + Regolith

diff --git a/assets/logo.jpeg b/assets/logo.jpeg new file mode 100644 index 0000000..3dab15a Binary files /dev/null and b/assets/logo.jpeg differ diff --git a/src/config.ts b/src/config.ts index 219b0c9..5f91a6a 100644 --- a/src/config.ts +++ b/src/config.ts @@ -10,6 +10,7 @@ const envConfig = readEnvFile([ 'ASSISTANT_HAS_OWN_NUMBER', 'DISCORD_BOT_TOKEN', 'DISCORD_ONLY', + 'AGENT_BACKEND', ]); export const ASSISTANT_NAME = @@ -20,6 +21,11 @@ export const DISCORD_BOT_TOKEN = process.env.DISCORD_BOT_TOKEN || envConfig.DISCORD_BOT_TOKEN || ''; export const DISCORD_ONLY = (process.env.DISCORD_ONLY || envConfig.DISCORD_ONLY) === 'true'; + +// Agent backend: 'container' (Claude Agent SDK in containers) or 'opencode' (OpenCode CLI/SDK) +export const AGENT_BACKEND = + (process.env.AGENT_BACKEND || envConfig.AGENT_BACKEND || 'container').toLowerCase() as 'container' | 'opencode'; + export const POLL_INTERVAL = 2000; export const SCHEDULER_POLL_INTERVAL = 60000; diff --git a/src/index.ts b/src/index.ts index d61b832..99d7e53 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,6 +3,7 @@ import fs from 'fs'; import path from 'path'; import { + AGENT_BACKEND, ASSISTANT_NAME, DATA_DIR, DISCORD_BOT_TOKEN, @@ -16,7 +17,107 @@ import { DiscordChannel } from './channels/discord.js'; import { WhatsAppChannel } from './channels/whatsapp.js'; import { ContainerOutput, - runContainerAgent, + async function runAgent( + group: RegisteredGroup, + prompt: string, + chatJid: string, + onOutput?: (output: ContainerOutput) => Promise, + ): Promise<'success' | 'error'> { + // --- OpenCode backend --- + if (opencode) { + try { + const response = await opencode.chat(prompt, chatJid); + if (response.error) { + logger.error({ group: group.name, error: response.error }, 'OpenCode agent error'); + if (onOutput) { + await onOutput({ status: 'error', error: response.error, result: null, newSessionId: response.sessionId }); + } + return 'error'; + } + if (onOutput && response.text) { + await onOutput({ status: 'success', result: response.text, error: null, newSessionId: response.sessionId }); + } + return 'success'; + } catch (err) { + logger.error({ group: group.name, err }, 'OpenCode agent error'); + return 'error'; + } + } + + // --- Container backend (Claude Agent SDK) --- + const isMain = group.folder === MAIN_GROUP_FOLDER; + const sessionId = sessions[group.folder]; + + // Update tasks snapshot for container to read (filtered by group) + const tasks = getAllTasks(); + writeTasksSnapshot( + group.folder, + isMain, + tasks.map((t) => ({ + id: t.id, + groupFolder: t.group_folder, + prompt: t.prompt, + schedule_type: t.schedule_type, + schedule_value: t.schedule_value, + status: t.status, + next_run: t.next_run, + })), + ); + + // Update available groups snapshot (main group only can see all groups) + const availableGroups = getAvailableGroups(); + writeGroupsSnapshot( + group.folder, + isMain, + availableGroups, + new Set(Object.keys(registeredGroups)), + ); + + // Wrap onOutput to track session ID from streamed results + const wrappedOnOutput = onOutput + ? async (output: ContainerOutput) => { + if (output.newSessionId) { + sessions[group.folder] = output.newSessionId; + setSession(group.folder, output.newSessionId); + } + await onOutput(output); + } + : undefined; + + try { + const output = await runContainerAgent( + group, + { + prompt, + sessionId, + groupFolder: group.folder, + chatJid, + isMain, + }, + (proc, containerName) => queue.registerProcess(chatJid, proc, containerName, group.folder), + wrappedOnOutput, + ); + + if (output.newSessionId) { + sessions[group.folder] = output.newSessionId; + setSession(group.folder, output.newSessionId); + } + + if (output.status === 'error') { + logger.error( + { group: group.name, error: output.error }, + 'Container agent error', + ); + return 'error'; + } + + return 'success'; + } catch (err) { + logger.error({ group: group.name, err }, 'Agent error'); + return 'error'; + } + } +, writeGroupsSnapshot, writeTasksSnapshot, } from './container-runner.js'; @@ -41,6 +142,7 @@ import { findChannel, formatMessages, formatOutbound } from './router.js'; import { startSchedulerLoop } from './task-scheduler.js'; import { Channel, NewMessage, RegisteredGroup } from './types.js'; import { logger } from './logger.js'; +import { OpenCodeRuntime } from './opencode/runtime.js'; // Re-export for backwards compatibility during refactor export { escapeXml, formatMessages } from './router.js'; @@ -54,6 +156,7 @@ let messageLoopRunning = false; let whatsapp: WhatsAppChannel; const channels: Channel[] = []; const queue = new GroupQueue(); +const opencode = AGENT_BACKEND === 'opencode' ? new OpenCodeRuntime() : null; function loadState(): void { lastTimestamp = getRouterState('last_timestamp') || ''; @@ -221,6 +324,28 @@ async function runAgent( chatJid: string, onOutput?: (output: ContainerOutput) => Promise, ): Promise<'success' | 'error'> { + // --- OpenCode backend --- + if (opencode) { + try { + const response = await opencode.chat(prompt, chatJid); + if (response.error) { + logger.error({ group: group.name, error: response.error }, 'OpenCode agent error'); + if (onOutput) { + await onOutput({ status: 'error', error: response.error, result: null, newSessionId: response.sessionId }); + } + return 'error'; + } + if (onOutput && response.text) { + await onOutput({ status: 'success', result: response.text, error: null, newSessionId: response.sessionId }); + } + return 'success'; + } catch (err) { + logger.error({ group: group.name, err }, 'OpenCode agent error'); + return 'error'; + } + } + + // --- Container backend (Claude Agent SDK) --- const isMain = group.folder === MAIN_GROUP_FOLDER; const sessionId = sessions[group.folder]; @@ -464,7 +589,11 @@ function ensureContainerSystemRunning(): void { } async function main(): Promise { - ensureContainerSystemRunning(); + if (AGENT_BACKEND === 'container') { + ensureContainerSystemRunning(); + } else { + logger.info({ backend: AGENT_BACKEND }, 'Using OpenCode agent backend, skipping container system check'); + } initDatabase(); logger.info('Database initialized'); loadState(); diff --git a/src/opencode/test-opencode.ts b/src/opencode/test-opencode.ts new file mode 100644 index 0000000..cc9c741 --- /dev/null +++ b/src/opencode/test-opencode.ts @@ -0,0 +1,31 @@ +import { OpenCodeRuntime } from './runtime.js'; + +async function main() { + const runtime = new OpenCodeRuntime({ + mode: 'cli', + timeoutMs: 30_000, + }); + + console.log('Status:', runtime.getStatus()); + + // Test 1: Empty message rejection + const empty = await runtime.chat(' '); + console.log('\nEmpty message test:', empty); + + // Test 2: Actual chat (requires opencode binary installed) + try { + const response = await runtime.chat('Say hello in one sentence.', 'test-conv-1'); + console.log('\nChat response:', response); + } catch (err) { + console.log('\nChat failed (opencode binary probably not installed):', err); + } + + // Test 3: Session status after chat + console.log('\nStatus after chat:', runtime.getStatus()); + + // Cleanup + runtime.closeSession('test-conv-1'); + console.log('\nSession closed. Final status:', runtime.getStatus()); +} + +main().catch(console.error);