CODE_GUIDE.md

March 18, 2026 · View on GitHub

This guide explains how the code fits together, provides a recommended reading order for newcomers, and documents protocols and architectural invariants. It is intended for both human contributors and AI coding agents.

For using the tool as an AI agent (tool descriptions, example invocations, MCP tool schemas), see AGENTS.md.

(Side-note: I intend to maintain this file via AI, and an AI wrote it.)


Table of Contents

  1. Project Summary
  2. Reading Order for Comprehension
  3. Package Map
  4. Data Flow
  5. Command Registration Pattern
  6. Key Types and Relationships
  7. Generated Code and go generate
  8. Architectural Invariants and Pitfalls
  9. Protocols
  10. External Dependencies
  11. Testing
  12. Build and Run

Project Summary

character is a Go CLI tool for Unicode codepoint lookup, transformations, and encoding information. It is built on Cobra and has no runtime server — except when run as character agent mcp, which starts an MCP stdio server exposing eight Unicode lookup tools.

License: MIT. Copyright Phil Pennock.


Reading Order for Comprehension

For understanding the codebase end-to-end, read in this order:

#File(s)Why
1main.goEntry point; shows all command imports and go:generate directives
2commands/root/root.goRoot Cobra command; AddCommand, Start, Cobra exports
3sources/sources.goSources struct — the data aggregator everything depends on
4unicode/unicode.goCharInfo, Unicode, Load, LoadSearch
5unicode/blocks.goBlockInfo, Blocks, LookupInfo, FindByName
6unicode/category.goGeneralCategory(r rune) string
7unicode/emoji.goPresentationVariants, Emojiable
8internal/uformat/uformat.goPure byte-formatting helpers shared by CLI and MCP output
9resultset/resultset.goResultSet, JItem, JSON rendering — CLI output backbone
10resultset/cmdrender.goResultCmdFlags, flag registration, RenderPerCmdline
11commands/name/name.goA simple command — shows the typical command pattern
12internal/mcpstdio/mcpstdio.goMCP stdio server (~200 lines, hand-rolled)
13commands/agent/mcpserver/charprops.goCharProps, CharPropsFromRune
14commands/agent/mcpserver/tools.goEight MCP tool handlers
15commands/agent/mcpserver/server.goMCP server wiring
16commands/agent/agentmcp.goagent mcp command — ties it all together

After reading these 16 files you will understand every major subsystem.


Package Map

github.com/philpennock/character/

├── main.go                            Entry point, go:generate directives
├── repo_version.go                    Build-time version info

├── commands/                          Cobra command implementations
│   ├── root/                          Root command, AddCommand(), Start()
│   ├── name/                          `name <char>…`  — info about literal characters
│   ├── named/                         `named <NAME>`  — lookup by Unicode name
│   ├── code/                          `code <U+XXXX>` — lookup by codepoint
│   ├── browse/                        `browse -b <block>` — list block contents
│   ├── known/                         `known -b` — list block/charset names
│   ├── aliases/                       `aliases` — alias characters
│   ├── puny/                          `x-puny` — punycode encode/decode
│   ├── region/                        `region <CC>` — flag emoji
│   ├── transform/                     `transform <type>` — fraktur, math, scream, turn
│   ├── version/                       `version` — version info
│   ├── deprecated/                    Deprecated command stubs
│   └── agent/                         Agent sub-command tree
│       ├── agent.go                     Parent `agent` command
│       ├── agenthelp.go                 `agent help` — JSON schema of all commands
│       ├── agentexamples.go             `agent examples` — example invocations
│       ├── agentmcp.go                  `agent mcp` — start MCP stdio server
│       └── mcpserver/                   MCP tool implementations
│           ├── server.go                  Server wrapper + NewServer
│           ├── charprops.go               CharProps struct, CharPropsFromRune
│           ├── schemas.go                 JSON Schema constants for each tool
│           └── tools.go                   registerTools, 8 handler closures

├── unicode/                           Unicode data and lookups
│   ├── unicode.go                       CharInfo, Unicode struct, Load/LoadSearch
│   ├── blocks.go                        BlockInfo, Blocks, LookupInfo, FindByName
│   ├── category.go                      GeneralCategory()
│   ├── emoji.go                         PresentationVariants(), Emojiable()
│   ├── regional.go                      Regional indicator helpers
│   ├── sort.go                          Sort interface for CharInfo slices
│   ├── generated_data.go               ← go generate (character name maps)
│   ├── generated_blocks.go             ← go generate (block ranges)
│   └── generated_emoji.go              ← go generate (emojiable/textable sets)

├── sources/                           Data source aggregation
│   ├── sources.go                       Sources struct, NewFast(), NewAll()
│   ├── vim.go                           VimDigraph, VimData, digraph loaders
│   ├── x11.go                           X11Data, compose sequence loader
│   ├── generated_static_vim.go         ← go generate
│   └── generated_x11_compose.go        ← go generate

├── entities/                          HTML/XML entity lookup
│   ├── generated_html.go              ← go generate (HTMLEntities, reverse map)
│   └── generated_xml.go               ← go generate (XMLEntities, reverse map)

├── resultset/                         CLI result rendering
│   ├── resultset.go                     ResultSet, JItem, Add*, PrintJSON, PrintPlain
│   └── cmdrender.go                     ResultCmdFlags, RegisterCmdFlags, RenderPerCmdline

├── internal/
│   ├── mcpstdio/                      Hand-rolled MCP stdio server
│   │   └── mcpstdio.go                  Server, Handler, ToolDef, readFrame, writeFrame
│   ├── uformat/                       Pure rune → string formatting helpers
│   │   └── uformat.go                   UTF8Bytes, UTF8Escaped, UnicodeEscaped, etc.
│   ├── runemanip/                     Rune manipulation utilities
│   │   ├── runes.go                     RuneFromHexField
│   │   ├── hexDecode.go                 HexDecodeArgs
│   │   ├── widths.go                    DisplayCellWidth
│   │   ├── regional.go                  Regional indicator helpers
│   │   └── variations.go               Variation selector helpers
│   ├── table/                         Table rendering abstraction
│   │   └── tabular.go                   NewTable, Supported
│   ├── clipboard/                     Clipboard I/O (conditional build)
│   └── encodings/                     Charset decoders

├── extra/                             Extra data files, web assets

└── util/                              Build-time code generators + tools
    ├── update_unicode.go                Generate unicode/generated_*.go
    ├── update_entities.go               Generate entities/generated_*.go
    ├── update_x11_compose.go            Generate sources/generated_x11_compose.go
    ├── update_static_vim               Bash: generate sources/generated_static_vim.go
    └── mcp_test_driver                 Python: interactive MCP REPL for testing

Data Flow

CLI lookup (e.g. character name -J ✓)

main.go → root.Start()
  → Cobra dispatch → commands/name.Run
    → sources.NewFast()           load all static data (~1ms)
    → resultset.NewResultSet(srcs)
    → rs.AddCharacterByRune(r)    populate from Sources
    → rs.RenderPerCmdline()       dispatch on ResultCmdFlags
      → rs.PrintJSON()            marshal JItem structs

MCP server (e.g. character agent mcp)

main.go → root.Start()
  → Cobra dispatch → commands/agent/agentmcp.Run
    → sources.NewFast()
    → srcs.LoadUnicodeSearch()    build Ferret index (~100-300ms)
    → mcpserver.NewServer(srcs)
      → mcpstdio.NewServer("character", version)
      → registerTools(srv, srcs)  register 8 tool handlers
    → srv.ServeStdio(ctx)         read stdin, dispatch, write stdout
      → readFrame (newline-delimited JSON)
      → dispatch on method: initialize | tools/list | tools/call | …
      → handler(ctx, args) → CharPropsFromRune(r, srcs)
      → writeFrame (JSON + \n)

Sources loading

sources.NewFast()
  = NewEmpty()
      .LoadUnicode()          unicode.Load() → ByRune, ByName maps
      .LoadUnicodeBlocks()    unicode.LoadBlocks() → sorted []BlockInfo
      .LoadStaticVim()        compiled-in vim digraphs
      .LoadStaticX11()        compiled-in X11 compose sequences

sources.NewAll() additionally calls:
      .LoadUnicodeSearch()    Ferret inverted-suffix index (~100-300ms)
      .LoadLiveVim()          runs `vim` subprocess for live digraphs

Command Registration Pattern

Every command package self-registers in init():

// commands/name/name.go
func init() {
    root.AddCommand(nameCmd)
}

main.go imports each command package with a blank import:

import (
    _ "github.com/philpennock/character/commands/name"
    _ "github.com/philpennock/character/commands/named"
    // …
)

This means main.go is the single list of enabled commands. Sub-commands (e.g. agent help, agent mcp) are wired within their parent package's init().

Most commands register shared output flags via:

resultset.RegisterCmdFlags(cmd, supportsOneline)

This adds -v, -N, -J, -1, -c, emoji/text bias flags, etc., and makes them mutually exclusive where needed.


Key Types and Relationships

Sources                             (sources/sources.go)
  ├─ Unicode  unicode.Unicode       map[rune]CharInfo, map[string]CharInfo, Search
  ├─ UBlocks  unicode.Blocks        sorted []BlockInfo
  ├─ Vim      sources.VimData       map[rune][]VimDigraph
  └─ X11      sources.X11Data       map[rune]string

CharInfo                            (unicode/unicode.go)
  {Number rune, Name string, NameWidth int}

BlockInfo                           (unicode/blocks.go)
  {Min rune, Max rune, ID BlockID, Name string}

ResultSet                           (resultset/resultset.go)
  sources *Sources
  items []charItem → JItem (JSON rendering)

JItem                               (resultset/resultset.go)
  CLI JSON output — display-oriented fields, string decimal,
  "block" as string + "block_info" as object

CharProps                           (commands/agent/mcpserver/charprops.go)
  MCP JSON output — structured fields, int decimal,
  "block" as object, no display-oriented fields
  Computed by CharPropsFromRune(r, srcs)

JItem and CharProps are deliberately separate types with different JSON contracts. Both use internal/uformat for shared byte-formatting.


Generated Code and go generate

Run go generate ./... from the repo root to regenerate all static data.

GeneratorInputOutput
util/update_unicode.gounicode/UnicodeData.txt, unicode/Blocks.txt, unicode/emoji-variation-sequences.txtunicode/generated_data.go, unicode/generated_blocks.go, unicode/generated_emoji.go
util/update_entities.goHTML/XML entity specsentities/generated_html.go, entities/generated_xml.go
util/update_x11_compose.gosources/Compose.en_US.UTF-8.txtsources/generated_x11_compose.go
util/update_static_vim (bash)Runs vimsources/generated_static_vim.go

Generated files are committed to the repository and should not be hand-edited. After modifying a generator, re-run go generate and commit the regenerated output alongside the generator change.


Architectural Invariants and Pitfalls

ResultCmdFlags is global mutable state

resultset.ResultCmdFlags is a package-level struct populated by Cobra flag parsing. It drives CLI rendering decisions.

Invariant: CharPropsFromRune (MCP path) must never read ResultCmdFlags. The MCP server does not import resultset at all; this is enforced by Go's package dependency graph.

JItem backward compatibility

New fields in JItem use omitempty. The block object uses JSON key "block_info" to coexist with the legacy "block" string field. In CharProps (MCP), there is no legacy, so "block" is the structured object.

uformat is the single source of truth for byte-formatting

Both resultset.JSONEntry and mcpserver.CharPropsFromRune delegate byte-formatting to internal/uformat. Do not duplicate these computations.

MCP search index startup cost

srcs.LoadUnicodeSearch() builds a Ferret inverted-suffix index, taking ~100–300 ms. agent mcp calls it eagerly at startup because MCP servers are long-lived and first-request latency matters more than startup latency.

CJK Unified Ideographs gap

CJK Unified Ideographs (U+4E00–U+9FFF) are absent from srcs.Unicode.ByRune because the Unicode standard does not assign individual names to that range. unicode_browse_block("CJK Unified Ideographs") returns zero results. This matches the CLI behaviour and is not a bug.

JSON-RPC id field type

JSON-RPC 2.0 allows id to be a string, number, or null. The MCP server preserves the raw id as json.RawMessage and echoes it unchanged. Do not unmarshal it into interface{} and re-marshal — that risks converting bare integers to floats via Go's default JSON decoder.


Protocols

This section documents wire protocols used or referenced by the codebase, with summaries and pointers to authoritative specifications.

MCP — Model Context Protocol (stdio transport)

Used by: character agent mcpinternal/mcpstdio

MCP is a protocol for exposing tools to AI agents. The character tool implements a tool-only MCP server over the stdio transport.

Wire format

MCP stdio uses newline-delimited JSON (NDJSON). Each message is a single JSON-RPC 2.0 object on one line terminated by \n. Messages MUST NOT contain embedded newlines.

→ {"jsonrpc":"2.0","id":1,"method":"initialize","params":{...}}\n
← {"jsonrpc":"2.0","id":1,"result":{...}}\n

This is NOT the same as LSP framing (see below).

Required methods (tool-only server)

MethodDirectionResponse
initializeclient → serverInitializeResult (capabilities, serverInfo, protocolVersion)
notifications/initializedclient → serverNone (notification, no id)
tools/listclient → server{"tools": [...]}
tools/callclient → server{"content": [...], "isError": bool}

References

LSP — Language Server Protocol

Not used by this project, but referenced here because the MCP stdio transport is frequently confused with LSP's wire format.

LSP uses Content-Length framing over stdio:

Content-Length: 52\r\n
\r\n
{"jsonrpc":"2.0","id":1,"method":"initialize",...}

Each message is preceded by HTTP-style headers (Content-Length: N\r\n\r\n), then exactly N bytes of body. The body may contain newlines.

Key difference from MCP: LSP uses Content-Length + \r\n\r\n; MCP uses bare \n-delimited lines. Do not mix them up.

References

JSON-RPC 2.0

Both MCP and LSP build on JSON-RPC 2.0.

  • Requests have jsonrpc, method, id, and optional params.
  • Notifications have jsonrpc and method but no id — no response is sent.
  • Responses have jsonrpc, id, and either result or error.
  • Error objects have code (integer) and message (string).
  • Standard error codes: -32700 (parse error), -32600 (invalid request), -32601 (method not found), -32602 (invalid params), -32603 (internal error).

References


External Dependencies

ModulePurposeWhy
github.com/spf13/cobraCLI frameworkCommand tree, flag parsing, help generation
github.com/spf13/pflagPOSIX flag parsingCobra dependency; also used directly for flag introspection
github.com/argusdusty/FerretInverted-suffix indexSubstring search over ~35k Unicode character names
github.com/atotto/clipboardClipboard I/O-c flag: copy result to clipboard
github.com/mattn/go-runewidthTerminal cell widthCorrect column alignment in table output
github.com/mattn/go-shellwordsShell word splitting--argv flag: re-parse arguments
go.pennock.tech/tabularTable rendering-v verbose table output
golang.org/x/netIDN / punycodex-puny command
golang.org/x/textUnicode normalisationNFC/NFD handling
github.com/liquidgecka/testlibTest assertion helpersTest-only

No MCP SDK is used; internal/mcpstdio is ~200 lines of hand-rolled code.


Testing

go test ./...                           # all tests
go test ./internal/mcpstdio/            # MCP protocol tests
go test ./commands/agent/mcpserver/     # MCP tool handler tests
go test ./internal/uformat/            # formatting helper tests
go test ./unicode/                     # unicode data tests
go test ./resultset/                   # CLI rendering tests

The util/mcp_test_driver script is a Python REPL for interactively testing the MCP server end-to-end. It builds the binary, performs the MCP handshake, and lets you call tools from a prompt:

./util/mcp_test_driver
mcp> list
mcp> unicode_lookup_char char=✓
mcp> unicode_search query=snowman

Build and Run

# Build
go build -o character .

# Run
./character name
./character named -Jj CHECK MARK
./character agent mcp                 # start MCP server on stdio

# Regenerate data (after updating Unicode source files or generators)
go generate ./...

# Format
gofmt -w .