diff --git a/harness/doctor.org b/harness/doctor.org new file mode 100644 index 0000000..229de63 --- /dev/null +++ b/harness/doctor.org @@ -0,0 +1,168 @@ +#+TITLE: System Diagnostic Doctor (doctor.org) +#+AUTHOR: Agent +#+FILETAGS: :harness:setup:diagnostic: +#+STARTUP: content + +* Overview +The *System Doctor* is the primary diagnostic utility for the OpenCortex. Its purpose is to transform opaque startup failures into actionable engineering reports. + +By centralizing environment validation, we ensure that the "Brain" never attempts to boot in a compromised or incomplete state. + +* Phase A: Demand (Thinking) +** The XDG Standard Rationale +To ensure OpenCortex behaves as a first-class POSIX citizen, we adopt the **XDG Base Directory Specification**. This separates the system into four logical layers: + +1. **Configuration (`~/.config/opencortex`)**: User-editable settings and secrets. +2. **Data (`~/.local/share/opencortex`)**: Tangled Lisp engine artifacts (immutable by user). +3. **State (`~/.local/state/opencortex`)**: Dynamic persistence like brain snapshots. +4. **Bin (`~/.local/bin`)**: The CLI shim for global invocation. + +** The Detection Invariant: Shell Probing +Common Lisp's `uiop:getenv` is strictly typed in SBCL. The Doctor must ensure that missing variables are handled as logic failures, not type crashes. Furthermore, binary detection must use a shell probe (`command -v` or `which`) to account for varying `$PATH` inheritance between interactive and headless sessions. + +* Phase B: Protocol (Success Criteria) + +** Package Context +#+begin_src lisp :tangle (expand-file-name "doctor-tests.lisp" (concat (or (getenv "INSTALL_DIR") ".") "/tests")) +(defpackage :opencortex-doctor-tests + (:use :cl :fiveam :opencortex) + (:export #:doctor-suite)) +#+end_src + +#+begin_src lisp :tangle (expand-file-name "doctor-tests.lisp" (concat (or (getenv "INSTALL_DIR") ".") "/tests")) +(in-package :opencortex-doctor-tests) +#+end_src + +#+begin_src lisp :tangle (expand-file-name "doctor-tests.lisp" (concat (or (getenv "INSTALL_DIR") ".") "/tests")) +(def-suite doctor-suite :description "Verification of the System Doctor diagnostic logic") +#+end_src + +#+begin_src lisp :tangle (expand-file-name "doctor-tests.lisp" (concat (or (getenv "INSTALL_DIR") ".") "/tests")) +(in-suite doctor-suite) +#+end_src + +** Dependency Tests +#+begin_src lisp :tangle (expand-file-name "doctor-tests.lisp" (concat (or (getenv "INSTALL_DIR") ".") "/tests")) +(test test-dependency-check-fail + "Verify that missing binaries are correctly identified as failures." + (let ((opencortex::*doctor-required-binaries* '("non-existent-binary-123"))) + (is (null (opencortex:doctor-check-dependencies))))) +#+end_src + +** Environment Tests +#+begin_src lisp :tangle (expand-file-name "doctor-tests.lisp" (concat (or (getenv "INSTALL_DIR") ".") "/tests")) +(test test-env-validation-fail + "Verify that an invalid MEMEX_DIR triggers a critical failure." + (let ((old-m (uiop:getenv "MEMEX_DIR")) + (old-s (uiop:getenv "SKILLS_DIR"))) + (unwind-protect + (progn + (setf (uiop:getenv "MEMEX_DIR") "/non/existent/path/999") + (is (null (opencortex:doctor-check-env)))) + (setf (uiop:getenv "MEMEX_DIR") (or old-m "")) + (setf (uiop:getenv "SKILLS_DIR") (or old-s ""))))) +#+end_src + +* Phase C: Implementation (Build) + +** Package Context +#+begin_src lisp :tangle (expand-file-name "doctor.lisp" (concat (or (getenv "INSTALL_DIR") ".") "/harness")) +(in-package :opencortex) +#+end_src + +** Global Configuration +#+begin_src lisp :tangle (expand-file-name "doctor.lisp" (concat (or (getenv "INSTALL_DIR") ".") "/harness")) +(defvar *doctor-required-binaries* '("sbcl" "emacs" "git" "socat" "nc") + "List of external binaries required for full system operation.") +#+end_src + +** Dependency Verification +#+begin_src lisp :tangle (expand-file-name "doctor.lisp" (concat (or (getenv "INSTALL_DIR") ".") "/harness")) +(defun doctor-check-dependencies () + "Verifies that required external binaries are available in the PATH via a shell probe." + (let ((all-ok t)) + (harness-log "DOCTOR: Checking system dependencies...") + (dolist (dep *doctor-required-binaries*) + (let ((path (ignore-errors + (uiop:run-program (list "which" dep) + :output :string :ignore-error-status t)))) + (if (and path (> (length path) 0)) + (harness-log " [OK] Found ~a" dep) + (progn + (harness-log " [FAIL] Missing binary: ~a" dep) + (setf all-ok nil))))) + all-ok)) +#+end_src + +** Environment & XDG Validation +#+begin_src lisp :tangle (expand-file-name "doctor.lisp" (concat (or (getenv "INSTALL_DIR") ".") "/harness")) +(defun doctor-check-env () + "Validates XDG directories and environment configuration against the POSIX standard." + (harness-log "DOCTOR: Checking XDG environment...") + (let ((all-ok t) + (config-dir (uiop:getenv "OC_CONFIG_DIR")) + (data-dir (uiop:getenv "OC_DATA_DIR")) + (state-dir (uiop:getenv "OC_STATE_DIR")) + (memex-dir (uiop:getenv "MEMEX_DIR"))) + + (flet ((check-dir (name path critical) + (if (and path (> (length path) 0)) + (if (uiop:directory-exists-p path) + (harness-log " [OK] ~a: ~a" name path) + (progn + (harness-log " [FAIL] ~a directory missing: ~a" name path) + (when critical (setf all-ok nil)))) + (progn + (harness-log " [FAIL] ~a variable not set." name) + (when critical (setf all-ok nil)))))) + + (check-dir "Config (OC_CONFIG_DIR)" config-dir t) + (check-dir "Data (OC_DATA_DIR)" data-dir t) + (check-dir "State (OC_STATE_DIR)" state-dir t) + (check-dir "Memex (MEMEX_DIR)" memex-dir t)) + all-ok)) +#+end_src + +** LLM Connectivity +#+begin_src lisp :tangle (expand-file-name "doctor.lisp" (concat (or (getenv "INSTALL_DIR") ".") "/harness")) +(defun doctor-check-llm () + "Tests connectivity to primary LLM providers. Non-critical fallback allowed." + (harness-log "DOCTOR: Checking LLM connectivity...") + (let ((openrouter-key (uiop:getenv "OPENROUTER_API_KEY"))) + (if (and openrouter-key (> (length openrouter-key) 0)) + (progn + (harness-log " [OK] OpenRouter API Key detected.") + t) + (progn + (harness-log " [WARN] No OpenRouter API Key. Falling back to local inference only.") + t)))) +#+end_src + +** Orchestration +#+begin_src lisp :tangle (expand-file-name "doctor.lisp" (concat (or (getenv "INSTALL_DIR") ".") "/harness")) +(defun doctor-run-all () + "Executes the full diagnostic suite and returns T if system is healthy." + (harness-log "==================================================") + (harness-log " OPENCORTEX DOCTOR: Commencing Health Check") + (harness-log "==================================================") + (let ((dep-ok (doctor-check-dependencies)) + (env-ok (doctor-check-env)) + (llm-ok (doctor-check-llm))) + (harness-log "==================================================") + (if (and dep-ok env-ok) + (progn + (harness-log " ✓ SYSTEM HEALTHY: Ready for ignition.") + t) + (progn + (harness-log " ✗ SYSTEM UNHEALTHY: Fix the errors above.") + nil)))) +#+end_src + +** CLI Entry Point +#+begin_src lisp :tangle (expand-file-name "doctor.lisp" (concat (or (getenv "INSTALL_DIR") ".") "/harness")) +(defun doctor-main () + "Entry point for the 'doctor' CLI command." + (if (doctor-run-all) + (uiop:quit 0) + (uiop:quit 1))) +#+end_src diff --git a/harness/package.org b/harness/package.org index e1404d0..b0af452 100644 --- a/harness/package.org +++ b/harness/package.org @@ -32,6 +32,21 @@ The ~package.lisp~ file defines the public API of the ~opencortex~ harness. It s #:harness-log #:main + ;; --- Diagnostic Doctor --- + #:doctor-run-all + #:doctor-main + #:doctor-check-dependencies + #:doctor-check-env + + ;; --- Setup Wizard --- + #:register-provider + #:system-ready-p + #:run-setup-wizard + + ;; --- Diagnostic Doctor --- + #:doctor-run-all + #:doctor-main + ;; --- Memory (CLOSOS) --- #:ingest-ast #:lookup-object diff --git a/harness/setup.org b/harness/setup.org index 029c26e..c2197b2 100644 --- a/harness/setup.org +++ b/harness/setup.org @@ -6,275 +6,196 @@ * Zero-to-One Setup (setup.org) The ~setup.org~ file defines the automated installation and initialization sequence for the OpenCortex. -** The Installer Script (opencortex.sh) -#+begin_src bash :tangle (expand-file-name "../opencortex.sh" (concat (or (getenv "INSTALL_DIR") ".") "/harness")) -#!/bin/bash -set -e +* Phase A: Demand (Thinking) +** The Agnostic LLM Provider Registry +To fulfill the mandate of sovereignty and extensibility, the setup process must move away from a single hardcoded LLM provider (like OpenRouter). -PORT=9105 -HOST="localhost" -RED='\033[0;31m'; GREEN='\033[0;32m'; BLUE='\033[0;34m'; YELLOW='\033[0;33m'; NC='\033[0m' +** Design Goals: +1. **Modular Adapters:** Each provider (Ollama, Groq, OpenAI, etc.) is a data-driven structure defining its required fields (API_KEY, BASE_URL) and its "ping" validation logic. +2. **Interactive Selection:** The user should be presented with a multi-select list of providers. +3. **Local-First Default:** If no cloud keys are provided, the system must default to a local Ollama/llama.cpp configuration. +4. **State Persistence:** Configuration is saved to `providers.lisp` in the XDG Config directory. +5. **Secret Splitting:** Sensitive keys go to `.env`, while metadata (models, URLs) lives in `state/providers.lisp`. -command_exists() { command -v "$1" >/dev/null 2>&1; } +* Phase B: Protocol (Success Criteria) -# Resolve symlinks to find the actual repository location -SOURCE="${BASH_SOURCE[0]}" -while [ -h "$SOURCE" ]; do - DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )" - SOURCE="$(readlink "$SOURCE")" - [[ $SOURCE != /* ]] && SOURCE="$DIR/$SOURCE" -done -export SCRIPT_DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )" - -# Load environment variables if they exist -if [ -f "$SCRIPT_DIR/.env" ]; then - while IFS="=" read -r key value || [ -n "$key" ]; do - if [[ $key =~ ^[a-zA-Z_][a-zA-Z0-9_]*$ ]]; then - val=$(echo "$value" | sed "s/^\"//;s/\"$//") - export "$key=$val" - fi - done < "$SCRIPT_DIR/.env" - [ -n "$ORG_AGENT_DAEMON_PORT" ] && PORT=$ORG_AGENT_DAEMON_PORT - [ -n "$DAEMON_HOST" ] && HOST=$DAEMON_HOST -fi - -# --- 1. BOOTSTRAP --- -# If the script is run standalone, it clones the full repo and restarts itself. -if [ ! -d "$SCRIPT_DIR/.git" ] && [ ! -d "$HOME/.opencortex" ] && [[ ! "$(pwd)" =~ "opencortex" ]]; then - echo -e "${BLUE}=== OpenCortex: Zero-to-One Bootstrapper ===${NC}" - git clone ssh://git@10.10.10.201:2222/amr/opencortex.git ~/.opencortex - cd ~/.opencortex && git submodule update --init --recursive - exec ./opencortex.sh "$@" -fi - -# --- 2. SETUP --- -setup_system() { - NON_INTERACTIVE=false - for arg in "$@"; do - if [ "$arg" == "--non-interactive" ]; then NON_INTERACTIVE=true; fi - done - - echo -e "${BLUE}=== OpenCortex: Initializing System ===${NC}" - echo -e "${YELLOW}--- Installing System Dependencies ---${NC}" - if command_exists apt-get; then - sudo apt-get update && sudo apt-get install -y sbcl emacs-nox rlwrap netcat-openbsd curl git socat libssl-dev libncurses5-dev libffi-dev zlib1g-dev libsqlite3-dev - fi - if [ ! -d "$HOME/quicklisp" ]; then - curl -O https://beta.quicklisp.org/quicklisp.lisp - sbcl --non-interactive --load quicklisp.lisp --eval "(quicklisp-quickstart:install)" --eval "(ql-util:without-prompting (ql:add-to-init-file))" - rm quicklisp.lisp - fi - - cd "$SCRIPT_DIR" - if [ ! -f .env ]; then - if [ "$NON_INTERACTIVE" = true ]; then - echo "Non-interactive mode: Using environment variables for .env creation." - cp .env.example .env - [ -n "$MEMEX_USER" ] && sed -i "s|MEMEX_USER=.*|MEMEX_USER=\"$MEMEX_USER\"|" .env - [ -n "$MEMEX_ASSISTANT" ] && sed -i "s|MEMEX_ASSISTANT=.*|MEMEX_ASSISTANT=\"$MEMEX_ASSISTANT\"|" .env - [ -n "$OPENROUTER_API_KEY" ] && sed -i "s|OPENROUTER_API_KEY=.*|OPENROUTER_API_KEY=\"$OPENROUTER_API_KEY\"|" .env - [ -n "$MEMEX_DIR" ] && sed -i "s|MEMEX_DIR=.*|MEMEX_DIR=\"$MEMEX_DIR\"|" .env - else - cp .env.example .env - echo -e "\n${YELLOW}--- Identity Configuration ---${NC}" - read -p "Your Name [User]: " user_name < /dev/tty - user_name=${user_name:-User} - sed -i "s|MEMEX_USER=.*|MEMEX_USER=\"$user_name\"|" .env - - read -p "Agent Name [OpenCortex]: " agent_name < /dev/tty - agent_name=${agent_name:-OpenCortex} - sed -i "s|MEMEX_ASSISTANT=.*|MEMEX_ASSISTANT=\"$agent_name\"|" .env - - echo -e "\n${YELLOW}--- LLM Configuration ---${NC}" - read -p "OpenRouter API Key: " openrouter_key < /dev/tty - [ -n "$openrouter_key" ] && sed -i "s|OPENROUTER_API_KEY=.*|OPENROUTER_API_KEY=\"$openrouter_key\"|" .env - - echo -e "\n${YELLOW}--- Memex Folder Structure ---${NC}" - read -p "Memex Root [\$HOME/memex]: " memex_dir < /dev/tty - memex_dir=${memex_dir:-\$HOME/memex} - sed -i "s|MEMEX_DIR=.*|MEMEX_DIR=\"$memex_dir\"|" .env - fi - - # Hydrate default paths - M_DIR=$(grep MEMEX_DIR .env | cut -d'"' -f2 | sed "s|\$HOME|$HOME|") - sed -i "s|SKILLS_DIR=.*|SKILLS_DIR=\"$SCRIPT_DIR/skills\"|" .env - sed -i "s|ZETTELKASTEN_DIR=.*|ZETTELKASTEN_DIR=\"$M_DIR/notes\"|" .env - mkdir -p "$M_DIR" "$M_DIR/notes" "$M_DIR/areas" "$M_DIR/resources" "$M_DIR/archives" "$M_DIR/system" "$M_DIR/inbox" "$M_DIR/daily" "$M_DIR/projects" - fi - - mkdir -p library - for f in harness/*.org skills/*.org; do - emacs -Q --batch --eval "(require 'org)" --eval "(org-babel-tangle-file \"$f\")" >/dev/null 2>&1 || true - done - - mkdir -p "$HOME/.local/bin" - ln -sf "$SCRIPT_DIR/opencortex.sh" "$HOME/.local/bin/opencortex" - - for shell_config in "$HOME/.bashrc" "$HOME/.profile"; do - if [ -f "$shell_config" ]; then - if ! grep -q ".local/bin" "$shell_config"; then - echo 'export PATH="$HOME/.local/bin:$PATH"' >> "$shell_config" - fi - fi - done - export PATH="$HOME/.local/bin:$PATH" - - echo -e "${YELLOW}--- Compiling and Loading OpenCortex ---${NC}" - sbcl --non-interactive --eval '(load (merge-pathnames "quicklisp/setup.lisp" (user-homedir-pathname)))' --eval '(push (truename (uiop:getenv "SCRIPT_DIR")) asdf:*central-registry*)' --eval "(ql:quickload '(:opencortex :croatoan))" - - if [ $? -ne 0 ]; then - echo -e "${RED}✗ Compilation failed.${NC}" - exit 1 - fi - - if [ "$NON_INTERACTIVE" = true ]; then - echo "Setup complete (Non-interactive)." - exit 0 - fi - - echo -e "${YELLOW}--- Finalizing: Awakening the Brain ---${NC}" - "$SCRIPT_DIR/opencortex.sh" --boot > "$SCRIPT_DIR/brain.log" 2>&1 & - - success=false - for i in {1..30}; do - if nc -z localhost $PORT 2>/dev/null; then success=true; break; fi - sleep 2 - echo -n "." - done - - if [ "$success" = true ]; then - echo -e "\n${GREEN}✓ Brain is alive on port $PORT.${NC}" - exit 0 - else - echo -e "\n${RED}✗ Brain failed to wake up.${NC}" - exit 1 - fi -} - -# --- 3. COMMAND ROUTER --- -COMMAND=$1 -[ -z "$COMMAND" ] && COMMAND="cli" -shift || true - -DEFAULT_PORT=9105 -DEFAULT_HOST="localhost" -TARGET_PORT=${PORT:-$DEFAULT_PORT} -TARGET_HOST=${HOST:-$DEFAULT_HOST} - -# If uninitialized, force setup. -if [ ! -f "$SCRIPT_DIR/library/package.lisp" ] || [ ! -f "$SCRIPT_DIR/.env" ]; then - COMMAND="setup" -fi - -case "$COMMAND" in - setup) - setup_system "$@" - ;; - - --boot|boot) - export SKILLS_DIR="${SCRIPT_DIR}/skills" - [ -z "$MEMEX_DIR" ] && export MEMEX_DIR="$HOME/memex" - if [ -f "$SCRIPT_DIR/.env" ]; then - export OPENROUTER_API_KEY=$(grep OPENROUTER_API_KEY "$SCRIPT_DIR/.env" | cut -d'"' -f2) - fi - exec sbcl --non-interactive --eval '(load (merge-pathnames "quicklisp/setup.lisp" (user-homedir-pathname)))' --eval '(setf *debugger-hook* (lambda (c h) (declare (ignore h)) (format *error-output* "FATAL LISP ERROR: ~a~%" c) (uiop:print-backtrace :stream *error-output*) (uiop:quit 1)))' --eval '(push (truename (uiop:getenv "SCRIPT_DIR")) asdf:*central-registry*)' --eval '(format t "--- Quickloading OpenCortex ---~%")' --eval "(ql:quickload '(:opencortex :croatoan))" --eval '(opencortex:main)' - ;; - - tui) - if ! nc -z $TARGET_HOST $TARGET_PORT 2>/dev/null; then - echo -e "Brain is offline. Awakening..." - "$SCRIPT_DIR/opencortex.sh" --boot > "$SCRIPT_DIR/brain.log" 2>&1 & - for i in {1..15}; do - sleep 2 - if nc -z $TARGET_HOST $TARGET_PORT 2>/dev/null; then break; fi - echo -n "." - done - echo "" - fi - echo -e "Launching Croatoan TUI..." - export SKILLS_DIR="${SCRIPT_DIR}/skills" - [ -z "$MEMEX_DIR" ] && export MEMEX_DIR="$HOME/memex" - exec sbcl --eval '(load (merge-pathnames "quicklisp/setup.lisp" (user-homedir-pathname)))' --eval '(push (truename (uiop:getenv "SCRIPT_DIR")) asdf:*central-registry*)' --eval '(ql:quickload :opencortex/tui)' --eval '(opencortex.tui:main)' - ;; - - cli) - if ! nc -z $TARGET_HOST $TARGET_PORT 2>/dev/null; then - echo -e "Brain is offline. Awakening..." - "$SCRIPT_DIR/opencortex.sh" --boot > "$SCRIPT_DIR/brain.log" 2>&1 & - for i in {1..15}; do - sleep 2 - if nc -z $TARGET_HOST $TARGET_PORT 2>/dev/null; then break; fi - echo -n "." - done - echo "" - fi - if command_exists socat; then - echo -e "Connected to OpenCortex on $TARGET_HOST:$TARGET_PORT (Channel: CLI)" - while true; do - read -p "User: " MESSAGE - if [ -z "$MESSAGE" ]; then continue; fi - if [ "$MESSAGE" = "/exit" ]; then break; fi - - # Frame the message - PAYLOAD="(:TYPE :EVENT :META (:SOURCE :CLI) :PAYLOAD (:SENSOR :USER-INPUT :TEXT \"$MESSAGE\"))" - LEN=$(printf "%s" "$PAYLOAD" | wc -c) - HEXLEN=$(printf "%06x" $LEN) - - # Send and read response - (printf "%s%s" "$HEXLEN" "$PAYLOAD" | nc -N $TARGET_HOST $TARGET_PORT) | while read -r LINE; do - CLEAN=$(echo "$LINE" | sed 's/^......//') - if [[ "$CLEAN" == *":TEXT"* ]]; then - TEXT=$(echo "$CLEAN" | sed -n 's/.*:TEXT "\([^"]*\)".*/\1/p') - echo -e "Agent: $TEXT" - fi - done - done - else - echo "Error: socat required for CLI interaction." - exit 1 - fi - ;; - - *) - echo -e "Unknown command: $COMMAND" - echo "Available commands: setup, boot, tui, cli" - exit 1 - ;; -esac +** Test Suite Context +#+begin_src lisp :tangle (expand-file-name "setup-wizard-tests.lisp" (concat (or (getenv "INSTALL_DIR") ".") "/tests")) +(defpackage :opencortex-setup-tests + (:use :cl :fiveam :opencortex) + (:export #:setup-suite)) #+end_src -** Metabolic Docker Infrastructure (Dockerfile) -#+begin_src dockerfile :tangle (expand-file-name "../infrastructure/docker/Dockerfile" (concat (or (getenv "INSTALL_DIR") ".") "/harness")) -FROM debian:bullseye-slim - -ENV DEBIAN_FRONTEND=noninteractive - -RUN apt-get update && apt-get install -y \ - sbcl \ - emacs-nox \ - curl \ - git \ - socat \ - netcat-openbsd \ - libssl-dev \ - libncurses5-dev \ - libffi-dev \ - zlib1g-dev \ - libsqlite3-dev \ - && rm -rf /var/lib/apt/lists/* - -# Install Quicklisp -RUN curl -O https://beta.quicklisp.org/quicklisp.lisp \ - && sbcl --non-interactive --load quicklisp.lisp --eval "(quicklisp-quickstart:install)" --eval "(ql-util:without-prompting (ql:add-to-init-file))" \ - && rm quicklisp.lisp - -WORKDIR /app -COPY . . - -# Initialize system in non-interactive mode -RUN mkdir -p /root/memex && ./opencortex.sh setup --non-interactive - -EXPOSE 9105 - -CMD ["./opencortex.sh", "boot"] +#+begin_src lisp :tangle (expand-file-name "setup-wizard-tests.lisp" (concat (or (getenv "INSTALL_DIR") ".") "/tests")) +(in-package :opencortex-setup-tests) +#+end_src + +#+begin_src lisp :tangle (expand-file-name "setup-wizard-tests.lisp" (concat (or (getenv "INSTALL_DIR") ".") "/tests")) +(def-suite setup-suite :description "Verification of the Lisp Setup Wizard") +#+end_src + +#+begin_src lisp :tangle (expand-file-name "setup-wizard-tests.lisp" (concat (or (getenv "INSTALL_DIR") ".") "/tests")) +(in-suite setup-suite) +#+end_src + +** Persistence Tests +#+begin_src lisp :tangle (expand-file-name "setup-wizard-tests.lisp" (concat (or (getenv "INSTALL_DIR") ".") "/tests")) +(test test-provider-registry-persistence + "Verify that multiple providers can be registered and saved." + (let ((opencortex::*providers* nil)) + (opencortex:register-provider :ollama '(:url "http://localhost:11434" :model "llama3")) + (opencortex:register-provider :groq '(:key "gsk_123" :model "mixtral-8x7b")) + (is (equal "gsk_123" (getf (getf opencortex::*providers* :groq) :key))))) +#+end_src + +** Fallback Tests +#+begin_src lisp :tangle (expand-file-name "setup-wizard-tests.lisp" (concat (or (getenv "INSTALL_DIR") ".") "/tests")) +(test test-sovereign-fallback-logic + "Verify that the system identifies as healthy with only local providers." + (let ((opencortex::*providers* (list :ollama '(:url "http://localhost:11434")))) + (is (opencortex:system-ready-p)))) +#+end_src + +* Phase C: Implementation (Build) + +** Package Context +#+begin_src lisp :tangle (expand-file-name "setup-wizard.lisp" (concat (or (getenv "INSTALL_DIR") ".") "/harness")) +(in-package :opencortex) +#+end_src + +** Global Provider Registry +#+begin_src lisp :tangle (expand-file-name "setup-wizard.lisp" (concat (or (getenv "INSTALL_DIR") ".") "/harness")) +(defvar *providers* nil "Global registry of configured LLM providers.") +#+end_src + +** Provider Templates +#+begin_src lisp :tangle (expand-file-name "setup-wizard.lisp" (concat (or (getenv "INSTALL_DIR") ".") "/harness")) +(defvar *provider-templates* + '((:ollama . (:name "Ollama (Local)" :fields ((:url :label "URL") (:model :label "Model")) :default-url "http://localhost:11434" :default-model "llama3")) + (:openrouter . (:name "OpenRouter" :fields ((:key :label "API Key" :secret t) (:model :label "Model")) :default-model "anthropic/claude-3-opus-20240229")) + (:openai . (:name "OpenAI" :fields ((:key :label "API Key" :secret t) (:model :label "Model")) :default-model "gpt-4-turbo")) + (:groq . (:name "Groq" :fields ((:key :label "API Key" :secret t) (:model :label "Model")) :default-model "mixtral-8x7b-32768")) + (:gemini . (:name "Google Gemini" :fields ((:key :label "API Key" :secret t) (:model :label "Model")) :default-model "gemini-1.5-pro")) + (:anthropic . (:name "Anthropic" :fields ((:key :label "API Key" :secret t) (:model :label "Model")) :default-model "claude-3-5-sonnet-20240620"))) + "Templates for supported LLM providers. Fields marked :secret go to .env.") +#+end_src + +** XDG Configuration Utilities +#+begin_src lisp :tangle (expand-file-name "setup-wizard.lisp" (concat (or (getenv "INSTALL_DIR") ".") "/harness")) +(defun get-oc-config-dir () + "Resolves the OpenCortex configuration directory following XDG standards." + (let ((env (uiop:getenv "OC_CONFIG_DIR"))) + (if (and env (> (length env) 0)) + (uiop:ensure-directory-pathname env) + (merge-pathnames ".config/opencortex/" (user-homedir-pathname))))) +#+end_src + +** Secret Persistence +#+begin_src lisp :tangle (expand-file-name "setup-wizard.lisp" (concat (or (getenv "INSTALL_DIR") ".") "/harness")) +(defun save-secret (id key value) + "Appends a secret to the XDG config .env file and updates the current environment." + (let* ((env-key (format nil "~:@(~a_~a~)" id key)) + (path (merge-pathnames ".env" (get-oc-config-dir)))) + (ensure-directories-exist path) + (with-open-file (s path :direction :output :if-exists :append :if-does-not-exist :create) + (format s "~%~a=\"~a\"" env-key value)) + (setf (uiop:getenv env-key) value))) +#+end_src + +** Provider Metadata Persistence +#+begin_src lisp :tangle (expand-file-name "setup-wizard.lisp" (concat (or (getenv "INSTALL_DIR") ".") "/harness")) +(defun save-providers () + "Persist provider configuration to XDG config directory." + (let ((path (merge-pathnames "providers.lisp" (get-oc-config-dir)))) + (ensure-directories-exist path) + (with-open-file (s path :direction :output :if-exists :supersede) + (format s ";;; OpenCortex Provider Metadata~%~s~%" *providers*)))) +#+end_src + +#+begin_src lisp :tangle (expand-file-name "setup-wizard.lisp" (concat (or (getenv "INSTALL_DIR") ".") "/harness")) +(defun load-providers () + "Load provider configuration from XDG config directory." + (let ((path (merge-pathnames "providers.lisp" (get-oc-config-dir)))) + (when (uiop:file-exists-p path) + (with-open-file (s path) + (setf *providers* (read s)))))) +#+end_src + +** Registry API +#+begin_src lisp :tangle (expand-file-name "setup-wizard.lisp" (concat (or (getenv "INSTALL_DIR") ".") "/harness")) +(defun register-provider (id config) + "Update the global provider registry." + (setf (getf *providers* id) config)) +#+end_src + +#+begin_src lisp :tangle (expand-file-name "setup-wizard.lisp" (concat (or (getenv "INSTALL_DIR") ".") "/harness")) +(defun system-ready-p () + "Predicate verifying if at least one provider is configured." + (and *providers* (> (length *providers*) 0))) +#+end_src + +** User Interface Primitives +#+begin_src lisp :tangle (expand-file-name "setup-wizard.lisp" (concat (or (getenv "INSTALL_DIR") ".") "/harness")) +(defun prompt-for (label &optional default) + "Interactively prompt the user for input with an optional default." + (format t "~a~@[ [~a]~]: " label default) + (finish-output) + (let ((input (read-line))) + (if (and (string= input "") default) + default + input))) +#+end_src + +** Provider Configuration Loop +#+begin_src lisp :tangle (expand-file-name "setup-wizard.lisp" (concat (or (getenv "INSTALL_DIR") ".") "/harness")) +(defun configure-provider (id) + "Guided configuration for a specific LLM provider template." + (let* ((template (cdr (assoc id *provider-templates*))) + (fields (getf template :fields)) + (config nil)) + (format t "~%--- Configuring ~a ---~%" (getf template :name)) + (dolist (field-spec fields) + (let* ((field (first field-spec)) + (label (getf (rest field-spec) :label)) + (is-secret (getf (rest field-spec) :secret)) + (default-key (intern (format nil "DEFAULT-~a" field) :keyword)) + (default (getf template default-key)) + (val (prompt-for label default))) + (if is-secret + (save-secret id field val) + (setf (getf config field) val)))) + (register-provider id config) + (format t "✓ ~a metadata registered.~%" (getf template :name)))) +#+end_src + +** Main Setup Orchestrator +#+begin_src lisp :tangle (expand-file-name "setup-wizard.lisp" (concat (or (getenv "INSTALL_DIR") ".") "/harness")) +(defun run-setup-wizard () + "Entry point for the interactive OpenCortex Lisp Setup Wizard." + (format t "=== OpenCortex: Advanced Setup Wizard ===~%") + + ;; 1. Identity + (let ((user (prompt-for "Your Name" "User")) + (agent (prompt-for "Agent Name" "OpenCortex"))) + (format t "Welcome, ~a. I am ~a.~%" user agent)) + + ;; 2. Providers + (format t "~%Available Providers:~%") + (loop for (id . data) in *provider-templates* + do (format t " ~a: ~a~%" id (getf data :name))) + + (format t "~%Enter provider IDs to configure (comma separated, or 'all'): ") + (finish-output) + (let* ((input (read-line)) + (ids (if (string= input "all") + (mapcar #'car *provider-templates*) + (mapcar (lambda (s) (intern (string-upcase (string-trim " " s)) :keyword)) + (uiop:split-string input :separator ","))))) + (dolist (id ids) + (when (assoc id *provider-templates*) + (configure-provider id)))) + + (save-providers) + (format t "~%Setup complete. Running doctor check...~%") + (doctor-run-all)) #+end_src diff --git a/opencortex.asd b/opencortex.asd index 4ef5cda..430f50f 100644 --- a/opencortex.asd +++ b/opencortex.asd @@ -19,6 +19,8 @@ (:file "harness/reason") (:file "harness/act") (:file "harness/loop") + (:file "harness/doctor") + (:file "harness/setup-wizard") (:file "skills/org-skill-policy") (:file "skills/org-skill-bouncer") @@ -31,7 +33,6 @@ (:file "skills/org-skill-emacs-edit") (:file "skills/org-skill-tool-permissions") (:file "skills/org-skill-self-fix") - (:file "skills/org-skill-lisp-validator") (:file "skills/org-skill-peripheral-vision")) :build-operation "program-op" @@ -53,7 +54,9 @@ (:file "tests/lisp-validator-tests") (:file "tests/literate-programming-tests") (:file "tests/self-edit-tests") - (:file "tests/tool-permissions-tests"))) + (:file "tests/tool-permissions-tests") + (:file "tests/doctor-tests") + (:file "tests/setup-wizard-tests"))) (defsystem :opencortex/tui :depends-on (:opencortex :croatoan :usocket :bordeaux-threads) diff --git a/opencortex.sh b/opencortex.sh index 78b8d77..86d1d1d 100755 --- a/opencortex.sh +++ b/opencortex.sh @@ -7,7 +7,8 @@ RED='\033[0;31m'; GREEN='\033[0;32m'; BLUE='\033[0;34m'; YELLOW='\033[0;33m'; NC command_exists() { command -v "$1" >/dev/null 2>&1; } -# Resolve symlinks to find the actual repository location +# 1. XDG PATH RESOLUTION +# SCRIPT_DIR is the immutable source (where the git repo lives) SOURCE="${BASH_SOURCE[0]}" while [ -h "$SOURCE" ]; do DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )" @@ -16,25 +17,15 @@ while [ -h "$SOURCE" ]; do done export SCRIPT_DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )" -# Load environment variables if they exist -if [ -f "$SCRIPT_DIR/.env" ]; then - while IFS="=" read -r key value || [ -n "$key" ]; do - if [[ $key =~ ^[a-zA-Z_][a-zA-Z0-9_]*$ ]]; then - val=$(echo "$value" | sed "s/^\"//;s/\"$//") - export "$key=$val" - fi - done < "$SCRIPT_DIR/.env" - [ -n "$ORG_AGENT_DAEMON_PORT" ] && PORT=$ORG_AGENT_DAEMON_PORT - [ -n "$DAEMON_HOST" ] && HOST=$DAEMON_HOST -fi +# XDG Defaults +export OC_CONFIG_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/opencortex" +export OC_DATA_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/opencortex" +export OC_STATE_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/opencortex" +export OC_BIN_DIR="${XDG_BIN_HOME:-$HOME/.local/bin}" -# --- 1. BOOTSTRAP --- -# If the script is run standalone, it clones the full repo and restarts itself. -if [ ! -d "$SCRIPT_DIR/.git" ] && [ ! -f "$SCRIPT_DIR/harness/package.org" ] && [ ! -d "$HOME/.opencortex" ] && [[ ! "$(pwd)" =~ "opencortex" ]]; then - echo -e "${BLUE}=== OpenCortex: Zero-to-One Bootstrapper ===${NC}" - git clone ssh://git@10.10.10.201:2222/amr/opencortex.git ~/.opencortex - cd ~/.opencortex && git submodule update --init --recursive - exec ./opencortex.sh "$@" +# Load environment variables from the standard config location +if [ -f "$OC_CONFIG_DIR/.env" ]; then + source "$OC_CONFIG_DIR/.env" fi # --- 2. SETUP --- @@ -44,10 +35,15 @@ setup_system() { if [ "$arg" == "--non-interactive" ]; then NON_INTERACTIVE=true; fi done - echo -e "${BLUE}=== OpenCortex: Initializing System ===${NC}" + echo -e "${BLUE}=== OpenCortex: Initializing XDG-Compliant System ===${NC}" + + # Create standard directories + mkdir -p "$OC_CONFIG_DIR" "$OC_DATA_DIR" "$OC_STATE_DIR" "$OC_BIN_DIR" + mkdir -p "$OC_DATA_DIR/harness" "$OC_DATA_DIR/tests" "$OC_DATA_DIR/skills" "$OC_DATA_DIR/library" + echo -e "${YELLOW}--- Installing System Dependencies ---${NC}" if command_exists apt-get; then - sudo apt-get update && sudo apt-get install -y sbcl emacs-nox rlwrap netcat-openbsd curl git socat libssl-dev libncurses5-dev libffi-dev zlib1g-dev libsqlite3-dev + sudo apt-get update && sudo apt-get install -y sbcl emacs-nox rlwrap netcat-openbsd curl git socat libssl-dev libncurses-dev libffi-dev zlib1g-dev libsqlite3-dev fi if [ ! -d "$HOME/quicklisp" ]; then curl -O https://beta.quicklisp.org/quicklisp.lisp @@ -55,111 +51,33 @@ setup_system() { rm quicklisp.lisp fi + # Tangle the literate source from SCRIPT_DIR to OC_DATA_DIR (The Engine) + echo -e "${YELLOW}--- Deploying Engine to $OC_DATA_DIR ---${NC}" + cp "$SCRIPT_DIR/opencortex.asd" "$OC_DATA_DIR/" + cd "$SCRIPT_DIR" - if [ ! -f .env ]; then - if [ "$NON_INTERACTIVE" = true ]; then - echo "Non-interactive mode: Using environment variables for .env creation." - cp .env.example .env - [ -n "$MEMEX_USER" ] && sed -i "s|MEMEX_USER=.*|MEMEX_USER=\"$MEMEX_USER\"|" .env - [ -n "$MEMEX_ASSISTANT" ] && sed -i "s|MEMEX_ASSISTANT=.*|MEMEX_ASSISTANT=\"$MEMEX_ASSISTANT\"|" .env - [ -n "$OPENROUTER_API_KEY" ] && sed -i "s|OPENROUTER_API_KEY=.*|OPENROUTER_API_KEY=\"$OPENROUTER_API_KEY\"|" .env - [ -n "$MEMEX_DIR" ] && sed -i "s|MEMEX_DIR=.*|MEMEX_DIR=\"$MEMEX_DIR\"|" .env - else - cp .env.example .env - echo -e "\n${YELLOW}--- Identity Configuration ---${NC}" - read -p "Your Name [User]: " user_name < /dev/tty - user_name=${user_name:-User} - sed -i "s|MEMEX_USER=.*|MEMEX_USER=\"$user_name\"|" .env - - read -p "Agent Name [OpenCortex]: " agent_name < /dev/tty - agent_name=${agent_name:-OpenCortex} - sed -i "s|MEMEX_ASSISTANT=.*|MEMEX_ASSISTANT=\"$agent_name\"|" .env - - echo -e "\n${YELLOW}--- LLM Configuration ---${NC}" - read -p "OpenRouter API Key: " openrouter_key < /dev/tty - [ -n "$openrouter_key" ] && sed -i "s|OPENROUTER_API_KEY=.*|OPENROUTER_API_KEY=\"$openrouter_key\"|" .env - - echo -e "\n${YELLOW}--- Memex Folder Structure ---${NC}" - read -p "Memex Root [\$HOME/memex]: " memex_dir < /dev/tty - memex_dir=${memex_dir:-\$HOME/memex} - sed -i "s|MEMEX_DIR=.*|MEMEX_DIR=\"$memex_dir\"|" .env - fi - - # Hydrate default paths - M_DIR=$(grep MEMEX_DIR .env | cut -d'"' -f2 | sed "s|\$HOME|$HOME|") - I_DIR=$(grep INSTALL_DIR .env | cut -d'"' -f2 | sed "s|\$HOME|$HOME|") - if [ -z "$I_DIR" ]; then I_DIR="$HOME/opencortex"; fi - sed -i "s|SKILLS_DIR=.*|SKILLS_DIR=\"$I_DIR/skills\"|" .env - sed -i "s|ZETTELKASTEN_DIR=.*|ZETTELKASTEN_DIR=\"$M_DIR/notes\"|" .env - mkdir -p "$M_DIR" "$M_DIR/notes" "$M_DIR/areas" "$M_DIR/resources" "$M_DIR/archives" "$M_DIR/system" "$M_DIR/inbox" "$M_DIR/daily" "$M_DIR/projects" - fi - - I_DIR=$(grep INSTALL_DIR .env | cut -d'"' -f2 | sed "s|\$HOME|$HOME|") - if [ -z "$I_DIR" ]; then I_DIR="$HOME/opencortex"; fi - - if [ "$SCRIPT_DIR" != "$I_DIR" ]; then - echo -e "${YELLOW}--- Deploying to Instance Directory ($I_DIR) ---${NC}" - mkdir -p "$I_DIR/harness" "$I_DIR/skills" "$I_DIR/tests" - cp "$SCRIPT_DIR/opencortex.asd" "$I_DIR/" - cp "$SCRIPT_DIR/opencortex.sh" "$I_DIR/" - cp "$SCRIPT_DIR/.env" "$I_DIR/" - - echo -e "${BLUE}Tangling Lisp files to instance directory...${NC}" - export INSTALL_DIR="$I_DIR" - for f in harness/*.org skills/*.org; do - emacs -Q --batch --eval "(require 'org)" --eval "(org-babel-tangle-file \"$f\")" >/dev/null 2>&1 || true - done - else - echo -e "${BLUE}--- Running in Local Mode (Source == Instance) ---${NC}" - echo -e "${BLUE}Tangling Lisp files...${NC}" - export INSTALL_DIR="$I_DIR" - for f in harness/*.org skills/*.org; do - emacs -Q --batch --eval "(require 'org)" --eval "(org-babel-tangle-file \"$f\")" >/dev/null 2>&1 || true - done - fi - - mkdir -p "$HOME/.local/bin" - ln -sf "$I_DIR/opencortex.sh" "$HOME/.local/bin/opencortex" - - for shell_config in "$HOME/.bashrc" "$HOME/.profile"; do - if [ -f "$shell_config" ]; then - if ! grep -q ".local/bin" "$shell_config"; then - echo 'export PATH="$HOME/.local/bin:$PATH"' >> "$shell_config" - fi - fi + export INSTALL_DIR="$OC_DATA_DIR" + for f in harness/*.org skills/*.org; do + echo "Tangling $f..." + emacs -Q --batch --eval "(require 'org)" --eval "(org-babel-tangle-file \"$f\")" >/dev/null 2>&1 || true done - export PATH="$HOME/.local/bin:$PATH" - echo -e "${YELLOW}--- Compiling and Loading OpenCortex ---${NC}" - sbcl --non-interactive --eval '(load (merge-pathnames "quicklisp/setup.lisp" (user-homedir-pathname)))' --eval "(push (truename \"$I_DIR/\") asdf:*central-registry*)" --eval "(ql:quickload '(:opencortex :croatoan))" - - if [ $? -ne 0 ]; then - echo -e "${RED}✗ Compilation failed.${NC}" - exit 1 - fi + # Create the bin shim + echo -e "${YELLOW}--- Creating Bin Shim in $OC_BIN_DIR/opencortex ---${NC}" + ln -sf "$SCRIPT_DIR/opencortex.sh" "$OC_BIN_DIR/opencortex" if [ "$NON_INTERACTIVE" = true ]; then echo "Setup complete (Non-interactive)." exit 0 fi - echo -e "${YELLOW}--- Finalizing: Awakening the Brain ---${NC}" - export I_DIR="$I_DIR"; "$I_DIR/opencortex.sh" --boot > "$I_DIR/brain.log" 2>&1 & - - success=false - for i in {1..30}; do - if nc -z localhost $PORT 2>/dev/null; then success=true; break; fi - sleep 2 - echo -n "." - done - - if [ "$success" = true ]; then - echo -e "\n${GREEN}✓ Brain is alive on port $PORT.${NC}" - exit 0 - else - echo -e "\n${RED}✗ Brain failed to wake up.${NC}" - exit 1 - fi + echo -e "${YELLOW}--- Launching Lisp Setup Wizard ---${NC}" + # Use OC_DATA_DIR for the Lisp registry + exec sbcl --non-interactive \ + --eval '(load (merge-pathnames "quicklisp/setup.lisp" (user-homedir-pathname)))' \ + --eval "(push (truename \"$OC_DATA_DIR/\") asdf:*central-registry*)" \ + --eval '(ql:quickload :opencortex)' \ + --eval '(opencortex:run-setup-wizard)' } # --- 3. COMMAND ROUTER --- @@ -167,88 +85,39 @@ COMMAND=$1 [ -z "$COMMAND" ] && COMMAND="cli" shift || true -DEFAULT_PORT=9105 -DEFAULT_HOST="localhost" -TARGET_PORT=${PORT:-$DEFAULT_PORT} -TARGET_HOST=${HOST:-$DEFAULT_HOST} - -# If uninitialized, force setup. -if [ ! -f "$SCRIPT_DIR/harness/package.lisp" ] || [ ! -f "$SCRIPT_DIR/.env" ]; then - COMMAND="setup" -fi - case "$COMMAND" in + doctor) + export SKILLS_DIR="${OC_DATA_DIR}/skills" + [ -z "$MEMEX_DIR" ] && export MEMEX_DIR="$HOME/memex" + exec sbcl --non-interactive \ + --eval '(load (merge-pathnames "quicklisp/setup.lisp" (user-homedir-pathname)))' \ + --eval "(push (truename \"$OC_DATA_DIR/\") asdf:*central-registry*)" \ + --eval '(ql:quickload :opencortex)' \ + --eval '(opencortex:doctor-main)' + ;; + setup) setup_system "$@" ;; - --boot|boot) - export SKILLS_DIR="${SCRIPT_DIR}/skills" - [ -z "$MEMEX_DIR" ] && export MEMEX_DIR="$HOME/memex" - if [ -f "$SCRIPT_DIR/.env" ]; then - export OPENROUTER_API_KEY=$(grep OPENROUTER_API_KEY "$SCRIPT_DIR/.env" | cut -d'"' -f2) - fi - exec sbcl --non-interactive --eval '(load (merge-pathnames "quicklisp/setup.lisp" (user-homedir-pathname)))' --eval '(setf *debugger-hook* (lambda (c h) (declare (ignore h)) (format *error-output* "FATAL LISP ERROR: ~a~%" c) (uiop:print-backtrace :stream *error-output*) (uiop:quit 1)))' --eval '(let ((path (or (uiop:getenv "I_DIR") (uiop:getenv "SCRIPT_DIR")))) (when path (push (truename path) asdf:*central-registry*)))' --eval '(format t "--- Quickloading OpenCortex ---~%")' --eval "(ql:quickload '(:opencortex :croatoan))" --eval '(opencortex:main)' + boot|--boot) + exec sbcl --non-interactive \ + --eval '(load (merge-pathnames "quicklisp/setup.lisp" (user-homedir-pathname)))' \ + --eval "(push (truename \"$OC_DATA_DIR/\") asdf:*central-registry*)" \ + --eval "(ql:quickload '(:opencortex :croatoan))" \ + --eval '(opencortex:main)' ;; tui) - if ! nc -z $TARGET_HOST $TARGET_PORT 2>/dev/null; then - echo -e "Brain is offline. Awakening..." - "$SCRIPT_DIR/opencortex.sh" --boot > "$SCRIPT_DIR/brain.log" 2>&1 & - for i in {1..15}; do - sleep 2 - if nc -z $TARGET_HOST $TARGET_PORT 2>/dev/null; then break; fi - echo -n "." - done - echo "" - fi - echo -e "Launching Croatoan TUI..." - export SKILLS_DIR="${SCRIPT_DIR}/skills" - [ -z "$MEMEX_DIR" ] && export MEMEX_DIR="$HOME/memex" - exec sbcl --eval '(load (merge-pathnames "quicklisp/setup.lisp" (user-homedir-pathname)))' --eval '(let ((path (or (uiop:getenv "I_DIR") (uiop:getenv "SCRIPT_DIR")))) (when path (push (truename path) asdf:*central-registry*)))' --eval '(ql:quickload :opencortex/tui)' --eval '(opencortex.tui:main)' - ;; - - cli) - if ! nc -z $TARGET_HOST $TARGET_PORT 2>/dev/null; then - echo -e "Brain is offline. Awakening..." - "$SCRIPT_DIR/opencortex.sh" --boot > "$SCRIPT_DIR/brain.log" 2>&1 & - for i in {1..15}; do - sleep 2 - if nc -z $TARGET_HOST $TARGET_PORT 2>/dev/null; then break; fi - echo -n "." - done - echo "" - fi - if command_exists socat; then - echo -e "Connected to OpenCortex on $TARGET_HOST:$TARGET_PORT (Channel: CLI)" - while true; do - read -p "User: " MESSAGE - if [ -z "$MESSAGE" ]; then continue; fi - if [ "$MESSAGE" = "/exit" ]; then break; fi - - # Frame the message - PAYLOAD="(:TYPE :EVENT :META (:SOURCE :CLI) :PAYLOAD (:SENSOR :USER-INPUT :TEXT \"$MESSAGE\"))" - LEN=$(printf "%s" "$PAYLOAD" | wc -c) - HEXLEN=$(printf "%06x" $LEN) - - # Send and read response - (printf "%s%s" "$HEXLEN" "$PAYLOAD" | nc -N $TARGET_HOST $TARGET_PORT) | while read -r LINE; do - CLEAN=$(echo "$LINE" | sed 's/^......//') - if [[ "$CLEAN" == *":TEXT"* ]]; then - TEXT=$(echo "$CLEAN" | sed -n 's/.*:TEXT "\([^"]*\)".*/\1/p') - echo -e "Agent: $TEXT" - fi - done - done - else - echo "Error: socat required for CLI interaction." - exit 1 - fi + exec sbcl \ + --eval '(load (merge-pathnames "quicklisp/setup.lisp" (user-homedir-pathname)))' \ + --eval "(push (truename \"$OC_DATA_DIR/\") asdf:*central-registry*)" \ + --eval '(ql:quickload :opencortex/tui)' \ + --eval '(opencortex.tui:main)' ;; *) - echo -e "Unknown command: $COMMAND" - echo "Available commands: setup, boot, tui, cli" + echo "Available commands: setup, doctor, boot, tui" exit 1 ;; esac