Server

May 15, 2026 · View on GitHub

Running the Service Manually

cd server/

# Development — OS assigns a free port; actual address printed to stderr
go run .

# Pin to a specific port (useful for curl testing)
go run . \
  -addr 127.0.0.1:8088 \
  -cwd /path/to/workspace \
  -cli-path /path/to/@github/copilot/index.js \
  -lsp=true      # default: true — LSP server on stdio

# LSP-only helper that connects to an existing HTTP service
go run . \
  -lsp-only \
  -service-url http://127.0.0.1:8088

# Build a binary
go build -o copilot-agent .
./copilot-agent          # dynamic port
./copilot-agent -addr 127.0.0.1:8088   # fixed port

Flags:

FlagDefaultDescription
-addr(free port)HTTP listen address; empty or :0 → OS picks
-port-rangeTry ports lo–hi (e.g. 18000-19000); first free wins
-cwdcurrent dirDefault working directory for sessions
-model(sdk default)Default model for new sessions
-cli-pathauto-detectedPath to Copilot CLI binary/JS entrypoint
-cli-urlURL of an already-running Copilot CLI server
-log-levelCopilot CLI log level
-log-filestderr onlyMirror service logs to a file with INFO/WARN/ERROR prefixes
-lsptrueStart LSP server on stdio
-lsp-onlyfalseStart only the LSP server and reuse an existing HTTP service
-service-urlHTTP base URL used by -lsp-only

The service always prints COPILOT_AGENT_ADDR=127.0.0.1:<PORT> to stderr once the listener is bound. When auto_start = true, the plugin reads this line and configures its HTTP client automatically — no manual base_url needed.

Service log lines are prefixed with [INFO], [WARN], or [ERROR] when -log-file is set.

When -client-lease-dir is configured, detached-service shutdown is delayed by the lease watcher grace window. Current defaults (from server/main.go) are: clientLeasePollInterval = 500ms and clientLeaseEmptyGrace = 5m (i.e. the lease directory must stay empty for five minutes before shutdown).


HTTP API Reference

MethodPathDescription
GET/healthzHealth check
GET/sessionsList all sessions (live + persisted)
POST/sessionsCreate or resume a session
GET/sessions/{id}Get live session metadata
DELETE/sessions/{id}Disconnect; ?delete=true removes persisted state
GET/sessions/{id}/messagesFetch stored session events
POST/sessions/{id}/messagesSend a prompt with optional attachments
GET/sessions/{id}/eventsSSE stream of session + host events
POST/sessions/{id}/user-input/{reqID}Answer a pending ask_user request
POST/sessions/{id}/permission/{reqID}Answer a pending permission request (interactive mode)
POST/sessions/{id}/permission-modeUpdate permission mode on a live session
POST/sessions/{id}/modelSwitch model for a live session
POST/sessions/{id}/toolsUpdate excluded tools list
GET/modelsList available models

Permission modes for POST /sessions

{ "permissionMode": "interactive" }   // prompt Neovim UI per tool use
{ "permissionMode": "approve-all" }   // auto-approve everything
{ "permissionMode": "autopilot" }     // approve-all + auto-answer user inputs
{ "permissionMode": "reject-all" }    // reject all tool uses

SSE Event Types

EventDescription
session.eventRaw SDK events (assistant.message_delta, assistant.turn_end, etc.)
host.user_input_requestedAgent needs user input — reply via POST .../user-input/{id}
host.permission_requestedTool use needs approval (interactive mode) — reply via POST .../permission/{id}
host.permission_decisionLogged approval/rejection outcome
host.permission_mode_changedPermission mode updated on live session
host.model_changedModel switched on live session
host.session_disconnectedSession was closed
: keepaliveSSE keepalive comment (ignore)

Quick Start (HTTP API)

# Terminal 1: start the service using the pre-built binary (no Go needed)
# Download it first with :CopilotAgentInstall inside Neovim, then:
~/.local/share/nvim/lazy/copilot-agent.nvim/bin/copilot-agent

# Or build from source (requires Go 1.24+)
cd server/ && go run . -cli-path ~/.local/share/github-copilot/index.js
# prints: COPILOT_AGENT_ADDR=127.0.0.1:XXXXX

# Terminal 2: create a session (replace port)
curl -s -X POST http://127.0.0.1:XXXXX/sessions \
  -H 'Content-Type: application/json' \
  -d '{"workingDirectory":".","permissionMode":"approve-all","clientName":"test"}'

# Stream events (replace SESSION_ID)
curl -N http://127.0.0.1:XXXXX/sessions/SESSION_ID/events

# Send a message
curl -X POST http://127.0.0.1:XXXXX/sessions/SESSION_ID/messages \
  -H 'Content-Type: application/json' \
  -d '{"prompt":"Explain what this project does."}'

Testing

# Go tests (vet, fmt, unit tests, build)
cd server/
go vet ./...
go test -race ./...
go build ./...

# Lua unit tests (no Neovim required)
busted --lpath='lua/?.lua;lua/?/init.lua' tests/unit/

# Neovim integration tests (requires nvim on PATH)
nvim --headless -u tests/minimal_init.lua \
  -c "PlenaryBustedFile tests/integration/setup_spec.lua"

CI runs all of the above automatically on push and PR via GitHub Actions.