Interactive CLIs are built for humans and agents need the opposite
Interactive CLIs are designed for humans; they use prompts, menus, spinners, colored output, and readline for editing. AI agents (Claude Code, Cursor, etc.) need a different set of properties:
- Deterministic input; no prompts, no "press enter to continue"
- Structured output; JSON, not colored text with spinners
- One-shot commands; each invocation does one thing and exits
- State persistence; the connection must survive across commands
KEIBIDROP already had an interactive CLI (keibidrop-cli) and a desktop GUI, and neither worked for agents, so we needed a third interface.
Every command prints one JSON line, and the daemon calls the library directly
kd show fingerprint prints JSON and exits, and every response is
{"ok":true,"data":{...}} or {"ok":false,"error":"..."}.
The daemon calls kd.CreateRoom() and kd.AddFile() directly,
with no abstraction layers in between. State lives in the daemon process across commands.
FUSE mode is recommended so agents can read/write files via normal file I/O.
The daemon runs in the foreground and everything else talks to it over a Unix socket
kd start runs a foreground daemon that initializes KEIBIDROP and listens on
a Unix socket (default: /tmp/kd.sock). All other commands are thin clients
that connect to the socket, send a JSON request, and print the JSON response.
The daemon dispatches commands directly to Go functions. There is no HTTP server, no REST API, no protocol layer beyond minimal JSON framing:
// cmd/kd/main.go
func dispatch(kd *common.KeibiDrop, req Request, ...) Response {
switch req.Command {
case "show":
return cmdShow(kd, req.Args)
case "register":
kd.AddPeerFingerprint(req.Args[0])
return okResponse(map[string]any{"registered": req.Args[0]})
case "create":
return cmdCreateOrJoin(kd, "create") // calls kd.CreateRoom()
case "join":
return cmdCreateOrJoin(kd, "join") // calls kd.JoinRoom()
case "add":
kd.AddFile(req.Args[0])
return okResponse(map[string]any{"added": req.Args[0]})
case "list":
return cmdList(kd) // reads kd.SyncTracker.LocalFiles/RemoteFiles
case "status":
return cmdStatus(kd) // includes mount_path, save_path
case "disconnect":
// ...
case "stop":
// ...
}
}
Each command maps to one or two function calls on the KEIBIDROP instance. No abstraction layers, no middleware, no request validation beyond basic argument counts.
A Unix socket avoids ports, TLS, CORS and auth headers
Unix sockets are simpler for local-only communication. There are no port conflicts because they are socket files, no TLS is needed for local IPC, and there are no CORS rules or auth headers, since file permissions control access. This is the same pattern Docker uses with /var/run/docker.sock.
An agent that knows ls and cat needs no API at all
The recommended agent workflow uses FUSE. After connecting, the agent treats the mount path as a regular directory:
# Start daemon with FUSE
KD_SAVE_PATH=./saved KD_MOUNT_PATH=./mount ./kd start
# Connect to peer
./kd register <peer-fp>
./kd create
# After connecting, use the mount as a normal folder
ls ./mount/ # see peer's shared files
cat ./mount/readme.txt # read a remote file
cp ./myfile.pdf ./mount/ # share a file with peer
kd add
or kd pull; just use normal file I/O. This is why FUSE mode is recommended
for agents: fewer API calls, standard tools, no learning curve.
Here is a full session, with the JSON from a real run
A complete session with actual JSON output from a real test run:
$ KD_SAVE_PATH=./saved KD_NO_FUSE=1 KD_SOCKET=/tmp/kd.sock ./kd start
{"ok":true,"data":{"fingerprint":"Ea5btbne8xIJ5BRu...","fuse":false,
"ip":"2a02:2f00:...","socket":"/tmp/kd.sock"}}
$ ./kd show fingerprint
{"ok":true,"data":{"fingerprint":"Ea5btbne8xIJ5BRu..."}}
$ ./kd register "a3qR2sdQ_sCR-8Xhi..."
{"ok":true,"data":{"registered":"a3qR2sdQ_sCR-8Xhi..."}}
$ ./kd create
{"ok":true,"data":{"peer_ip":"2a02:2f00:...","status":"connected"}}
$ ./kd status
{"ok":true,"data":{"connection_status":"healthy","fingerprint":"Ea5b...",
"peer_fingerprint":"a3qR...","local_files":0,"remote_files":1,...}}
$ ./kd list
{"ok":true,"data":{"files":[{"name":"/rmg.img","size":318767104,
"path":"","source":"remote"}]}}
$ ./kd pull "/rmg.img" "./saved/rmg.img"
{"ok":true,"data":{"pulled":"/rmg.img","to":"./saved/rmg.img"}}
$ ./kd disconnect
{"ok":true,"data":{"new_fingerprint":"-y-dYaTF...","status":"disconnected"}}
$ ./kd stop
{"ok":true,"data":{"status":"stopped"}}
You can run two peers on one machine if you give each its own ports and socket
For testing on a single machine, use different ports and sockets:
# Alice
KD_SAVE_PATH=./SaveAlice KD_NO_FUSE=1 \
KD_INBOUND_PORT=26001 KD_OUTBOUND_PORT=26002 \
KD_SOCKET=/tmp/kd-alice.sock ./kd start
# Bob
KD_SAVE_PATH=./SaveBob KD_NO_FUSE=1 \
KD_INBOUND_PORT=26003 KD_OUTBOUND_PORT=26004 \
KD_SOCKET=/tmp/kd-bob.sock ./kd start
# Connect them
KD_SOCKET=/tmp/kd-alice.sock ./kd register <bob-fp>
KD_SOCKET=/tmp/kd-bob.sock ./kd register <alice-fp>
KD_SOCKET=/tmp/kd-alice.sock ./kd create &
KD_SOCKET=/tmp/kd-bob.sock ./kd join
Agents prefer FUSE, and JSON output has to be one line
Agents prefer FUSE, because standard file I/O means fewer API calls; an agent that knows
ls and cat can use KEIBIDROP without reading documentation.
JSON output must be a single line. Agents parse output with jq or
json.loads(), and multi-line JSON or mixed text/JSON output breaks parsing.
Blocking commands need background execution. kd create blocks until the peer
joins, so agents must run it with & or a timeout.
The entire CLI is one Go file: cmd/kd/main.go, 520 lines, covering the daemon, the client, the protocol, dispatch and the help text. No framework, no external dependencies beyond the KEIBIDROP core library.
The full agent integration guide with all commands, environment variables, and JSON output examples is at docs/kd-agent-guide.md.