coderrr-cli
Version:
AI-powered coding agent that understands natural language requests and autonomously creates, modifies, and manages code across your projects
226 lines (194 loc) • 8.9 kB
JavaScript
/**
* Coderrr CLI - Phase 2
* - TUI chat (blessed)
* - Sends prompt to backend /chat
* - Parses structured JSON 'plan' in backend response
* - Shows plan and asks for confirmation
* - Calls executor to perform safe file ops & commands
*/
const blessed = require('blessed');
const axios = require('axios');
const { executePlan } = require('../src/executor');
const { tryExtractJSON } = require('../src/utils');
require('dotenv').config();
const BACKEND = process.env.CODERRR_BACKEND;
const TIMEOUT_MS = parseInt(process.env.TIMEOUT_MS || '120000');
// screen
const screen = blessed.screen({ smartCSR: true, title: 'Coderrr' });
// messages box
const messagesBox = blessed.box({
top: 0, left: 0, width: '100%', height: '85%-1',
tags: true, scrollable: true, alwaysScroll: true,
scrollbar: { ch: ' ', inverse: true },
keys: true, mouse: true, vi: true,
border: { type: 'line' },
style: { fg: 'white', border: { fg: '#00afff' } }
});
// status bar with improved styling
const status = blessed.box({
bottom: 3, left: 0, height: 1, width: '100%', tags: true,
style: { fg: '#aaaaaa', bg: '#1a1a1a' }
});
// Set initial status
status.setContent('{green-fg}● Ready{/green-fg} {grey-fg}|{/grey-fg} {cyan-fg}Waiting for your request...{/cyan-fg}');
// input
const input = blessed.textbox({
bottom: 0, left: 0, height: 3, width: '100%',
keys: true, mouse: true, inputOnFocus: true,
padding: { left: 1 }, style: { fg: 'white', bg: '#222222' },
border: { type: 'line', fg: '#00ff88' }
});
screen.append(messagesBox);
screen.append(status);
screen.append(input);
let conversation = [];
function appendMessage(who, text) {
const time = new Date().toLocaleTimeString();
const tag = who === 'user' ? '{bold}{green-fg}You{/}' : '{bold}{cyan-fg}Coderrr{/}';
messagesBox.pushLine(`${tag} {grey-fg}${time}{/}:`);
const lines = String(text).split('\n');
for (const line of lines) messagesBox.pushLine(' ' + line);
messagesBox.pushLine('');
messagesBox.setScrollPerc(100);
screen.render();
}
// Helper: show simple JSON plan prettily
function renderPlan(planObj) {
try {
if (!planObj || !Array.isArray(planObj.plan)) {
appendMessage('assistant', 'No structured plan found in response.');
return;
}
appendMessage('assistant', 'Structured Plan:');
planObj.plan.forEach((step, i) => {
appendMessage('assistant', `${i + 1}. ${step.action} — ${step.path || step.command || ''}`);
if (step.summary) appendMessage('assistant', ` summary: ${step.summary}`);
});
} catch (e) {
appendMessage('assistant', 'Failed to render plan: ' + String(e));
}
}
async function sendToBackend(payload) {
try {
status.setContent('{yellow-fg}● Thinking{/yellow-fg} {grey-fg}|{/grey-fg} {cyan-fg}Processing your request...{/cyan-fg}');
screen.render();
const resp = await axios.post(BACKEND + '/chat', payload, { timeout: TIMEOUT_MS });
status.setContent('{green-fg}● Ready{/green-fg} {grey-fg}|{/grey-fg} {cyan-fg}Response received{/cyan-fg}');
screen.render();
return resp.data;
} catch (err) {
status.setContent('{red-fg}● Error{/red-fg} {grey-fg}|{/grey-fg} {red-fg}Failed to connect to backend{/red-fg}');
screen.render();
if (err.response && err.response.data) {
return { error: 'Backend error', details: err.response.data };
}
return { error: 'Request error', details: err.message || String(err) };
}
}
// ask a yes/no using blessed.question
function askYesNo(question) {
return new Promise((resolve) => {
const q = blessed.question({
parent: screen,
left: 'center',
top: 'center',
width: '70%',
height: 7,
label: ' Confirm ',
border: { type: 'line' },
tags: true
});
q.ask(`${question}\n(y/n)`, (err, val) => {
q.destroy();
screen.render();
const yes = String(val || '').toLowerCase();
resolve(yes === 'y' || yes === 'yes');
});
});
}
// Display welcome banner with ASCII art
function displayWelcomeBanner() {
const banner = `
{cyan-fg}{bold}
██████╗ ██████╗ ██████╗ ███████╗██████╗ ██████╗ ██████╗
██╔════╝██╔═══██╗██╔══██╗██╔════╝██╔══██╗██╔══██╗██╔══██╗
██║ ██║ ██║██║ ██║█████╗ ██████╔╝██████╔╝██████╔╝
██║ ██║ ██║██║ ██║██╔══╝ ██╔══██╗██╔══██╗██╔══██╗
╚██████╗╚██████╔╝██████╔╝███████╗██║ ██║██║ ██║██║ ██║
╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝
{/bold}{/cyan-fg}
{magenta-fg}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━{/magenta-fg}
{yellow-fg}{bold} Your friendly neighbourhood Open Source Coding Agent{/bold}{/yellow-fg}
{magenta-fg}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━{/magenta-fg}
{yellow-fg}💡 Quick Tips:{/yellow-fg}
{grey-fg}• Type your coding request and press Enter{/grey-fg}
{grey-fg}• Use /quit or /exit to leave{/grey-fg}
{grey-fg}• Press Tab to focus input, Ctrl+C to exit{/grey-fg}
{blue-fg}🔗 Backend:{/blue-fg} {white-fg}${BACKEND}{/white-fg}
{magenta-fg}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━{/magenta-fg}
`;
const lines = banner.split('\n');
lines.forEach(line => messagesBox.pushLine(line));
messagesBox.pushLine('');
// Add welcome message
appendMessage('assistant', "I'm ready to help! Type your coding request and I'll create a plan for you.");
messagesBox.setScrollPerc(100);
}
// Display welcome banner
displayWelcomeBanner();
input.focus();
input.key('enter', async () => {
const value = input.getValue().trim();
input.clearValue();
screen.render();
if (!value) return;
if (value === '/quit' || value === '/exit') process.exit(0);
appendMessage('user', value);
conversation.push({ role: 'user', content: value });
// Send prompt + optional context (for now just prompt)
const backendResp = await sendToBackend({ prompt: value });
if (backendResp.error) {
appendMessage('assistant', `Error: ${JSON.stringify(backendResp.details)}`);
return;
}
// backend returns { response: <text> } and optionally parsed_json
const rawText = backendResp.response || '';
appendMessage('assistant', rawText);
// Try to extract JSON plan from text
const parsed = tryExtractJSON(rawText);
if (!parsed) {
appendMessage('assistant', 'Could not parse a structured JSON plan from the model response. Ask the model to output a JSON plan, or enable developer mode.');
return;
}
// show plan summary
renderPlan(parsed);
// ask user to proceed with applying the plan
const proceed = await askYesNo('Proceed to apply the above steps?');
if (!proceed) {
appendMessage('assistant', 'Operation aborted by user.');
status.setContent('{yellow-fg}● Idle{/yellow-fg} {grey-fg}|{/grey-fg} {cyan-fg}Waiting for your next request...{/cyan-fg}');
screen.render();
return;
}
// execute plan — executor streams logs back via callbacks
try {
status.setContent('{cyan-fg}● Executing{/cyan-fg} {grey-fg}|{/grey-fg} {yellow-fg}Applying changes...{/yellow-fg}');
screen.render();
await executePlan(parsed.plan, {
appendMessage,
askYesNo,
status
});
appendMessage('assistant', 'Plan execution finished.');
status.setContent('{green-fg}● Complete{/green-fg} {grey-fg}|{/grey-fg} {green-fg}All tasks finished successfully{/green-fg}');
screen.render();
} catch (e) {
appendMessage('assistant', 'Execution error: ' + String(e));
status.setContent('{red-fg}● Error{/red-fg} {grey-fg}|{/grey-fg} {red-fg}Execution failed{/red-fg}');
screen.render();
}
});
// keybindings
screen.key(['C-c'], () => process.exit(0));
screen.key(['tab'], () => input.focus());
screen.render();