This guide is for Claude Code and developers working on the llm9p codebase.
# Build
go build -o llm9p ./cmd/llm9p
# Run
ANTHROPIC_API_KEY=sk-... ./llm9p -addr :5640
# Run with debug logging
ANTHROPIC_API_KEY=sk-... ./llm9p -addr :5640 -debug
# Run all tests
go test ./...
# Run with coverage
go test -cover ./...
# Test specific package
go test ./internal/protocol/...
go test ./internal/llmfs/...
# Install dependencies
go mod tidy
# Format code
go fmt ./...
# Vet code
go vet ./...
# Build
go build -o llm9p ./cmd/llm9p
llm9p/
├── cmd/
│ └── llm9p/
│ └── main.go # Entry point, CLI flags, server setup
├── internal/
│ ├── protocol/ # 9P2000 protocol implementation
│ │ ├── protocol.go # Message types, constants, encoding
│ │ ├── message.go # Individual message types
│ │ ├── server.go # Connection handling
│ │ └── fs.go # File/Dir interfaces, base implementations
│ ├── llm/ # LLM client wrapper
│ │ └── client.go # Anthropic API integration
│ └── llmfs/ # LLM filesystem implementation
│ ├── root.go # Root directory construction
│ ├── ask.go # Ask file (shim pattern)
│ ├── state.go # Model, temperature files
│ ├── tokens.go # Read-only token counter
│ ├── new.go # Conversation reset trigger
│ ├── context.go # Conversation history
│ ├── example.go # Usage examples
│ └── stream.go # Streaming interface
├── go.mod
├── go.sum
├── README.md
└── CLAUDE.md
Protocol Layer (internal/protocol/)
File and Dir interfaces define the filesystem abstractionLLM Client (internal/llm/client.go)
LLM Filesystem (internal/llmfs/)
AskFile is the core interaction pointmodel, temperature) modify client settingsChunkFile provides streaming accessinternal/llmfs/:package llmfs
import (
"github.com/NERVsystems/llm9p/internal/llm"
"github.com/NERVsystems/llm9p/internal/protocol"
)
type MyFile struct {
*protocol.BaseFile
client *llm.Client
}
func NewMyFile(client *llm.Client) *MyFile {
return &MyFile{
BaseFile: protocol.NewBaseFile("myfile", 0666),
client: client,
}
}
func (f *MyFile) Read(p []byte, offset int64) (int, error) {
// Implement read
}
func (f *MyFile) Write(p []byte, offset int64) (int, error) {
// Implement write
}
func (f *MyFile) Stat() protocol.Stat {
s := f.BaseFile.Stat()
// Update s.Length if dynamic
return s
}
internal/llmfs/root.go:root.AddChild(NewMyFile(client))
Tversion/Rversion - Protocol negotiationTattach/Rattach - Connect to filesystemTwalk/Rwalk - Navigate directory treeTopen/Ropen - Open a fileTread/Rread - Read from fileTwrite/Rwrite - Write to fileTclunk/Rclunk - Close a fid// File is the interface that files must implement
type File interface {
Stat() Stat
Open(mode uint8) error
Read(p []byte, offset int64) (int, error)
Write(p []byte, offset int64) (int, error)
Close() error
}
// Dir extends File with directory operations
type Dir interface {
File
Children() []File
Lookup(name string) (File, error)
}
ANTHROPIC_API_KEY=sk-... ./llm9p -debug
This logs all 9P messages sent and received.
# Using 9pfuse
9pfuse localhost:5640 /mnt/llm
# Using Plan 9's 9p tool (no mount needed)
9p -a localhost:5640 ls llm
9p -a localhost:5640 read llm/model
9p -a localhost:5640 read llm/temperature
9p -a localhost:5640 write llm/ask "What is 2+2?"
9p -a localhost:5640 read llm/ask # Returns "4"
9p -a localhost:5640 read llm/tokens # Returns token count
# Multi-turn conversation
9p -a localhost:5640 write llm/ask "Remember the number 42"
9p -a localhost:5640 write llm/ask "What number did I just mention?"
9p -a localhost:5640 read llm/ask # Returns "42"
# Add system message (e.g., persona)
9p -a localhost:5640 write llm/context "Respond like a pirate"
9p -a localhost:5640 write llm/ask "Hello"
9p -a localhost:5640 read llm/ask # Pirate-style response
# Reset conversation
9p -a localhost:5640 write llm/new "reset"
# Start infernode (from infernode directory)
./emu
# Inside infernode shell:
mkdir /n/llm
mount -A tcp!127.0.0.1!5640 /n/llm
ls -l /n/llm
cat /n/llm/model
echo 'What is the capital of France?' > /n/llm/ask
cat /n/llm/ask
Infernode Notes:
127.0.0.1 not localhost (DNS resolution differs)mkdir /n/llm-A flag enables anonymous auth"file not found"
"permission denied"
Connection refused
API errors
Files should handle errors gracefully and expose them to the user:
func (f *AskFile) Write(p []byte, offset int64) (int, error) {
response, err := f.client.Ask(ctx, prompt)
if err != nil {
// Store error so it can be read back
f.lastResponse = "Error: " + err.Error()
return len(p), nil
}
f.lastResponse = response
return len(p), nil
}
Always implement Stat() to return accurate Length:
func (f *MyFile) Stat() protocol.Stat {
s := f.BaseFile.Stat()
s.Length = uint64(len(f.content))
return s
}
The following scenarios have been tested and verified working:
ls llm - List filesystem rootread llm/model - Returns model nameread llm/temperature - Returns temperatureread llm/tokens - Returns 0 initially, updates after queriesread llm/_example - Returns usage exampleswrite llm/temperature "0.5" - Updates temperature settingwrite llm/ask "What is 2+2?" followed by read llm/ask - Returns "4"write llm/context "Respond like a pirate" - System message workswrite llm/new "reset" - Clears conversation historymount -A tcp!127.0.0.1!5640 /n/llm - Mounts successfullyls -l /n/llm - Lists all files with correct permissionscat /n/llm/model - Returns model namecat /n/llm/temperature - Returns temperatureecho 'prompt' > /n/llm/ask followed by cat /n/llm/ask - Full LLM interaction works