cat / 012
clone

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

Live mode tape: a scripted agent (demos/liveagent) suggests, asks and is accepted; the README lists live mode

Dami committed 45e3759
commit
45e37596168be8a6bdf2b413b6dcce7e578df516
parent
7f52fda7e706a4dfee4a2a0b6b3115e03532abb7
author
Dami <dami.etoile@gmail.com>
kind
feat risk score 0.3 of 3, Jev's estimate
5 files changed, +196 -0
MREADME.md4+
Ademos/live.tape46++++++++++
Ademos/liveagent/main.go140++++++++++++++++++++++++++++++
Mdocs/agents/live.md6+
Adocs/media/live.gifbin
M README.md +4 -0
A demos/live.tape +46 -0
A demos/liveagent/main.go +140 -0
@@ -0,0 +1,140 @@
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.7+package main8+9+import (10+ "bufio"11+ "encoding/json"12+ "flag"13+ "fmt"14+ "os"15+ "os/exec"16+ "time"17+)18+19+const 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)"}}`21+22+func 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+}38+39+func fail(err error) {40+ fmt.Fprintln(os.Stderr, "liveagent:", err)41+ os.Exit(1)42+}43+44+// agent speaks MCP's JSON-RPC to 012 mcp --attach.45+type agent struct {46+ in *json.Encoder47+ out *bufio.Reader48+ next int49+}50+51+// attach starts 012 mcp --attach once a session listens, and52+// initializes the connection as the host called claude.53+func 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+}73+74+// call sends a request and returns its result.75+func (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+}97+98+// tool calls a tool and returns its structured result.99+func (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+}104+105+// settled waits until none of the agent's suggestions is pending.106+func (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+}121+122+func (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+}
M docs/agents/live.md +6 -0
A docs/media/live.gif +0 -0

Binary file.