:PROPERTIES:
:ID:       cli-gateway-skill
:CREATED:  [2026-04-13 Mon 17:00]
:END:
#+TITLE: SKILL: CLI Gateway (Universal Literate Note)
#+STARTUP: content
#+FILETAGS: :gateway:cli:io:autonomy:

* Overview
The *CLI Gateway* is the primary interaction point for the OpenCortex MVP. It provides a lightweight TCP socket server that allows local terminal clients to communicate with the daemon. It ensures a frictionless "First Contact" experience immediately following installation.

* Phase A: Demand (PRD)
:PROPERTIES:
:STATUS: SIGNED
:END:

** 1. Purpose
Provide a secure, local, and low-latency terminal interface for the OpenCortex.

** 2. Success Criteria
- [X] *Ingress:* Accept plain-text messages over TCP port 9105.
- [X] *Normalisation:* Inject messages into the harness as `:chat-message` signals.
- [X] *Egress:* Implement the `:cli` actuator to route agent responses back to the correct client socket.
- [X] *Client:* Provide a standalone client script for the user's host machine.

* Phase B: Blueprint (PROTOCOL)
:PROPERTIES:
:STATUS: SIGNED
:END:

** 1. Architectural Intent
The gateway runs a multi-threaded TCP server. Each connection is handled in its own thread. Inbound lines are wrapped in a Signal and processed. The `:cli` actuator retrieves the `:reply-stream` from the signal context to send the response back to the specific connected client.

** 2. Semantic Interfaces
- Inbound: `(:sensor :chat-message :channel :cli :text "...")`
- Outbound: `(:type :REQUEST :target :cli :text "...")`

* Phase D: Build (Implementation)

** Package Context
#+begin_src lisp
(in-package :opencortex)
#+end_src

** State: Server Control
#+begin_src lisp
(defvar *cli-server-thread* nil)
(defvar *cli-server-socket* nil)
(defvar *cli-port* 9105)
#+end_src

** Actuator: CLI Response
The CLI actuator writes the agent's response back to the client's network stream. It applies a simple "Agent: " prefix for clarity.

#+begin_src lisp
(defun execute-cli-action (action context)
  "Sends a message back to the connected CLI client via its network stream."
  (let* ((payload (getf action :payload))
         (text (or (getf payload :text) (getf action :text)))
         (stream (getf context :reply-stream)))
    (handler-case
        (if (and stream (open-stream-p stream))
            (progn
              (prin1 (list :type :chat :text text) stream)
              (terpri stream)
              (finish-output stream))
            (harness-log "CLI ERROR: No active or open reply stream for signal."))
      (error (c) (harness-log "CLI ACTUATOR ERROR: ~a" c)))))
#+end_src

** Server: Client Handler
Handles an individual TCP connection. It reads lines until the connection is closed.

#+begin_src lisp
(defun handle-cli-slash-command (cmd stream)
  "Handles TUI slash commands by returning structured Lisp s-expressions."
  (cond
    ((string= cmd "/status")
     (prin1 '(:type :status :scribe :idle :gardener :sleeping) stream)
     (terpri stream)
     (finish-output stream))
    ((string= cmd "/exit")
     (prin1 '(:type :info :text "Goodbye!") stream)
     (terpri stream)
     (finish-output stream))
    (t (prin1 (list :type :error :text (format nil "Unknown command: ~a" cmd)) stream)
       (terpri stream)
       (finish-output stream))))

(defun handle-cli-client (stream)
  "Reads lines from a CLI client and injects them as stimuli."
  (harness-log "CLI: Client connected.")
  (handler-case
      (loop for line = (read-line stream nil nil)
            while line do
            (let ((trimmed (string-trim '(#\Space #\Tab #\Newline #\Return) line)))
              (when (> (length trimmed) 0)
                (if (and (> (length trimmed) 0) (char= (char trimmed 0) #\/))
                    (handle-cli-slash-command trimmed stream)
                    (progn
                      (harness-log "CLI: Received input -> ~a" trimmed)
                      (inject-stimulus (list :type :EVENT 
                                             :payload (list :sensor :chat-message 
                                                            :channel :cli 
                                                            :text trimmed))
                                       :stream stream))))))
    (error (c) (harness-log "CLI CLIENT DISCONNECT: ~a" c)))
  (harness-log "CLI: Client disconnected."))
#+end_src

** Server: Main Loop
Listens for new TCP connections on the configured port.

#+begin_src lisp
(defun start-cli-gateway (&optional (port *cli-port*))
  "Starts the TCP listener for local CLI clients."
  (setf *cli-server-socket* (usocket:socket-listen "0.0.0.0" port :reuse-address t))
  (setf *cli-server-thread*
        (bt:make-thread
         (lambda ()
           (unwind-protect
                (loop
                  (let* ((socket (usocket:socket-accept *cli-server-socket*))
                         (stream (usocket:socket-stream socket)))
                    (bt:make-thread (lambda () 
                                      (unwind-protect (handle-cli-client stream)
                                        (usocket:socket-close socket)))
                                    :name "opencortex-cli-client-handler")))
             (usocket:socket-close *cli-server-socket*)))
         :name "opencortex-cli-gateway"))
  (harness-log "CLI: Gateway listening on port ~a" port))
#+end_src

** Registration
#+begin_src lisp
(register-actuator :cli #'execute-cli-action)

(defskill :skill-gateway-cli
  :priority 200
  :trigger (lambda (ctx) (declare (ignore ctx)) nil)
  :probabilistic nil
  :deterministic (lambda (action ctx) (declare (ignore ctx)) action))
#+end_src

** Initialization
#+begin_src lisp
(start-cli-gateway)
#+end_src

* Phase E: The Client (Scripts)
We tangle a lightweight client script that the user can run on their host machine.

** The Bash Client
#+begin_src bash :tangle ../scripts/opencortex-chat.sh :shebang "#!/bin/bash"
# opencortex-chat: The terminal mouthpiece for the Autonomous Brain.
PORT=9105
HOST=${1:-localhost}

# Check for socat (preferred)
if command -v socat >/dev/null 2>&1; then
    # Use socat with READLINE for history and arrow-key support.
    # It establishes a persistent bidirectional connection.
    socat READLINE,history=$HOME/.org_agent_history TCP:$HOST:$PORT
else
    # Fallback to nc (netcat) for a single-shot connection if socat is missing.
    # Note: This is less robust for agents with long-thinking times.
    echo "WARNING: socat not found. Falling back to nc (no line-editing support)."
    while true; do
        read -p "User: " MESSAGE
        if [ -z "$MESSAGE" ]; then continue; fi
        echo "$MESSAGE" | nc -N $HOST $PORT
    done
fi
#+end_src
