GitHub - yasyf/cc-interact: Never write a daemon for a Claude Code tool again · GitHub
Skip to content

yasyf/cc-interact

Repository files navigation

cc-interact

Never write a daemon for a Claude Code tool again. cc-interact extracts cc-review's daemon, event log, SSE, edit-gate, and MCP plumbing into a reusable Go framework; the echo example is 381 lines.

CI Version License: PolyForm-Noncommercial-1.0.0

Get started

go get github.com/yasyf/cc-interact

Terminal running the examples/echo round trip — a curl POST appends echo.item and the agent's MCP reply streams back as echo.reply on /events

That run is examples/echo, the whole framework exercised in 381 lines: two domain handlers, one REST mount, one MCP tool. A human POSTs an item, the agent replies through its channel, and both events come back off the same /events plane a browser would read. Regenerate the capture with docs/scripts/demo.sh.

Driving with an agent? Paste this:

Run `go get github.com/yasyf/cc-interact` in my Go project.
Study https://github.com/yasyf/cc-interact/tree/main/examples/echo — a complete headless consumer in 381 lines — then stand up a human-in-the-loop surface for my domain: one reducer, start/reply handlers, and one MCP channel tool.
Verify the round trip: POST an item to the daemon's REST plane and confirm the agent's reply streams back over /events.

Use cases

Block agent edits until a human signs off

A review tool that can't stop the agent from editing mid-review is a suggestion box. Inject the verdict as one function:

daemon.Config{
	// cc-review: block while a review is open.
	Gate: func(ctx context.Context, s subject.Subject, tool daemon.ToolCall) (bool, string) {
		return s.Status != "open", "a human is still reviewing — reply to their comments first"
	},
	GateErrorReason: "review state unreadable; blocking edits",
}

The guard-edit hook routes every edit the Claude session attempts through this verdict: while the subject is open, Claude sees your reason instead of a completed write. Errors reading the subject fail closed (GateErrorReason), and a missing daemon fails open — a crashed daemon never bricks the session.

Feed one gap-free event log to the browser and the agent alike

Two realtime paths — a socket for the UI, polling for the agent — drift, drop events, and disagree after a reconnect. Here both roles read one log:

go run ./examples/echo watch

watch streams the same append-only log as the browser's /events, one JSON event per line, with exclude_origin=agent so the agent never reacts to its own echo. Delivery is at-least-once with a persisted per-consumer cursor, so a dropped connection resumes where it left off, and the consumer survives a daemon swap.

Ship your agent surface as a Claude Code plugin

The binary is half the ship surface; the plugin payload — MCP server wiring, binary installer, session hooks, a start skill — is its own pile of boilerplate. Render it instead:

./plugin-template/render.sh out/ PLUGIN_NAME=mytool DISPLAY_NAME=MyTool \
  BINARY_NAME=mytool RELEASE_REPO=you/mytool MCP_SUBCOMMAND=channel \
  SKILL_NAME=mytool:start

You get cc-review's plugin payload with the review strings swapped for yours: the channel MCP server wiring, a release-binary installer, session-record and guard-edit hooks, and a start skill. plugin-template/ documents every variable.

What the framework owns

One process model, shipped: a lazily spawned daemon owns a single-writer SQLite (WAL) holding an append-only per-subject event log; every consumer — browser, watch, MCP channel — reads the same SSE plane; newest-wins eviction upgrades the daemon in place when a newer binary lands. You register domain ops against a generic control envelope and the framework routes the rest.

Package Owns
daemon lazy spawn, newest-wins eviction, peer-credential identity, the op registry, the edit gate
store pure-Go SQLite: subjects plus the event log; your tables via a migrate hook
event the log-entry type and the per-subject pub/sub wakeup
sse the HTTP /events plane: at-least-once SSE with Last-Event-ID resume
consume the agent-side SSE client with a persisted resume cursor
channel the stdio MCP server: tools in, notifications out
cmd ready-made cobra constructors: daemon, watch, status, stop, plus the hidden hook and channel entry points
subject ownership: one subject per window and scope, stable across /clear and compaction
paths the ~/.<app> state layout: socket, DB, HTTP handshake, locks
vcs optional: git/jj working-copy snapshots and the per-prompt turn ledger

The browser UI is opt-in

A headless consumer never imports it. When you want a diff-style web client:

npm install @cc-interact/react

Mount sse.StaticHandler on the daemon and the React package's stream and query primitives read the same /events plane as everything else.

State

Everything lives under ~/.<app>/state.db, daemon.sock, http.json, daemon.log. The core schema is versioned by your migrate hook; the echo example carries none, so its reset is rm -rf ~/.cc-echo.


Status: pre-1.0 — the API moves with cc-review, and every break lands in the changelog. Licensed under PolyForm Noncommercial 1.0.0.

About

Never write a daemon for a Claude Code tool again

Resources

License

Stars

Watchers

Forks

Packages

Contributors