serve: add --bind ADDR for VPS / remote-control exposure

ober

bfacc320eb82478b9d46ffccfdf5faf9a831b92e

diff --git a/src/jcode/ui/cli.ss b/src/jcode/ui/cli.ss
index 8e00e7f..14567ad 100644
--- a/src/jcode/ui/cli.ss
+++ b/src/jcode/ui/cli.ss
@@ -166,7 +166,10 @@ COMMANDS:
     session resume   Resume a previous session
     config           Show or edit configuration
     serve            Start JSONL server (stdio mode)
-    serve --port N   Start TCP server on 127.0.0.1:N
+    serve --port N   Start TCP server (default bind: 127.0.0.1)
+    serve --port N --bind ADDR
+                     Bind to a specific address (e.g. 0.0.0.0 for
+                     VPS / remote control). Token still required.
     serve --rotate-token   Generate new auth token and exit
     serve --show-token     Display current auth token and exit
 
@@ -175,6 +178,7 @@ EXAMPLES:
     jcode \"Read main.ss\"           One-shot query
     jcode session list              List sessions
     jcode serve --port 8321         Start TCP server for Android app
+    jcode serve --port 8321 --bind 0.0.0.0    Expose on VPS (front with TLS)
 "))
 
 (def (interactive-mode opts)
diff --git a/src/jcode/ui/serve.ss b/src/jcode/ui/serve.ss
index 195215c..9cab8a5 100644
--- a/src/jcode/ui/serve.ss
+++ b/src/jcode/ui/serve.ss
@@ -369,12 +369,18 @@
 
 ;; ── TCP server loop ───────────────────────────────────────────────────
 
-(def (tcp-serve-loop port-num)
-  "Listen on 127.0.0.1:port, accept one client at a time, auth + event loop."
+(def (tcp-serve-loop bind-addr port-num)
+  "Listen on bind-addr:port, accept one client at a time, auth + event loop."
   (let ((token (ensure-token!))
-        (srv (tcp-listen "127.0.0.1" port-num)))
-    (fprintf (current-error-port) "[INFO] listening on 127.0.0.1:~a~n"
-             (tcp-server-port srv))
+        (srv (tcp-listen bind-addr port-num)))
+    (fprintf (current-error-port) "[INFO] listening on ~a:~a~n"
+             bind-addr (tcp-server-port srv))
+    (unless (or (equal? bind-addr "127.0.0.1") (equal? bind-addr "::1"))
+      (fprintf (current-error-port)
+               "[WARN] non-loopback bind (~a): token traverses cleartext TCP.~n"
+               bind-addr)
+      (fprintf (current-error-port)
+               "       Front with TLS (stunnel/caddy) or SSH/WireGuard for public exposure.~n"))
     (flush-output-port (current-error-port))
     (let accept-loop ()
       (fprintf (current-error-port) "[INFO] waiting for client...~n")
@@ -414,6 +420,8 @@
            (begin
              (fprintf (current-error-port) "[ERROR] invalid port: ~a~n" (cadr args))
              (exit 1)))))
+      ((and (equal? (car args) "--bind") (pair? (cdr args)))
+       (loop (cddr args) (cons (cons 'bind (cadr args)) opts)))
       ((equal? (car args) "--rotate-token")
        (loop (cdr args) (cons '(rotate-token . #t) opts)))
       ((equal? (car args) "--show-token")
@@ -448,12 +456,14 @@
       (thread-join! t-mcp)
       (thread-join! t-lsp))
     ;; Dispatch to TCP or stdio mode
-    (let ((port-opt (assoc 'port opts)))
+    (let ((port-opt (assoc 'port opts))
+          (bind-opt (assoc 'bind opts)))
       (if port-opt
         ;; TCP mode
-        (begin
-          (log-info logger "serve-start" `((mode . "tcp") (port . ,(cdr port-opt))))
-          (tcp-serve-loop (cdr port-opt)))
+        (let ((bind-addr (if bind-opt (cdr bind-opt) "127.0.0.1")))
+          (log-info logger "serve-start"
+            `((mode . "tcp") (bind . ,bind-addr) (port . ,(cdr port-opt))))
+          (tcp-serve-loop bind-addr (cdr port-opt)))
         ;; stdio mode (backward compat)
         (begin
           (log-info logger "serve-start" '((mode . "stdio")))