diff --git a/Makefile b/Makefile index e3b9037..2e837ee 100755 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ all: build build_rpylib run build_mingw # Variables for compiler and flags CC = gcc -CFLAGS = -Ofast -Werror -Wall -lreadline -lncurses -lcurl -ljson-c -lsqlite3 -lm $(pkg-config --cflags --libs gnutls gmp) -lssl -lcrypto +CFLAGS = -Ofast -Werror -Wall -lreadline -lncurses -lcurl -ljson-c -lsqlite3 -lm -lpthread $(pkg-config --cflags --libs gnutls gmp) -lssl -lcrypto # MinGW Variables MINGW_CC = x86_64-w64-mingw32-gcc # Change to x86_64-w64-mingw32-gcc for 64-bit diff --git a/README.md b/README.md index ce00f4f..0717905 100755 --- a/README.md +++ b/README.md @@ -1,36 +1,292 @@ -# Henry IRC Server +# R -A simple IRC server implemented in Python as a package named `henry`. +Author: retoor + +R is a command-line AI assistant written in C that provides an autonomous agent with full system access through function calling. It interfaces with OpenAI-compatible APIs and operates in an agentic loop, reasoning about tasks, executing tools, and iterating until goals are achieved. ## Features -- Basic IRC commands: NICK, USER, JOIN, PRIVMSG, QUIT -- Supports multiple clients and channels + +### Autonomous Agent Architecture +- ReAct (Reasoning + Acting) pattern implementation +- Automatic tool execution loop with up to 300 iterations +- Intelligent response parsing to detect incomplete work +- Automatic continuation when tasks require multiple steps +- Built-in retry logic for API failures + +### AI Provider Support +- OpenAI API (default) +- Anthropic Claude +- Ollama (self-hosted) +- Grok +- Any OpenAI-compatible API endpoint + +### Tool System +The agent has access to 15+ tools exposed via JSON schema descriptions: + +| Tool | Description | +|------|-------------| +| `linux_terminal_execute` | Execute shell commands with output capture | +| `linux_terminal_execute_interactive` | Execute interactive commands (vim, htop, etc.) | +| `read_file` | Read file contents | +| `write_file` | Write content to files with automatic parent directory creation | +| `directory_glob` | List directory contents with file metadata | +| `mkdir` | Create directories recursively | +| `chdir` | Change working directory | +| `getpwd` | Get current working directory | +| `http_fetch` | Fetch URL contents via HTTP GET | +| `web_search` | Search the web for information | +| `web_search_news` | Search for news articles | +| `db_set` | Store key-value pairs in local SQLite database | +| `db_get` | Retrieve values from database | +| `db_query` | Execute arbitrary SQL queries | +| `index_source_directory` | Index source files with contents | +| `python_execute` | Execute Python code and capture output | + +### Database +- Local SQLite database at `~/.r.db` +- Persistent key-value storage +- Full SQL query support +- File version history tracking + +### REPL Interface +- Readline integration with tab completion +- Persistent command history at `~/.r_history` +- Markdown syntax highlighting in terminal output +- Session management for separate conversation contexts + +### REPL Commands +| Command | Description | +|---------|-------------| +| `!dump` | Show raw message JSON | +| `!clear` | Clear conversation history | +| `!session` | Display current session info | +| `!verbose` | Toggle verbose mode | +| `!tools` | List available tools | +| `!models` | Show available models | +| `!model ` | Switch AI model | +| `exit` | Exit the REPL | ## Installation -Clone the repository or copy the `henry` package directory. +### Dependencies +``` +libcurl json-c readline ncurses sqlite3 gnutls gmp openssl +``` + +On Debian/Ubuntu: +```bash +apt install libcurl4-openssl-dev libjson-c-dev libreadline-dev libncurses-dev libsqlite3-dev libgnutls28-dev libgmp-dev libssl-dev +``` + +### Build +```bash +make build +``` + +The binary is output to `bin/r` and copied to `./r`. + +### Cross-Compilation +```bash +make build_mingw # Windows executable +``` + +### AppImage +```bash +make appimage # Creates portable r-x86_64.AppImage +``` + +## Configuration + +### Environment Variables + +| Variable | Description | Example | +|----------|-------------|---------| +| `R_KEY` | API key (takes precedence over OPENAI_API_KEY) | `sk-...` | +| `OPENAI_API_KEY` | OpenAI API key (fallback) | `sk-...` | +| `OPENROUTER_API_KEY` | OpenRouter API key | `sk-or-...` | +| `R_BASE_URL` | Custom API base URL (without /v1/...) | `https://openrouter.ai/api` | +| `R_MODEL` | AI model name | `gpt-4o`, `x-ai/grok-code-fast-1` | +| `R_USE_TOOLS` | Enable/disable function calling | `true`, `false`, `1`, `0` | +| `R_USE_STRICT` | Enable strict mode for tool schemas | `true`, `false`, `1`, `0` | +| `R_SYSTEM_MESSAGE` | Custom system prompt injected at startup | Any text | +| `R_SESSION` | Default session name for conversation persistence | `myproject`, `$(date)` | + +### Context Files +- `.rcontext.txt` - Project-specific context (current directory) +- `~/.rcontext.txt` - Global user context + +## Bash Integration + +Integrate R as a fallback command handler in bash. When a command is not found, R will attempt to interpret it as a natural language query with automatic date-based sessions. + +Add to `~/.bashrc`: + +```bash +export R_BASE_URL="https://openrouter.ai/api" +export R_KEY="$OPENROUTER_API_KEY" +export R_MODEL="x-ai/grok-code-fast-1" +export R_SESSION=$(date +%Y-%m-%d) + +command_not_found_handle() { + script_path="/path/to/r" + first_argument="$1" + if "$script_path" "$@"; then + echo -e "" + else + echo "bash: $first_argument: command not found" + fi +} +``` + +This enables: +- Automatic session per day via `R_SESSION=$(date +%Y-%m-%d)` +- Natural language commands directly in terminal +- Fallback to standard "command not found" on failure + +## Why Terminal AI + +Having an AI agent directly in your terminal eliminates context switching. Instead of copying error messages to a browser, describing your environment, or explaining file contents, the agent can see and act on everything directly. + +### Practical Examples + +**Debug errors instantly** +```bash +./r "The build failed, check the error and fix it" +# Agent reads error output, identifies the issue, edits the file, rebuilds +``` + +**Explore unfamiliar codebases** +```bash +./r "How does authentication work in this project?" +# Agent searches files, reads relevant code, explains the flow +``` + +**Automate repetitive tasks** +```bash +./r "Rename all .jpeg files to .jpg and create a backup folder first" +# Agent creates directory, moves files with proper naming +``` + +**System administration** +```bash +./r "Check disk usage and find the largest files in /var/log" +# Agent runs du, finds large files, presents a summary +``` + +**Database operations** +```bash +./r "Show me all users who signed up last week" +# Agent queries the database, formats results +``` + +**Web research while coding** +```bash +./r "What's the correct syntax for a PostgreSQL upsert?" +# Agent searches the web, returns the answer with examples +``` + +**Code generation** +```bash +./r "Write a Python script that monitors CPU usage and alerts if above 90%" +# Agent writes the script, saves it, makes it executable +``` + +**Git workflows** +```bash +./r "Show what changed since yesterday and summarize the commits" +# Agent runs git log, git diff, provides summary +``` + +**File operations** +```bash +./r "Find all TODO comments in this project and list them by file" +# Agent greps through codebase, organizes findings +``` + +**Data processing** +```bash +cat data.csv | ./r --stdin "Calculate the average of the third column" +# Agent processes piped data, returns calculation +``` + +**Multi-step tasks** +```bash +./r "Set up a new Python virtualenv, install requests and pytest, create a basic test file" +# Agent executes multiple commands in sequence, creates files +``` + +**Environment inspection** +```bash +./r "What ports are in use and what processes are using them?" +# Agent runs netstat/lsof, explains the output +``` + +The agent maintains conversation context within sessions, so follow-up queries understand previous actions. Combined with the bash integration, any unrecognized command becomes a natural language query to the AI. ## Usage -Run the IRC server using the CLI entry point: - +### Interactive Mode ```bash -python -m henry.cli +./r ``` -The server listens on `127.0.0.1:6667` by default. - -## Connecting - -Use any IRC client to connect to `localhost` on port `6667`. - -## Example - +### Single Query ```bash -# Using irssi client -irssi -c 127.0.0.1 -p 6667 +./r "What files are in the current directory?" ``` +### With Piped Input +```bash +cat error.log | ./r --stdin "Analyze these errors" +``` + +### With Context File +```bash +./r --context project_docs.txt "Explain this project" +``` + +### Session Management +```bash +./r --session=myproject # Named session +./r -s myproject # Short form +``` + +### Options +| Flag | Description | +|------|-------------| +| `--verbose` | Enable verbose output | +| `--stdin` | Read prompt from stdin | +| `--context ` | Include file as context | +| `--py ` | Include Python file | +| `--session ` | Use named session | +| `--nh` | Disable syntax highlighting | +| `--free` | Use free tier API | +| `--api` | API mode | + +## Architecture + +### Header-Only Design +The codebase uses a header-only architecture where `.h` files contain both declarations and implementations. All code compiles through includes from `main.c`. + +### Core Components +| File | Purpose | +|------|---------| +| `main.c` | Entry point, REPL loop, argument parsing | +| `agent.h` | Autonomous agent loop with ReAct pattern | +| `r.h` | Global configuration and environment handling | +| `auth.h` | API key resolution | +| `openai.h` | API communication | +| `chat.h` | JSON prompt construction | +| `messages.h` | Conversation history management | +| `tools.h` | Tool definitions and execution dispatcher | +| `http_curl.h` | libcurl wrapper for HTTP requests | +| `line.h` | Readline integration | +| `db_utils.h` | SQLite operations | +| `browse.h` | Web search functionality | +| `markdown.h` | Terminal markdown rendering | +| `utils.h` | Path utilities | +| `indexer.h` | Source directory indexing | + ## License MIT diff --git a/http_curl.h b/http_curl.h index be2e4ee..3ef7eed 100644 --- a/http_curl.h +++ b/http_curl.h @@ -29,10 +29,12 @@ #include "auth.h" #include +#include #include #include #include #include +#include #include struct ResponseBuffer { @@ -65,11 +67,40 @@ static size_t WriteCallback(void *contents, size_t size, size_t nmemb, #define HTTP_MAX_RETRIES 3 #define HTTP_RETRY_DELAY_MS 2000 +static struct timespec spinner_start_time = {0, 0}; +static volatile int spinner_running = 0; + +static double get_elapsed_seconds() { + struct timespec now; + clock_gettime(CLOCK_MONOTONIC, &now); + return (now.tv_sec - spinner_start_time.tv_sec) + + (now.tv_nsec - spinner_start_time.tv_nsec) / 1e9; +} + +static void *spinner_thread(void *arg) { + (void)arg; + const char *frames[] = {"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"}; + int frame = 0; + while (spinner_running) { + double elapsed = get_elapsed_seconds(); + fprintf(stderr, "\r%s Querying AI... (%.1fs) ", frames[frame % 10], elapsed); + fflush(stderr); + frame++; + usleep(80000); + } + return NULL; +} + char *curl_post(const char *url, const char *data) { CURL *curl; CURLcode res; struct ResponseBuffer response = {NULL, 0}; int retry_count = 0; + pthread_t spinner_tid; + + clock_gettime(CLOCK_MONOTONIC, &spinner_start_time); + spinner_running = 1; + pthread_create(&spinner_tid, NULL, spinner_thread, NULL); while (retry_count < HTTP_MAX_RETRIES) { if (response.data) { @@ -78,6 +109,8 @@ char *curl_post(const char *url, const char *data) { response.data = malloc(1); response.size = 0; if (!response.data) { + spinner_running = 0; + pthread_join(spinner_tid, NULL); return NULL; } response.data[0] = '\0'; @@ -85,6 +118,8 @@ char *curl_post(const char *url, const char *data) { curl = curl_easy_init(); if (!curl) { free(response.data); + spinner_running = 0; + pthread_join(spinner_tid, NULL); return NULL; } @@ -107,16 +142,26 @@ char *curl_post(const char *url, const char *data) { curl_easy_cleanup(curl); if (res == CURLE_OK) { + spinner_running = 0; + pthread_join(spinner_tid, NULL); + fprintf(stderr, "\r \r"); + fflush(stderr); return response.data; } retry_count++; + spinner_running = 0; + pthread_join(spinner_tid, NULL); + fprintf(stderr, "\r \r"); fprintf(stderr, "Network error: %s (attempt %d/%d)\n", curl_easy_strerror(res), retry_count, HTTP_MAX_RETRIES); if (retry_count < HTTP_MAX_RETRIES) { fprintf(stderr, "Retrying in %d seconds...\n", HTTP_RETRY_DELAY_MS / 1000); usleep(HTTP_RETRY_DELAY_MS * 1000); + clock_gettime(CLOCK_MONOTONIC, &spinner_start_time); + spinner_running = 1; + pthread_create(&spinner_tid, NULL, spinner_thread, NULL); } } diff --git a/main.c b/main.c index ad663ad..b7616dc 100644 --- a/main.c +++ b/main.c @@ -260,10 +260,16 @@ static void init(void) { char datetime[64]; strftime(datetime, sizeof(datetime), "%Y-%m-%d %H:%M:%S %Z", tm_info); + char cwd[4096]; + if (!getcwd(cwd, sizeof(cwd))) { + strcpy(cwd, "unknown"); + } + snprintf( payload, sizeof(payload), "# AUTONOMOUS AGENT INSTRUCTIONS\n" - "Current date/time: %s\n\n" + "Current date/time: %s\n" + "Working directory: %s\n\n" "You are an autonomous AI agent. You operate in a loop: reason about the task, " "select and execute tools when needed, observe results, and continue until the goal is achieved.\n\n" "## Reasoning Pattern (ReAct)\n" @@ -292,7 +298,7 @@ static void init(void) { "You MUST copy/paste relevant data from tool results into your response.\n" "Bad: 'I searched and found information about X.'\n" "Good: 'Here is what I found: [actual content from search results]'\n", - datetime, schema); + datetime, cwd, schema); free(schema); fprintf(stderr, "Loading...");