- commit
- 45e37596168be8a6bdf2b413b6dcce7e578df516
- parent
- 7f52fda7e706a4dfee4a2a0b6b3115e03532abb7
- author
- Dami <dami.etoile@gmail.com>
- kind
- feat risk score 0.3 of 3, Jev's estimate
| M | README.md | 4 | + |
| A | demos/ | 46 | ++++++++++ |
| A | demos/ | 140 | ++++++++++++++++++++++++++++++ |
| M | docs/ | 6 | + |
| A | docs/ | bin |
@@ -57,6 +57,10 @@ rest: building from a clone, importing files, `012 serve` and settings.
5757 with JSON results, a Claude Code skill, and `012 mcp`, an MCP server5858 whose writes go through the same checks as yours, with views5959 of ranges and charts in the chat ([agents](docs/agents/README.md)).60+- **Coworking with an agent**: `012 --listen` lets the agent into your61+ running 012, its pointer and name in the grid, its changes suggestions62+ you accept or reject cell by cell, within the scope you give it63+ ([live mode](docs/agents/live.md)).6064 - **A stage in a pipeline**: `ls | sheet | where size > 1kb` edits a6165 table on the terminal and sends it on with its types, through the6266 nushell module `012 nu --install-module` installs, or as@@ -0,0 +1,46 @@
1+# Live mode: a household budget open with 012 --listen, and an agent,2+# claude (liveagent, scripted), attached with 012 mcp --attach. Claude3+# points at the bills and suggests March's figures; its cells carry a4+# ◇ and the context line says what it would put there. The person5+# reviews and accepts them, answers claude's question on the context6+# line, and accepts the line it then suggests.7+Output out/live.gif8+Source lib/setup.tape9+10+Hide11+Type "export XDG_RUNTIME_DIR=$(mktemp -d) && go build -o ../bin/liveagent ./liveagent && ../bin/liveagent -write out/budget.012 && (../bin/liveagent >/dev/null 2>&1 &) && clear && ../bin/012 --listen out/budget.012" Enter12+Wait+Screen@60s /READY/13+Show14+15+Wait+Screen@20s /claude/16+Sleep 1.5s17+Wait+Screen@20s /1 suggestion/18+Sleep 800ms19+Down 320+Right21+Sleep 2s22+Screenshot out/stills/live-suggestion.png23+Sleep 1s24+Ctrl+K25+Type@80ms "review suggestions"26+Sleep 400ms27+Enter28+Sleep 1.5s29+Screenshot out/stills/live-review.png30+Sleep 1s31+Enter32+Sleep 1.5s33+Wait+Screen@20s /asks/34+Sleep 1.5s35+Screenshot out/stills/live-ask.png36+Sleep 1s37+Enter38+Wait+Screen@20s /1 suggestion/39+Sleep 1s40+Ctrl+K41+Type@80ms "review suggestions"42+Sleep 400ms43+Enter44+Sleep 1.5s45+Enter46+Sleep 2.5s@@ -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+}@@ -23,6 +23,12 @@ the room takes them, each attributed to whoever made it, so undo takes
2323 back only your own and `012 diff` shows everyone's. The agent uses the2424 [MCP server's](mcp.md) tools, on this one workbook.2525 26+Claude, attached to a household budget, points at the bills and27+suggests March's figures; you review and accept them, answer its28+question, and accept the line it then suggests:29+30+31+2632 ## Starting2733 2834 Let agents in when you start, or from a session already running:Binary file.