cat / 012
clone

a spreadsheet for the terminal: lotus 1-2-3 looks, google sheets keys and formulas, one go binary

main
012 / demos / liveagent / main.go
main.go 4.0K
1// Command liveagent is the agent of the live-mode tape (live.tape): it2// writes a household budget, then attaches to the 012 listening on it3// with 012 mcp --attach, as a host would, and works through a script:4// it points at the bills, suggests March's figures, waits for the5// person to settle them, asks whether to add a line for the gym and,6// told yes, suggests it.7package main89import (10 "bufio"11 "encoding/json"12 "flag"13 "fmt"14 "os"15 "os/exec"16 "time"17)1819const budget = `{"version": 2, "cells": {"A1": "Household budget", "A3": "Rent", "B3": "1200", "A4": "Food", "B4": "450",20 "A5": "Power", "B5": "90", "A6": "Total", "B6": "=SUM(B3:B5)"}}`2122func main() {23 write := flag.String("write", "", "write the budget to this file and exit")24 bin := flag.String("012", "../bin/012", "the 012 to attach with")25 flag.Parse()26 if *write != "" {27 if err := os.WriteFile(*write, []byte(budget), 0o644); err != nil {28 fail(err)29 }30 return31 }32 a, err := attach(*bin)33 if err != nil {34 fail(err)35 }36 a.script()37}3839func fail(err error) {40 fmt.Fprintln(os.Stderr, "liveagent:", err)41 os.Exit(1)42}4344// agent speaks MCP's JSON-RPC to 012 mcp --attach.45type agent struct {46 in *json.Encoder47 out *bufio.Reader48 next int49}5051// attach starts 012 mcp --attach once a session listens, and52// initializes the connection as the host called claude.53func attach(bin string) (*agent, error) {54 time.Sleep(3 * time.Second) // the session starts listening55 cmd := exec.Command(bin, "mcp", "--attach")56 in, err := cmd.StdinPipe()57 if err != nil {58 return nil, err59 }60 out, err := cmd.StdoutPipe()61 if err != nil {62 return nil, err63 }64 if err := cmd.Start(); err != nil {65 return nil, err66 }67 a := &agent{in: json.NewEncoder(in), out: bufio.NewReader(out)}68 a.call("initialize", map[string]any{"protocolVersion": "2025-06-18", "capabilities": map[string]any{},69 "clientInfo": map[string]any{"name": "claude", "version": "1"}})70 a.in.Encode(map[string]any{"jsonrpc": "2.0", "method": "notifications/initialized"})71 return a, nil72}7374// call sends a request and returns its result.75func (a *agent) call(method string, params any) map[string]any {76 a.next++77 a.in.Encode(map[string]any{"jsonrpc": "2.0", "id": a.next, "method": method, "params": params})78 for {79 line, err := a.out.ReadBytes('\n')80 if err != nil {81 fail(err)82 }83 var msg struct {84 ID int `json:"id"`85 Result map[string]any `json:"result"`86 Error json.RawMessage `json:"error"`87 }88 if json.Unmarshal(line, &msg) != nil || msg.ID != a.next {89 continue90 }91 if msg.Error != nil {92 fail(fmt.Errorf("%s: %s", method, msg.Error))93 }94 return msg.Result95 }96}9798// tool calls a tool and returns its structured result.99func (a *agent) tool(name string, args map[string]any) map[string]any {100 res := a.call("tools/call", map[string]any{"name": name, "arguments": args})101 out, _ := res["structuredContent"].(map[string]any)102 return out103}104105// settled waits until none of the agent's suggestions is pending.106func (a *agent) settled() {107 for {108 list, _ := a.tool("suggestions", nil)["suggestions"].([]any)109 pending := false110 for _, s := range list {111 if s.(map[string]any)["state"] == "pending" {112 pending = true113 }114 }115 if !pending {116 return117 }118 time.Sleep(300 * time.Millisecond)119 }120}121122func (a *agent) script() {123 a.tool("read_range", map[string]any{"ref": "A1:B6"})124 a.tool("focus", map[string]any{"ref": "B3:B5"})125 time.Sleep(1500 * time.Millisecond)126 a.tool("write_cells", map[string]any{"message": "March's bills came in higher",127 "entries": []map[string]any{{"ref": "B4", "input": "480"}, {"ref": "B5", "input": "95"}}})128 a.settled()129 time.Sleep(1200 * time.Millisecond)130 answer := a.tool("ask", map[string]any{"message": "Add a line for the gym?"})131 if answer["action"] != "accept" {132 return133 }134 a.tool("apply_operations", map[string]any{"message": "the gym, as you said", "operations": []map[string]any{135 {"op": "insert_rows", "ref": "A6"},136 {"op": "set", "ref": "A6", "input": "Gym"},137 {"op": "set", "ref": "B6", "input": "40"}}})138 a.settled()139 time.Sleep(3 * time.Second)140}