Documentation
¶
Overview ¶
Package lanchat provides a decentralized LAN chat system built on top of the A2A HTTP server and mDNS discovery. Each ggcode node runs two roles: a human user and their agent. Messages are P2P (HTTP POST) with no central server.
Index ¶
- Constants
- func AgentNick(humanNick string) string
- func AuthMiddleware(apiKey string, next http.HandlerFunc) http.HandlerFunc
- func DownloadAttachment(url, apiKey string) ([]byte, string, error)
- func FormatPeersInfo(hub *Hub, workspace string) string
- func LoadApprovalPolicies(dir string) (map[string]string, error)
- func LoadNick(dir string) (string, error)
- func LoadRole(dir string) (string, error)
- func LoadTeam(dir string) (string, error)
- func MountHandlers(mux *http.ServeMux, hub *Hub)
- func ParseNickRole(input string) (nick, role string)
- func ParseNickRoleTeam(input string) (nick, role, team string)
- func RandomNick() string
- func ReadFileForAttachment(path string) (string, []byte, string, error)
- func ResolveNickConflict(nick string, taken map[string]bool) string
- func SaveApprovalPolicies(dir string, policies map[string]string) error
- func SaveNick(dir, nick string) error
- func SaveRole(dir, role string) error
- func SaveTeam(dir, team string) error
- func SetAttachmentURL(endpoint string, att *Attachment)
- type ArchivedPeer
- type Attachment
- type AttachmentDownloadError
- type AttachmentManager
- type Hub
- func (h *Hub) APIKey() string
- func (h *Hub) AgentNick() string
- func (h *Hub) ApproveMessage(messageID string) (*Message, error)
- func (h *Hub) Archive() []ArchivedPeer
- func (h *Hub) Attachments() *AttachmentManager
- func (h *Hub) BroadcastAsAgent(ctx context.Context, content string) error
- func (h *Hub) GetApprovalPolicies() map[string]string
- func (h *Hub) GetReceipt(messageID string) (Receipt, bool)
- func (h *Hub) HandleIncomingMessage(msg Message)
- func (h *Hub) HandleNickChange(change NickChange)
- func (h *Hub) HandleParticipantQuery() Participant
- func (h *Hub) HandlePresence(p Participant)
- func (h *Hub) HandleReceipt(r Receipt)
- func (h *Hub) HumanNick() string
- func (h *Hub) IsOnline(nodeID string) bool
- func (h *Hub) LoadHistory(sessionID string) ([]Message, error)
- func (h *Hub) LookupArchiveByNick(nick string) *ArchivedPeer
- func (h *Hub) LookupArchiveByNodeID(nodeID string) *ArchivedPeer
- func (h *Hub) LookupArchiveByTeamRole(team, role string) *ArchivedPeer
- func (h *Hub) Messages() []Message
- func (h *Hub) NodeID() string
- func (h *Hub) NotifyAgentComplete(messageID string)
- func (h *Hub) Participants() []Participant
- func (h *Hub) PendingApprovals() []PendingAgentMsg
- func (h *Hub) PersistMessage(sessionID string, msg Message) error
- func (h *Hub) RejectMessage(messageID, reason string) error
- func (h *Hub) Role() string
- func (h *Hub) SelfParticipant() Participant
- func (h *Hub) SendAsAgent(ctx context.Context, toNodeID, toRole, content string) error
- func (h *Hub) SendBroadcast(ctx context.Context, content string, attachments []Attachment) error
- func (h *Hub) SendDirect(ctx context.Context, toNodeID, toRole, content string, ...) error
- func (h *Hub) SetAgentBusy(busy bool)
- func (h *Hub) SetApprovalPolicy(peerNick string, policy string)
- func (h *Hub) SetAttachments(am *AttachmentManager)
- func (h *Hub) SetCallbacks(onMessage func(Message), onReceipt func(Receipt), ...)
- func (h *Hub) SetNickRole(nick, role string) error
- func (h *Hub) SetNickRoleTeam(nick, role, team string) error
- func (h *Hub) SetOnAutoApprove(cb func(Message))
- func (h *Hub) SetSessionID(baseDir, sessionID string)
- func (h *Hub) SetWorkspaceMeta(ws WorkspaceMeta)
- func (h *Hub) Team() string
- func (h *Hub) UpdatePeers(participants []Participant)
- type Message
- type NickChange
- type Participant
- type PendingAgentMsg
- type Receipt
- type Store
- type WorkspaceMeta
Constants ¶
const ( RoleHuman = "human" RoleAgent = "agent" )
Role constants.
const ( StatusDelivered = "delivered" StatusPending = "pending" // waiting for host approval StatusApproved = "approved" // host approved, agent about to run StatusProcessing = "processing" // agent is running StatusCompleted = "completed" // agent run finished StatusRejected = "rejected" )
Receipt status constants.
const DefaultRole = "developer"
DefaultRole is assigned when no role is specified via /nick.
const DefaultTeam = "dev-team"
DefaultTeam is assigned when no team is specified via /nick.
Variables ¶
This section is empty.
Functions ¶
func AuthMiddleware ¶
func AuthMiddleware(apiKey string, next http.HandlerFunc) http.HandlerFunc
AuthMiddleware wraps an http.HandlerFunc with API key validation. Accepts either the configured API key or the built-in community key (for zero-config LAN Chat between instances with different auth configs).
func DownloadAttachment ¶
DownloadAttachment fetches an attachment from a peer's URL.
func FormatPeersInfo ¶ added in v1.3.101
lanchatPeersInfo builds a dynamic system prompt section listing all online lanchat peers. It shows both busy and idle agents, and specially marks peers in the same workspace to highlight collaboration opportunities and file-edit conflict risks.
func LoadApprovalPolicies ¶
LoadApprovalPolicies reads persisted approval policies from <dir>/approval-policies.json. Returns map[peerNodeID]policy. Missing file returns empty map + nil error.
func LoadNick ¶
LoadNick reads the nickname from <dir>/lanchat-nick. Returns "" and no error if the file does not exist.
func LoadRole ¶ added in v1.3.84
LoadRole reads the role from <dir>/lanchat-role. Returns "" and no error if the file does not exist.
func LoadTeam ¶ added in v1.3.85
LoadTeam reads the team from <dir>/lanchat-team. Returns "" and no error if the file does not exist.
func MountHandlers ¶
MountHandlers registers lanchat HTTP endpoints on the given mux. All endpoints are wrapped with AuthMiddleware using the Hub's API key.
func ParseNickRole ¶ added in v1.3.84
ParseNickRole splits "alice@frontend" into ("alice", "frontend"). "alice" → ("alice", "developer"). Deprecated: Use ParseNickRoleTeam for full 3-part parsing.
func ParseNickRoleTeam ¶ added in v1.3.85
ParseNickRoleTeam splits "alice@frontend@platform" into ("alice", "frontend", "platform"). Missing parts get defaults: role="developer", team="dev-team".
"alice" → ("alice", "developer", "dev-team")
"alice@frontend" → ("alice", "frontend", "dev-team")
"alice@frontend@platform" → ("alice", "frontend", "platform")
The split is on '@' so nicks, roles, and teams cannot contain '@'.
func RandomNick ¶
func RandomNick() string
RandomNick generates a random nickname like "CleverOtter".
func ReadFileForAttachment ¶
ReadFileForAttachment reads a local file and returns attachment metadata.
func ResolveNickConflict ¶
ResolveNickConflict appends a number if the nick is already taken.
func SaveApprovalPolicies ¶
SaveApprovalPolicies persists approval policies to <dir>/approval-policies.json.
func SaveNick ¶
SaveNick persists the nickname to <dir>/lanchat-nick. The directory is created if it does not exist.
func SetAttachmentURL ¶
func SetAttachmentURL(endpoint string, att *Attachment)
SetAttachmentURL populates the URL field for an attachment based on the sender's endpoint.
Types ¶
type ArchivedPeer ¶ added in v1.3.92
type ArchivedPeer struct {
NodeID string `json:"node_id"`
HumanNick string `json:"human_nick"`
AgentNick string `json:"agent_nick"`
Role string `json:"role"`
Team string `json:"team"`
Workspace string `json:"workspace,omitempty"`
ProjectName string `json:"project_name,omitempty"`
Languages []string `json:"languages,omitempty"`
LastSeen int64 `json:"last_seen"`
ArchivedAt int64 `json:"archived_at"` // unix seconds when the peer was archived
}
ArchivedPeer is a snapshot of a participant stored when the peer is deleted from the active peers map (after peerDeleteAfter). This allows long-running agents to correlate a returning peer (new NodeID) with its previous identity (same team + role, or same nicks).
type Attachment ¶
type Attachment struct {
ID string `json:"id"`
Name string `json:"name"`
Size int64 `json:"size"`
MIMEType string `json:"mime_type"`
URL string `json:"url"` // http://sender:port/lanchat/attach/<id>
}
Attachment represents a file attachment. The sender hosts the file and provides a URL; the receiver downloads it on demand.
type AttachmentDownloadError ¶
AttachmentDownloadError represents a failed attachment download.
func (*AttachmentDownloadError) Error ¶
func (e *AttachmentDownloadError) Error() string
type AttachmentManager ¶
type AttachmentManager struct {
// contains filtered or unexported fields
}
AttachmentManager stores attachments in memory with TTL-based cleanup.
func NewAttachmentManager ¶
func NewAttachmentManager() *AttachmentManager
NewAttachmentManager creates a new attachment manager and starts the background cleanup goroutine.
func (*AttachmentManager) Get ¶
func (am *AttachmentManager) Get(id string) *pendingAttachment
Get retrieves an attachment by ID. Returns nil if not found or expired.
func (*AttachmentManager) HandleAttachmentDownload ¶
func (am *AttachmentManager) HandleAttachmentDownload(w http.ResponseWriter, r *http.Request)
HandleAttachmentDownload serves an attachment by ID via HTTP.
func (*AttachmentManager) Stop ¶
func (am *AttachmentManager) Stop()
Stop halts the cleanup goroutine.
func (*AttachmentManager) Store ¶
func (am *AttachmentManager) Store(name string, data []byte, mimeType string) Attachment
Store saves an attachment and returns its metadata.
type Hub ¶
type Hub struct {
// contains filtered or unexported fields
}
Hub is the core LAN chat coordinator. It manages participants, sends and receives messages, tracks receipts, and queues agent-direct messages for host approval.
func NewHub ¶
func NewHub(nodeID, mode, endpoint, apiKey string, store *Store, ws WorkspaceMeta) *Hub
NewHub creates a new chat hub with a random nickname. Each session gets its own random nick — there is no global nick. SetSessionID persists the nick to the session directory when called.
func (*Hub) ApproveMessage ¶
ApproveMessage removes a pending approval and returns the message for injection into the agent loop. Sends "approved" then "processing" receipts. The caller must call NotifyAgentComplete(messageID) when the agent run finishes.
func (*Hub) Archive ¶ added in v1.3.92
func (h *Hub) Archive() []ArchivedPeer
Archive returns a copy of the archived peer snapshots (FIFO order).
func (*Hub) Attachments ¶
func (h *Hub) Attachments() *AttachmentManager
Attachments returns the attachment manager (nil if not enabled).
func (*Hub) BroadcastAsAgent ¶ added in v1.3.85
BroadcastAsAgent sends a message from the agent to all peers.
func (*Hub) GetApprovalPolicies ¶
GetApprovalPolicies returns a copy of all approval policies.
func (*Hub) GetReceipt ¶
GetReceipt returns the latest receipt for a message ID.
func (*Hub) HandleIncomingMessage ¶
HandleIncomingMessage processes a message received from a peer.
func (*Hub) HandleNickChange ¶
func (h *Hub) HandleNickChange(change NickChange)
HandleNickChange updates a peer's nickname and fires the onNickChange callback so the UI can display a system message.
func (*Hub) HandleParticipantQuery ¶
func (h *Hub) HandleParticipantQuery() Participant
HandleParticipantQuery returns this node's participant info.
func (*Hub) HandlePresence ¶
func (h *Hub) HandlePresence(p Participant)
HandlePresence processes an incoming presence announcement from a peer. This is called when a newly discovered peer sends us their participant info.
func (*Hub) HandleReceipt ¶
HandleReceipt processes a receipt from a peer.
func (*Hub) LoadHistory ¶
LoadHistory loads recent messages for a session from the store.
func (*Hub) LookupArchiveByNick ¶ added in v1.3.92
func (h *Hub) LookupArchiveByNick(nick string) *ArchivedPeer
LookupArchiveByNick searches the archive for a peer with the given human_nick or agent_nick (case-insensitive). Returns the most recent match, or nil if not found.
func (*Hub) LookupArchiveByNodeID ¶ added in v1.3.92
func (h *Hub) LookupArchiveByNodeID(nodeID string) *ArchivedPeer
LookupArchiveByNodeID searches the archive for a peer with the given node_id. Returns nil if not found.
func (*Hub) LookupArchiveByTeamRole ¶ added in v1.3.92
func (h *Hub) LookupArchiveByTeamRole(team, role string) *ArchivedPeer
LookupArchiveByTeamRole searches the archive for peers matching the given team AND role. Returns the most recent match (last added), or nil if none found.
func (*Hub) NotifyAgentComplete ¶
NotifyAgentComplete sends a "completed" receipt to the sender of a manually-approved message after the agent run finishes. For auto-approved messages, the receipt is sent automatically inside the onAutoApprove callback wrapper.
func (*Hub) Participants ¶
func (h *Hub) Participants() []Participant
Participants returns all known participants (self + peers).
func (*Hub) PendingApprovals ¶
func (h *Hub) PendingApprovals() []PendingAgentMsg
PendingApprovals returns messages awaiting host approval.
func (*Hub) PersistMessage ¶
PersistMessage saves a message to the per-session store.
func (*Hub) RejectMessage ¶
RejectMessage removes a pending approval and sends "rejected" receipt.
func (*Hub) SelfParticipant ¶
func (h *Hub) SelfParticipant() Participant
SelfParticipant returns this node's own participant info.
func (*Hub) SendAsAgent ¶
SendAsAgent sends a message from the agent role (for agent responses to @agent messages).
func (*Hub) SendBroadcast ¶
SendBroadcast sends a message to all peers (human role only).
func (*Hub) SendDirect ¶
func (h *Hub) SendDirect(ctx context.Context, toNodeID, toRole, content string, attachments []Attachment) error
SendDirect sends a targeted message to a specific node/role.
func (*Hub) SetAgentBusy ¶ added in v1.3.87
SetAgentBusy updates the local agent's busy state and broadcasts presence to all online peers so they know whether this node's agent is available.
func (*Hub) SetApprovalPolicy ¶
SetApprovalPolicy sets the approval policy for a peer (by nick) and persists it. policy: "always" (auto-approve), "never" (auto-reject), "" (ask).
func (*Hub) SetAttachments ¶
func (h *Hub) SetAttachments(am *AttachmentManager)
SetAttachments enables attachment support on this hub.
func (*Hub) SetCallbacks ¶
func (h *Hub) SetCallbacks( onMessage func(Message), onReceipt func(Receipt), onParticipantAdd func(Participant), onParticipantRm func(nodeID, humanNick string), onApprovalReq func(PendingAgentMsg), onNickChange func(nodeID, oldNick, newNick string), )
SetCallbacks registers UI event callbacks.
func (*Hub) SetNickRole ¶ added in v1.3.84
SetNickRole is a backward-compatible wrapper that preserves the current team.
func (*Hub) SetNickRoleTeam ¶ added in v1.3.85
SetNickRole changes the human nickname and role, updates agent nick, broadcasts to peers, and persists to the session-scoped path. The humanNick is composed as "nick_role" (e.g. "alice_dev") so that identity + role are self-evident and collision probability is low. On conflict, resolveNickConflict will produce "alice_dev2", "alice_dev3", etc. SetNickRoleTeam changes the human nickname, role, and team; updates agent nick, persists all three, and broadcasts the change to all peers.
func (*Hub) SetOnAutoApprove ¶
SetOnAutoApprove registers the callback invoked when a message is auto-approved (by policy or daemon mode). The host uses this to inject the message into the agent loop.
func (*Hub) SetSessionID ¶
SetSessionID switches nick persistence to a per-session directory. This loads the nick from <baseDir>/sessions/<sessionID>/lanchat-nick if it exists, overriding the global nick. Subsequent SetNick calls persist to this session-scoped path. If sessionID is empty, the global path is used.
func (*Hub) SetWorkspaceMeta ¶
func (h *Hub) SetWorkspaceMeta(ws WorkspaceMeta)
SetWorkspaceMeta updates the workspace metadata for this node. Call this when switching workspaces so that subsequent presence exchanges announce the new project info.
func (*Hub) UpdatePeers ¶
func (h *Hub) UpdatePeers(participants []Participant)
UpdatePeers synchronizes the peer list from A2A registry discovery results. Only verified peers (successful presence exchange) trigger join/leave callbacks. Unverified peers are tracked internally but never surfaced to the UI.
type Message ¶
type Message struct {
ID string `json:"id"`
FromNodeID string `json:"from_node_id"`
FromRole string `json:"from_role"` // "human" or "agent"
FromNick string `json:"from_nick"`
ToNodeID string `json:"to_node_id"` // empty = broadcast
ToRole string `json:"to_role"` // "human", "agent" (for direct)
Content string `json:"content"`
Attachments []Attachment `json:"attachments,omitempty"`
Timestamp int64 `json:"timestamp"` // unix ms
}
Message is a chat message exchanged between nodes.
func (Message) IsBroadcast ¶
IsBroadcast returns true if this is a broadcast message (no specific recipient).
func (Message) IsDirectToAgent ¶
IsDirectToAgent returns true if this message is directed at an agent role.
type NickChange ¶
type NickChange struct {
NodeID string `json:"node_id"`
HumanNick string `json:"human_nick"`
AgentNick string `json:"agent_nick"`
Role string `json:"role"` // new role
Team string `json:"team"` // new team
Timestamp int64 `json:"timestamp"`
}
NickChange broadcasts a nickname/role update to all peers.
type Participant ¶
type Participant struct {
NodeID string `json:"node_id"`
HumanNick string `json:"human_nick"`
AgentNick string `json:"agent_nick"`
Mode string `json:"mode"` // "tui", "gui", "daemon"
Endpoint string `json:"endpoint"`
Role string `json:"role"` // user-defined role, e.g. "frontend", "backend", "devops"
Team string `json:"team"` // user-defined team, e.g. "platform", "mobile" (default: "dev-team")
Online bool `json:"online"`
LastSeen int64 `json:"last_seen"`
// Workspace & project info (populated via presence exchange).
Workspace string `json:"workspace,omitempty"` // full path to working directory
ProjectName string `json:"project_name,omitempty"` // basename or git remote name
Languages []string `json:"languages,omitempty"` // e.g. ["go", "typescript"]
Frameworks []string `json:"frameworks,omitempty"` // e.g. ["npm", "flutter"]
HasGit bool `json:"has_git,omitempty"`
HasTests bool `json:"has_tests,omitempty"`
// AgentBusy indicates whether the agent on this node is currently
// processing a task. Propagated via presence exchange so peers know
// which agents are available and which are occupied.
AgentBusy bool `json:"agent_busy,omitempty"`
// contains filtered or unexported fields
}
Participant represents one of the two roles on a node.
type PendingAgentMsg ¶
type PendingAgentMsg struct {
Message Message `json:"message"`
Received time.Time `json:"received"`
}
PendingAgentMsg is an incoming @agent direct message awaiting host approval.
type Receipt ¶
type Receipt struct {
MessageID string `json:"message_id"`
Status string `json:"status"`
FromNodeID string `json:"from_node_id"` // node reporting the receipt (the original receiver)
FromNick string `json:"from_nick"` // human or agent nick of the node reporting the receipt
FromRole string `json:"from_role"` // "human" or "agent" — which role the receipt is for
ToNodeID string `json:"to_node_id"` // original sender (for DM routing on the receiving side)
ToRole string `json:"to_role"` // original sender's role (human/agent)
Timestamp int64 `json:"timestamp"`
Reason string `json:"reason,omitempty"`
}
Receipt is an acknowledgement sent back to the original sender.
type Store ¶
type Store struct {
// contains filtered or unexported fields
}
Store persists chat messages per session to JSONL files. Each session gets its own file with at most maxHistoryPerSession messages.
type WorkspaceMeta ¶
type WorkspaceMeta struct {
Workspace string `json:"workspace"`
ProjectName string `json:"project_name"`
Languages []string `json:"languages"`
Frameworks []string `json:"frameworks"`
HasGit bool `json:"has_git"`
HasTests bool `json:"has_tests"`
}
WorkspaceMeta describes the workspace for presence exchange. All fields are optional; empty fields are omitted from presence.
func DetectWorkspaceMeta ¶
func DetectWorkspaceMeta(dir string) WorkspaceMeta
DetectWorkspaceMeta scans the working directory for language/framework signals and returns a WorkspaceMeta suitable for presence exchange. This is a lightweight version that doesn't import the a2a package.