lanchat

package
v1.3.105 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 3, 2026 License: MIT Imports: 17 Imported by: 0

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

View Source
const (
	RoleHuman = "human"
	RoleAgent = "agent"
)

Role constants.

View Source
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.

View Source
const DefaultRole = "developer"

DefaultRole is assigned when no role is specified via /nick.

View Source
const DefaultTeam = "dev-team"

DefaultTeam is assigned when no team is specified via /nick.

Variables

This section is empty.

Functions

func AgentNick

func AgentNick(humanNick string) string

AgentNick returns the agent nickname derived from a human nick.

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

func DownloadAttachment(url, apiKey string) ([]byte, string, error)

DownloadAttachment fetches an attachment from a peer's URL.

func FormatPeersInfo added in v1.3.101

func FormatPeersInfo(hub *Hub, workspace string) string

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

func LoadApprovalPolicies(dir string) (map[string]string, error)

LoadApprovalPolicies reads persisted approval policies from <dir>/approval-policies.json. Returns map[peerNodeID]policy. Missing file returns empty map + nil error.

func LoadNick

func LoadNick(dir string) (string, error)

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

func LoadRole(dir string) (string, error)

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

func LoadTeam(dir string) (string, error)

LoadTeam reads the team from <dir>/lanchat-team. Returns "" and no error if the file does not exist.

func MountHandlers

func MountHandlers(mux *http.ServeMux, hub *Hub)

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

func ParseNickRole(input string) (nick, role string)

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

func ParseNickRoleTeam(input string) (nick, role, team string)

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

func ReadFileForAttachment(path string) (string, []byte, string, error)

ReadFileForAttachment reads a local file and returns attachment metadata.

func ResolveNickConflict

func ResolveNickConflict(nick string, taken map[string]bool) string

ResolveNickConflict appends a number if the nick is already taken.

func SaveApprovalPolicies

func SaveApprovalPolicies(dir string, policies map[string]string) error

SaveApprovalPolicies persists approval policies to <dir>/approval-policies.json.

func SaveNick

func SaveNick(dir, nick string) error

SaveNick persists the nickname to <dir>/lanchat-nick. The directory is created if it does not exist.

func SaveRole added in v1.3.84

func SaveRole(dir, role string) error

SaveRole persists the role to <dir>/lanchat-role.

func SaveTeam added in v1.3.85

func SaveTeam(dir, team string) error

SaveTeam persists the team to <dir>/lanchat-team.

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

type AttachmentDownloadError struct {
	StatusCode int
	URL        string
}

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) APIKey

func (h *Hub) APIKey() string

APIKey returns the A2A API key used for peer authentication.

func (*Hub) AgentNick

func (h *Hub) AgentNick() string

AgentNick returns this node's agent nickname.

func (*Hub) ApproveMessage

func (h *Hub) ApproveMessage(messageID string) (*Message, error)

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

func (h *Hub) BroadcastAsAgent(ctx context.Context, content string) error

BroadcastAsAgent sends a message from the agent to all peers.

func (*Hub) GetApprovalPolicies

func (h *Hub) GetApprovalPolicies() map[string]string

GetApprovalPolicies returns a copy of all approval policies.

func (*Hub) GetReceipt

func (h *Hub) GetReceipt(messageID string) (Receipt, bool)

GetReceipt returns the latest receipt for a message ID.

func (*Hub) HandleIncomingMessage

func (h *Hub) HandleIncomingMessage(msg Message)

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

func (h *Hub) HandleReceipt(r Receipt)

HandleReceipt processes a receipt from a peer.

func (*Hub) HumanNick

func (h *Hub) HumanNick() string

HumanNick returns this node's human nickname.

func (*Hub) IsOnline

func (h *Hub) IsOnline(nodeID string) bool

IsOnline returns whether a node is currently online.

func (*Hub) LoadHistory

func (h *Hub) LoadHistory(sessionID string) ([]Message, error)

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) Messages

func (h *Hub) Messages() []Message

Messages returns a copy of all received messages.

func (*Hub) NodeID

func (h *Hub) NodeID() string

NodeID returns this node's ID.

func (*Hub) NotifyAgentComplete

func (h *Hub) NotifyAgentComplete(messageID string)

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

func (h *Hub) PersistMessage(sessionID string, msg Message) error

PersistMessage saves a message to the per-session store.

func (*Hub) RejectMessage

func (h *Hub) RejectMessage(messageID, reason string) error

RejectMessage removes a pending approval and sends "rejected" receipt.

func (*Hub) Role added in v1.3.84

func (h *Hub) Role() string

Role returns this node's user-defined role.

func (*Hub) SelfParticipant

func (h *Hub) SelfParticipant() Participant

SelfParticipant returns this node's own participant info.

func (*Hub) SendAsAgent

func (h *Hub) SendAsAgent(ctx context.Context, toNodeID, toRole, content string) error

SendAsAgent sends a message from the agent role (for agent responses to @agent messages).

func (*Hub) SendBroadcast

func (h *Hub) SendBroadcast(ctx context.Context, content string, attachments []Attachment) error

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

func (h *Hub) SetAgentBusy(busy bool)

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

func (h *Hub) SetApprovalPolicy(peerNick string, policy string)

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

func (h *Hub) SetNickRole(nick, role string) error

SetNickRole is a backward-compatible wrapper that preserves the current team.

func (*Hub) SetNickRoleTeam added in v1.3.85

func (h *Hub) SetNickRoleTeam(nick, role, team string) error

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

func (h *Hub) SetOnAutoApprove(cb func(Message))

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

func (h *Hub) SetSessionID(baseDir, sessionID string)

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) Team added in v1.3.85

func (h *Hub) Team() string

Team returns this node's user-defined team.

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

func (m Message) IsBroadcast() bool

IsBroadcast returns true if this is a broadcast message (no specific recipient).

func (Message) IsDirectToAgent

func (m Message) IsDirectToAgent() bool

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.

func NewStore

func NewStore(dir string) *Store

NewStore creates a message store rooted at the given directory.

func (*Store) Append

func (s *Store) Append(sessionID string, msg Message) error

Append writes a message to the session's history file, trimming to the most recent maxHistoryPerSession entries.

func (*Store) LoadRecent

func (s *Store) LoadRecent(sessionID string, limit int) ([]Message, error)

LoadRecent returns up to limit most recent messages for a session. If limit <= 0, returns maxHistoryPerSession.

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL