5.9 KiB
SKILL: CLI Gateway (Universal Literate Note)
- Overview
- Phase A: Demand (PRD)
- Phase B: Blueprint (PROTOCOL)
- Phase D: Build (Implementation)
- Phase E: The Client (Scripts)
Overview
The CLI Gateway is the primary interaction point for the Org-Agent 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)
1. Purpose
Provide a secure, local, and low-latency terminal interface for the Org-Agent.
2. Success Criteria
- Ingress: Accept plain-text messages over TCP port 9105.
- Normalisation: Inject messages into the harness as `:chat-message` signals.
- Egress: Implement the `:cli` actuator to route agent responses back to the correct client socket.
- Client: Provide a standalone client script for the user's host machine.
Phase B: Blueprint (PROTOCOL)
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
(in-package :org-agent)
State: Server Control
(defvar *cli-server-thread* nil)
(defvar *cli-server-socket* nil)
(defvar *cli-port* 9105)
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.
(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
(format stream "Agent: ~a~%" text)
(finish-output stream))
(harness-log "CLI ERROR: No active or open reply stream for signal."))
(error (c) (harness-log "CLI ACTUATOR ERROR: ~a" c)))))
Server: Client Handler
Handles an individual TCP connection. It reads lines until the connection is closed.
(defun handle-cli-client (stream)
"Reads lines from a CLI client and injects them as stimuli."
(harness-log "CLI: Client connected.")
(format stream "--------------------------------------------------~%")
(format stream " Connected to OpenCortex~%")
(format stream "--------------------------------------------------~%")
(finish-output stream)
(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)
(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."))
Server: Main Loop
Listens for new TCP connections on the configured port.
(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 "org-agent-cli-client-handler")))
(usocket:socket-close *cli-server-socket*)))
:name "org-agent-cli-gateway"))
(harness-log "CLI: Gateway listening on port ~a" port))
Registration
(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))
Initialization
(start-cli-gateway)
Phase E: The Client (Scripts)
We tangle a lightweight client script that the user can run on their host machine.
The Bash Client
# org-agent-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