goofi v3.0.0

CLI and ops

The same binary is the server and the client of a running server. With no server to talk to it starts one; with one already up, it speaks ops to it: the 73 phrases below, which are also everything the editor and an agent can do.

usage
goofi [--port N] [--bind HOST] [--extra-nodes DIR]
      [--headless] [--debug] [--demo] [--load PATCH]

Flags

FlagDefaultEffect
--port N8000The port to serve on.
--bind HOST127.0.0.1The address to serve on. Any address reachable from other machines prints a warning, because there is no auth and /term is a real shell.
--extra-nodes ROOTnoneA folder of node files — .py, .rs and .wgsl — scanned after the shipped bundles and before the open patch’s own workspace. Repeatable; a later root wins a type name it shares with an earlier one.
--headlessnoneServe the API alone: /control, /data, /term, /mcp. The app’s routes are never mounted.
--debugnoneOpen /dev/*: the UI primitive gallery at /dev/ui, and the other development surfaces. Shut otherwise.
--demononeThe mode a PUBLIC goofi serves in. It disables the terminal, agents, the filesystem, save and load, and audio. It is NOT a sandbox: a param expression is still Python.
--load PATCHnoneThe patch to open on start.

Three flags have environment spellings: GOOFI_HEADLESS=1, GOOFI_DEBUG=1 and GOOFI_DEMO=1. Set GOOFI_HEADLESS for the build and the app is left out of the binary entirely. One door is environment only: GOOFI_BOOT_ONLY=1 boots the node library, prints the type names and exits without serving, which is what a CI job wants.

Client mode

goofi <op phrase> [--json] sends one op to a running server. goofi - reads ops from stdin as one batch, which is one undo step. Every phrase is from the op vocabulary, all 73 of them, with no CLI-only verbs.

goofi node add Psd                    # one op
goofi library list --json             # …as JSON
goofi - < ops.txt                     # a batch from stdin: ONE undo step
goofi completions zsh > _goofi        # a completion script with no vocabulary of its own

goofi completions zsh|bash emits a shell-completion script that holds no vocabulary of its own: every TAB asks the running server, so the script cannot go stale against the binary.

The op vocabulary

Everything goofi can do, it does through these 73 ops. They are what the CLI speaks, phrase for phrase: goofi node param edit … on a command line is the op node param edit. But the CLI is one transport among several. The editor, an MCP agent, a /control message and a test all go through the same entry point, so this list is goofi's one language for manipulating a patch, whichever door you come in by.

The registry is a phrase tree and is rendered as one here: group words carry their own docs, and a leaf op authors only its final word.

read 21 reads state; changes nothing
write 32 undoable, and may ride in a batch
effect 20 runs alone, not undoable and never batched

An argument schema is name:type, with ! marking required and [] a list. Positional says the argument may be given without its name on a command line, in the order shown.

goofi agent 3 ops 1 read2 effect
goofi agent list read

The agents the config offers to launch, and the ones goofi has running.

returns
{
  instances: [
    {
      id,
      harness,
      state,
      exit_code
    }
  ],
  agents: [
    {
      name,
      command
    }
  ],
  config_error: string | null
}
goofi agent start effect
argumenttype
namestringrequired, positional

Launch a config-listed agent on a PTY with the patch workspace as its cwd, under a login shell — a command that cannot launch fails on the PTY itself. Read its terminal at /term/<instance_id>. An unknown name is refused with the config's list.

returns
{
  instance_id: string
}
goofi agent stop effect
argumenttype
instancestringrequired, positional

Stop a running agent (SIGTERM, then SIGKILL), or dismiss one that already exited. The shell's undo stack dies with it; the exit code arrives on harness_changed.

returns
{
  ok: true
}
goofi control 6 ops 1 read4 write1 effect
goofi control list read

Every control panel and the group it draws, and every group holding a widget: each element with its value, its widget (`control`), its lock and what it follows (`source`).

returns
{
  panels: [
    {
      panel,
      group
    }
  ],
  groups: {
    group: {
      lock,
      elements: [
        {
          name,
          element,
          type,
          value,
          control,
          lock,
          source?
        }
      ]
    }
  }
}
goofi control add write
argumenttype
groupstringrequired, positional
kindstringrequired, positional
elementstringoptional
xfloatoptional
yfloatoptional
wfloatoptional
hfloatoptional
valueanyoptional
minfloatoptional
maxfloatoptional
stepfloatoptional
optionsjsonoptional

Bear a widget in a control panel's group: a variable of the kind's own type, carrying the widget. `kind` is knob/slider/number/text/toggle/dropdown/paint. `element` is minted `knob0`, `knob1`, … when not given, and the cell is the first free one when `x`/`y` are not. A config-locked group refuses it, as it refuses every other edit to what it holds.

returns
{
  name,
  control
}

the variable's full name and the widget as stored

goofi control edit write
argumenttype
groupstringrequired, positional
elementstringrequired, positional
namestringoptional
kindstringoptional
minfloatoptional
maxfloatoptional
stepfloatoptional
optionsjsonoptional
xfloatoptional
yfloatoptional
wfloatoptional
hfloatoptional

Change a widget: `name` renames the element (every expression reading it follows), and the rest re-shape the widget, its range, its options or its cell. ONE undo step, and refused by a config lock.

returns
{
  name
}

the element's full name after the edit

goofi control remove write
argumenttype
groupstringrequired, positional
elementstringrequired, positional

Delete a widget and the variable under it. Refused by a config lock.

returns
{
  removed: true
}
goofi control paint effect
argumenttype
groupstringrequired, positional
elementstringrequired, positional
stepsstringrequired

Draw on a `paint` widget with turtle steps — another hand on the pad, not a second painter: the op parses the script and the WIDGET makes the strokes, by the code a mouse reaches. So a pad nobody has open draws nothing, and the reply says how many clients heard it. `steps` is one step per line and the whole block is one submission; `//` to end of line is a comment, and `#` cannot be one because it opens every colour. Coordinates span a 1000 square whatever pixel size the pad is, the origin is the TOP-left with y running down, heading 0 faces +x and `right` turns clockwise. The steps: `forward <d>`, `back <d>`, `left <deg>`, `right <deg>`, `heading <deg>`, `goto <x> <y>`, `home` (the middle, facing +x), `up`, `down`, `curve <c1x> <c1y> <c2x> <c2y> <x> <y>` — a cubic bezier in the turtle's OWN frame, +x along the heading and +y to its right, which it leaves along the curve's exit tangent — `pen <#rgb|#rrggbb|#rrggbbaa|erase>`, `width <w>`, `soft <s>` and `clear`. `pen`, `width` and `soft` are the widget's own colour, size and softness, so a script says what a hand would set.

returns
{
  steps,
  marks,
  clients
}

the steps read, the strokes they make, and how many clients were listening

goofi control source write
argumenttype
groupstringrequired, positional
elementstringrequired, positional
referencestringrequired
indexintoptional

Make a widget FOLLOW one producer output, `node.slot`, with `index` picking one number out of a wide frame — a MIDI controller's `cc` is 128 of them — so a knob on a controller drives the widget. An empty reference clears it. Refused by a config lock.

returns
{
  source: {
    reference,
    index
  } | null
}
goofi dir 2 ops 2 read
goofi dir stat read
argumenttype
pathstringrequired, positional

Resolve a host path and report whether it names a file, a directory, or nothing.

returns
{
  path,
  kind: file | dir | missing
}
goofi dir list read
argumenttype
pathstringoptional, positional
hiddenbooloptional
sortstringoptional
reversebooloptional

List a directory on the goofi host — the save/load browser's read. Without a path it opens the folder of the last patch loaded or saved, else the working directory; `roots` lists Home, the working directory and the five folders patches were last loaded from or saved to (`recent`). Dot-names are left out; `--hidden` includes them. Directories come first; `--sort` is name (the default), modified or size, and `--reverse` turns the order.

returns
{
  path,
  parent,
  entries: [
    {
      name,
      kind,
      is_gfi,
      modified,
      size
    }
  ],
  roots: [
    {
      label,
      path,
      recent
    }
  ]
}

`modified` is epoch milliseconds, `size` bytes and null for a directory

goofi layout 8 ops 1 read6 write1 effect
goofi layout inspect read
argumenttype
tabstringoptional, positional

The arrangement as a tree: every tab, split and panel with its id, order and share of its parent. How a caller discovers the ids every layout op addresses. `tab` narrows it to one tab; no arg = all of them.

returns
{
  text: string
}
goofi layout move write
argumenttype
entrystringrequired, positional
besidestringoptional
sidestringoptional
ratiofloatoptional
instringoptional
indexintoptional
namestringoptional

Move a layout entry — a panel, a whole split's subtree, or a tab; one op per drag gesture, so a drop is one undo step. With `--beside` (and `--side`, `--ratio`) it lands beside that panel. With `--in` it lands inside that split, at `--index` among its children. Bare, a TAB moves to `--index` in the strip, and anything else wraps onto a tab of its own, labelled `--name`. Taking a tab's last panel takes the tab with it.

returns
{
  id,
  tab,
  text
}

what was moved, the tab it is on, and the arrangement as `layout inspect` draws it

goofi layout remove write
argumenttype
entrystringrequired, positional

Close a layout entry: a panel, a whole split's subtree, or a tab and every panel on it. Its space goes to its siblings; a tab keeps its last panel, and the last tab stays.

returns
{
  text
}

the resulting arrangement, as `layout inspect` draws it

goofi layout panel 2 ops 2 write
goofi layout panel add write
argumenttype
besidestringoptional
sidestringoptional
ratiofloatoptional
namestringoptional
indexintoptional

A fresh empty panel. With `--beside` it divides that panel, on its `left`/`right`/`top`/`bottom` (`--side`, default right), taking `--ratio` of its space (default half). Bare, it lands on a new tab at `--index` in the strip, labelled `--name` — minted (`Tab 2`, `Tab 3`, …) unless you give one.

returns
{
  id,
  tab,
  text
}

the born panel, the tab it is on, and the arrangement as `layout inspect` draws it

goofi layout panel edit write
argumenttype
panelstringrequired, positional
typepanel_typeoptional
statejsonoptional

Edit a PANEL's content: its type, its state, or both in one call and one undo. State MERGES key by key — send only what changes, and null to clear a key. A new type clears the old type's state, so send both together to rebind. `type` is one of: empty (a placeholder with no content yet — what a fresh split births); node-editor (the patch canvas — nodes, wires and sub-patches); inspector (the parameters, metadata and errors of one node); viewer (live frames from one output slot, drawn by `state.kind`); console (the patch log; a bound node filters it to that node); variables (the patch variables, which any expression can read); control (knobs, sliders and text widgets over one group of variables); agent (a terminal on an agent harness, running in the patch workspace); recorder (capture the output slots of any node to disk, on one clock). A viewer panel's `state.kind` is one of: line (a time plot: one series (1-D) or one per channel (C, N); or, as a setting, the trajectory over pairs of rows); image (a bitmap: (H, W), or (H, W, C) for 1–4 channels); brain (a scalp map of one scalar per channel (1-D), or the connectivity between channels (C, C) as a ring or in 3-D); string (the text of a STRING slot); table (the rows of a TABLE slot); a STRING or TABLE slot ignores it and uses its own.

returns
{
  text
}

the resulting arrangement, as `layout inspect` draws it

goofi layout tab 1 op 1 write
goofi layout tab edit write
argumenttype
tabstringrequired, positional
namestringrequired

Relabel a TAB. Its id and every panel on it stand; the strip index is where it sits, which `layout move` owns.

returns
{
  text
}

the resulting arrangement, as `layout inspect` draws it

goofi layout split 1 op 1 write
goofi layout split edit write
argumenttype
splitstringrequired, positional
fractionfloat, a listrequired

Set the shares of ALL of a SPLIT's children at once, in child order — what a resize drag commits. Renormalized to fill the slot.

returns
{
  text
}

the resulting arrangement, as `layout inspect` draws it

goofi layout viewpoint 1 op 1 effect
goofi layout viewpoint edit effect
argumenttype
valuejsonrequired

Store where this client is looking — active tab, maximize, camera, each panel's sub-patch path. ONE stored value, replaced whole, last writer wins; persisted in the `.gfi`, never converged, never dirtying.

returns
{
  ok: true
}
goofi library 4 ops 2 read2 effect
goofi library list read
argumenttype
fullbooloptional

The node library as an INDEX: every registered type, and the first line of its doc. Everything else about the one type you then pick — where it came from, its slots, its params, the rest of its doc — is `library get`'s, so choosing from this list costs a catalog and not a manual. `--full` answers the palette a client draws the add-menu from instead.

returns
{
  types: [
    {
      type,
      doc
    }
  ]
}

an unloadable one also carrying `available: false` and saying so in its doc. With `--full`, every entry carries {source, bundle, tags, available, missing_deps, editor, input_slots, input_multi, output_slots, params} and the WHOLE doc.

goofi library get read
argumenttype
typestringrequired, positional
sourcebooloptional

ONE library entry in full: the palette fields — slots, params, availability — plus where the type came from. `--source` reads the file itself too, under `text`. Copy a node into the patch workspace to modify one.

returns

the `library list --full` entry plus {language, tier, provenance, path, shadowed}, and `text` under `--source` — `shadowed` being the same type's files in the roots BEHIND the winner, each {provenance, path}

goofi library save effect
argumenttype
typestringrequired, positional
overwritebooloptional
namestringoptional

Save a patch or custom node in the private library. An existing file requires --overwrite. Use --name to save under another file name with the same extension; this keeps the source for existing instances. Without a new name, a patch file moves into the library and future patch saves bundle it from there.

returns
{
  type,
  path
}

the type as stored, and the library file it now lives in

goofi library refresh effect

Re-read the shipped and patch node directories; live instances of a changed type restart onto the new code. Call after writing a node file.

returns
{
  added: [
    type
  ],
  changed: [
    type
  ],
  removed: [
    type
  ]
}
goofi log 2 ops 1 read1 effect
goofi log list read

Read the retained log groups, ordered by their last occurrence.

returns
{
  cursor,
  oldest,
  reset,
  groups
}
goofi log write effect
argumenttype
textstringrequired, positional
levelstringoptional
componentstringoptional

Write an application message. Level is info, warning or error.

returns
{
  logged: true
}
goofi node 10 ops 2 read5 write3 effect
goofi node state read
argumenttype
nodeuidrequired, positional
slotstringoptional
paramsbooloptional
errorbooloptional

Read one node: its params (values, ranges, expression bindings), each output slot's name and kind and whether the node is emitting on it, and its error. `slot` narrows to one output; `--no-params` and `--no-error` drop a section. The FRAMES are not here: `node snapshot` reads one raw, and `/data/<node>/<slot>` streams them exactly as a viewer sees them.

returns
{
  text: string
}
goofi node snapshot read
argumenttype
outputendpointrequired, positional
rawbooloptional

The output's latest frame, once — the analysis read, addressed `node/slot`. A facade or a boundary port resolves to the stream behind it, exactly as a viewer's does. It reads the cache the slot's reducer already keeps, so it never wakes the node and never touches the viewers' shared stream. An ARRAY answers its shape and its range, which is what tells silence from signal; `--raw` answers the numbers themselves as base64 NPY. STRING and TABLE answer plain JSON, a table's ARRAY members reading the same way. A slot asked about before anything was cached answers `{frame: null}` with the reason — asking is also what opens the slot's feed, so ask again after the node's next emit.

returns
{
  meta,
  shape,
  range: {
    min,
    max,
    mean
  }
}

for ARRAY, or {meta, npy_b64} under `--raw`; {meta, value} for STRING/TABLE; {frame: null, reason} before the first cached frame

goofi node add write
argumenttype
typestringrequired, positional
posfloat2optional
namestringoptional
inst_iduidoptional
member_uiduidoptional
paramjson, a listoptional

Create a node of `type`. `inst_id` births it inside that sub-patch; absent = root. `name` is a letter then letters or digits and not a Python keyword — one that is taken or illegal is refused, never silently swapped — and an omitted one is minted. Each `--param` is one birth param, self-addressed: `{"name": "group/param", …}` carrying `node param edit`'s fields — inside a JSON flag under bash, spell nested strings with ESCAPED double quotes (`"nd(\"other\").out.sfreq"`); a single-quoted `nd('x')` inside a single-quoted shell token loses its quotes silently. `member_uid` asks for a CHOSEN uid, so a caller rebuilding a graph it already knows — or wiring a batch it is still building — keeps its uid-keyed bindings; naming one the patch already holds answers with that node rather than a second one. The boundary types (InArray/InString/InTable/InAudio/InTexture/OutArray/OutString/OutTable/OutAudio/OutTexture) create a PORT of the sub-patch named by `inst_id`, which is required for them. A port is a node in every way an op can see — it is named, moved, wired and removed by the same ops — but it never runs, so it takes no params. To COPY a node rather than build one, read it with `nodes copy` and put it back with `nodes paste`.

returns
{
  name,
  uid,
  input_slots,
  output_slots,
  params
}

the node as born, so it can be wired and tuned without a follow-up read. `name` is what every op and nd() address it by; the uid is for a caller keying its own records.

goofi node edit write
argumenttype
nodeuidrequired, positional
namestringoptional
posfloat2optional
viewerjson, a listoptional

Edit a node's own record: rename it, move it, set viewers — any of them, in one step and one undo. An omitted field is left alone. Params are `node param edit`'s. A sub-patch boundary port takes every field: its name is in the one namespace nd() reads, so a collision is refused exactly as a leaf's is, and its `value` slot takes a viewer exactly as a leaf's output does. A `name` is a letter then letters or digits, and not a Python keyword, for every kind of node. An expression reads a name as an ATTRIBUTE — a sub-patch's slot in `nd('chain').out.drain` — and a reference spells `name.slot`, so one that cannot be read there breaks every source naming it, and the rewrite that follows the NEXT rename can no longer find what it broke. Each `--viewer` is one slot's inline view, `{"slot": "out", "kind": …, "settings": …}`, merged slot by slot so only the slots named move; `{"slot": "out", "clear": true}` removes that slot's stored view. `kind` is one of: line (a time plot: one series (1-D) or one per channel (C, N); or, as a setting, the trajectory over pairs of rows); image (a bitmap: (H, W), or (H, W, C) for 1–4 channels); brain (a scalp map of one scalar per channel (1-D), or the connectivity between channels (C, C) as a ring or in 3-D); string (the text of a STRING slot); table (the rows of a TABLE slot).

returns
{
  ok: true
}
goofi node remove write
argumenttype
nodeuidrequired, positional

Delete whatever `node` names — a leaf, a boundary port or a whole sub-patch. A sub-patch takes everything inside it, to any depth: nested sub-patches, their members and their ports. A port of an enclosing sub-patch that exposed the deleted node STAYS, unwired — a port is a node, and it outlives what was behind it exactly as an unconnected node outlives the cable it lost. Idempotent: a uid naming no node succeeds having deleted nothing, and says so.

returns
{
  removed: bool
}

false when it named nothing

goofi node baseline write
argumenttype
nodeuidrequired, positional

Take the touched filter's zero point to be what this node holds NOW — what the inspector's Clear button does. A param reads as touched when its value, its expression or its reference differs from that zero, and with no zero recorded the zero is the type's own declared default. A plugin's declared default is its FACTORY default, so loading a preset moves hundreds of params at once and the filter that exists to show the few in play fills with everything the preset moved; this is how that is reset to the few that follow. It edits no param and breaks no binding: an expression or a reference keeps driving exactly as it did, and the Expression and Reference filters still find it. Undoable.

returns
{
  ok: true,
  cleared: int
}

how many params the new zero point covers

goofi node restart effect
argumenttype
nodeuidrequired, positional

Respawn a node in place, keeping its uid, name, params, links and scope. Recovery, not an edit — `setup()` runs again.

returns
{
  ok: true
}
goofi node editor effect
argumenttype
nodeuidrequired, positional
showbooloptional

Open a node's own editor window — a plugin's GUI — on the machine goofi runs on, never in the page; `--no-show` closes it. Only a type whose palette row says `editor: true` has one, and a machine with no display has none.

returns
{
  changed: bool
}

false when the editor was already open, or already closed

goofi node param 2 ops 1 write1 effect
goofi node param edit write
argumenttype
nodeuidrequired, positional
paramparam_addrrequired, positional
valuestringoptional
expressionstringoptional
referencestringoptional
modestringoptional
triggersbooloptional

Set ONE param, addressed `group/param`. `value` is coerced to the param's declared type — a fraction into an int rounds, a value of the wrong kind falls back to that type's zero; the declared min/max are the editor's range, NOT a clamp. A param has ONE active source, named by `mode`: `constant` (the value), `expression` (Python over nd(), variables and me, at control rate), or `reference` (one producer output spelled `node.slot`, no Python, at the producer's rate). Giving an `expression` or a `reference` implies its mode, so binding one is a single flag; the other two are RETAINED across a mode switch, an empty text clears that text (and the mode, if it was the active one), and a mode or trigger given alone edits what is already there. A `value` on a driven param switches it to `constant`. A reference's producer slot must match the param: a number or bool references any output that may feed an ARRAY input and that holds one element, or selects one flat element with `node.slot[index]`. A string references a STRING output. `triggers` defaults false, and that is almost always right: a binding re-evaluates on its own — when a referenced node emits, when a referenced param (`nd('x').params.<group>.<param>`, `me.params.…`) is edited, or on each of the node's own runs for a ref-less one — and the node reads the fresh value on its next normal run. `triggers: true` ALSO wakes the node's process() on every evaluation, making the reference its clock. Reach for it only when the node would otherwise not run (a trigger input with no wire into it) and you want the referenced node to drive it. Never on a ref-less expression (`t`, `variables.x`): that free-runs the node at its common.max_frequency.

returns
{
  value,
  error
}

the value as STORED, with its bind error: a compile failure, an unknown producer, or a slot of the wrong kind.

goofi node param request effect
argumenttype
nodeuidrequired, positional
paramparam_addrrequired, positional
requeststringrequired

One request to a node's own thread, addressed `group/param`: `refresh` asks a refreshable string param (a device or stream picker) to re-enumerate its options — read them back with `node state`; `pulse` fires a pulse param once: a reset, a trigger, a clear. A request holds no value, so it is never an edit of the document, and this reply only says it was dispatched.

returns
{
  ok: true
}

the request was dispatched to the node's own thread

goofi nodes 5 ops 2 read3 write
goofi nodes inspect read
argumenttype
scopeuidoptional, positional

Read one scope as a mermaid flowchart — nodes, sub-patches, boundary ports and wires. A node's mermaid id is its NAME, which is what every other op takes. No arg = the root scope. Scope-wide and nothing more: what is broken is the whole patch's business, so `session status` answers that.

returns
{
  text: string
}
goofi nodes copy read
argumenttype
nodesuid, a listrequired, positional

Read `nodes` and everything they hold — a sub-patch's members, their ports and the nested sub-patches below them, to any depth — as a self-contained fragment. A link rides only when BOTH its ends are in the fragment. The shape is the `.gfi`'s own, so a fragment is a patch's worth of nodes in the format a patch is written in, and `nodes paste` is what puts one back.

returns
{
  doc: {
    nodes,
    links
  }
}

the fragment, keyed by the uids it was read from

goofi nodes paste write
argumenttype
docjsonrequired
posfloat2optional
inst_iduidoptional

Add a `nodes copy` fragment on FRESH uids and fresh names, so it lands beside whatever it was copied from rather than colliding with it. `pos` shifts the whole fragment by that offset; `inst_id` puts its roots inside that sub-patch, absent = root. A record naming a scope that is IN the fragment keeps the shape it was copied with. One command, so it is one undo step.

returns
{
  rename: {
    old_uid: new_uid
  }
}

every record's uid in the fragment mapped to the one it was created at

goofi nodes group write
argumenttype
nodesuid, a listrequired, positional
posfloat2optional

Collapse nodes into a new sub-patch, returning it. `nodes` must share one scope, and one of them may itself be a sub-patch. Every wire that ends up CROSSING the new boundary mints a port to carry it, so nothing is disconnected and nothing stops running; a wire buried in a nested member mints a port there too, so it can reach the new boundary.

returns
{
  name,
  inst_id
}

the sub-patch as born; `name` is what every op takes, `inst_id` the uid behind it

goofi nodes ungroup write
argumenttype
subpatchuidrequired, positional

Dissolve a sub-patch, returning its members to the parent scope. Its ports go with it and every wire they carried stands, because a port keeps its wire against the node behind it. A port of an ENCLOSING sub-patch that exposed one of these follows down onto what it exposed.

returns
{
  ok: true
}
goofi op 2 ops 2 read
goofi op list read
argumenttype
docbooloptional

Every op this server speaks: its name, its arguments (`!` marks a required one) and its kind — a `write` is undoable and may ride in a batch, an `effect` runs alone. Every argument is reachable as `--name value`, which is all a caller needs to write one. What an op DOES is `<op> --help`; `--doc` answers the whole vocabulary explained, which is the manual and costs like one.

returns
{
  ops: [
    {
      op,
      args,
      kind
    }
  ]
}

each row also carrying {positional, doc, result} under `--doc`

goofi op complete read
argumenttype
linestringoptional, positional

What can come NEXT on a partial command line — the shell completion read. Each candidate is a word with a one-line doc: a group or op word mid-phrase, a flag once the op is named, or a value for a flag with a known vocabulary (a panel type, a live node's name). The line's last word, when partial, filters the candidates.

returns
{
  text: string
}

one candidate per line, `word<TAB>doc`

goofi plugin 1 op 1 read
goofi plugin list read

List installed plugins and their status.

returns
{
  plugins
}
goofi record 6 ops 1 read3 write2 effect
goofi record status read

Whether a recording runs, where it writes, and every armed stream's health: frames written, frames dropped, and how full its buffer is. The one read a panel, an agent and a test all use.

returns
{
  running: bool,
  folder: string | null,
  elapsed: number | null,
  streams: [
    {
      node,
      slot,
      engine,
      file,
      frames,
      dropped,
      fill
    }
  ],
  error: null
}

`error` is what the `record_changed` event puts a failed finalize in; a status read always answers null

goofi record arm write
argumenttype
outputendpointrequired, positional

Capture this output slot, addressed `node/slot`. Arming rides the node's own record, so it is undone, saved and copied with the node, and a re-wire elsewhere cannot disarm it. Arming while a recording runs opens a new file for that stream at once. `changed` is false when the slot was already armed, which records no command.

returns
{
  ok: true,
  changed: bool
}
goofi record quality write
argumenttype
outputendpointrequired, positional
qualitystringrequired, positional

Set an armed video output's quality: small, high (default), or very_high. Saved with the patch and undoable. During recording, a change starts a new video file.

returns
{
  ok: true,
  changed: bool
}
goofi record disarm write
argumenttype
outputendpointrequired, positional

Stop capturing this output slot. A file open for it is closed and named in the manifest. `changed` is false when the slot was not armed, which records no command.

returns
{
  ok: true,
  changed: bool
}
goofi record start effect
argumenttype
namestringoptional, positional
rootstringoptional
annotationsjsonoptional

Begin a recording. `name` names the folder, which otherwise carries the UTC of this moment; `root` overrides the recordings folder for this one recording. Either one absent is read from `variables.record.name` and `variables.record.root`. Refused when nothing is armed, and refused when one already runs.

returns
{
  folder: string
}
goofi record stop effect

End the recording: every file is closed and the manifest is finalized.

returns
{
  folder: string
}
goofi session 9 ops 4 read5 effect
goofi session status read

The session's identity AND its health: which instance this is, where the patch lives, whether it differs from disk, and every standing error with how long it has stood. One read for `is my patch healthy, and have I saved it`.

returns
{
  instance_id,
  save_path: string | null,
  workspace,
  dirty: bool,
  errors: [
    {
      node,
      path,
      error,
      standing
    }
  ],
  audio,
  graphics
}

`node` is the name to pass back, `path` where it sits; `audio` and `graphics` carry that engine's clock and counters — `graphics.windows` is how many `Window` nodes have a window open — and are null where the engine is not registered

goofi session state read

The whole replicated document, exact and ATOMIC: nodes, links, variables and arrangement in one read — what every client mirrors, read without the sync protocol that carries it. ONE `nodes` map carries leaves, sub-patch facades and boundary ports alike, each naming its scope, and a port's inner wire is in `links` like any other cable. Narrowing is the caller's: pipe it through `jq`.

returns
{
  nodes,
  links,
  variables,
  arrangement
}

nodes and variables keyed by id, links a list.

goofi session manifest read

The open patch as YAML — the manifest a `.gfi` holds, diffable and versionable.

returns
{
  yaml: string
}
goofi session save effect
argumenttype
pathstringoptional, positional
overwritebooloptional

Pack the patch and its workspace to a `.gfi`. With no `path` it saves to the patch's home — refused when the patch has never been saved — and a given `path` becomes the new home. Set `overwrite` to false to refuse an existing file; normal Save replaces the current file.

returns
{
  path: string
}
goofi session load effect
argumenttype
pathstringoptional, positional
contentstringoptional
adoptbooloptional

Replace the open patch, losing unsaved work. `path` names a `.gfi` and brings its workspace with it; `--content` is an inline YAML manifest and carries no workspace. Exactly ONE of the two — the empty patch is `session new`. `adopt` (default true) decides whether a loaded FILE becomes the patch's home, which is what a later silent save overwrites; `/patch.gfi` passes false, because the file a browser upload came from lives on the user's machine and the staged copy this reads is deleted immediately.

returns
{
  ok: true,
  layout_warning: string | null
}
goofi session new effect

Replace the open patch with the empty one, losing unsaved work. The undo history is cleared, so this cannot be taken back.

returns
{
  ok: true,
  layout_warning: string | null
}
goofi session recoverable read

Every autosave a goofi that did not shut down cleanly left behind: unsaved work, kept beside the crashed session's workspace. `workspace` is what `session recover` and `session discard` take; `home` is the `.gfi` the patch was saved as, null for one never saved; `at` is when the autosave was taken, in seconds since the epoch.

returns
{
  recoveries: [
    {
      workspace,
      home: string | null,
      at: float | null
    }
  ]
}
goofi session recover effect
argumenttype
workspacestringoptional, positional

Replace the open patch with a crash's autosave, losing unsaved work. The recovered patch is UNSAVED work with its old home: a plain Save writes it where the lost session would have. The autosave is removed once it is open.

returns
{
  ok: true,
  layout_warning: string | null
}
goofi session discard effect
argumenttype
workspacestringoptional, positional

Remove a crash's autosave without opening it. Refused for a workspace a running goofi owns.

returns
{
  ok: true
}
goofi variable 10 ops 1 read9 write
goofi variable list read

Every patch variable — what an expression can read and the variable writes can set — each with the lock that holds it (its own and its group's together), and every group that carries a lock. The `system` group is goofi's own: config-locked for life, and its EPHEMERAL members — `system.goofi_home` and the `system.audio_*` facts the audio engine publishes — are goofi's own value, never saved into a patch.

returns
{
  variables: [
    {
      name,
      type,
      value,
      lock: {
        config,
        value
      },
      control?,
      source?
    }
  ],
  groups: {
    group: {
      lock
    }
  }
}
goofi variable entry 6 ops 6 write
goofi variable entry add write
argumenttype
namestringoptional, positional
groupstringoptional
typestringoptional
valueanyoptional
controljsonoptional

Create a patch variable. Give `group` alone to add a float entry with value 0 and the first free entry0/entry1/... name. Otherwise `name`, `type` and `value` are required. `name` is `group.element` — every variable is in a group. `type` is one of float/int/bool/string; a name the patch already holds is refused — `variable entry edit` changes one. `control` makes it a control-panel element: {kind, min, max, step, options, x, y, w, h}, where kind is knob/slider/number/text/toggle/dropdown/paint and must be able to draw the type.

returns
{
  name,
  value
}

the name and value as stored

goofi variable entry edit write
argumenttype
namestringrequired, positional
valueanyoptional
typestringoptional
controljsonoptional

Change an existing variable's value, type-coerced to the type it holds. An explicit `type` changes the type, converting the current value when no value is supplied (an unsupported conversion uses an empty value); config-locked entries refuse type changes. A control widget must support the new type. A value-locked variable refuses the edit, and so does an ephemeral one (system.goofi_home, system.audio_*). `control` sets the control-panel widget and its place, `null` clears it, and giving one makes `value` optional — which is what a panel sends when it moves a widget; a config-locked variable refuses it.

returns
{
  value
}

the value as stored, type-coerced

goofi variable entry remove write
argumenttype
namestringrequired, positional

Delete a patch variable. A config-locked one refuses, and a system variable always is.

returns
{
  removed: true
}
goofi variable entry source write
argumenttype
namestringrequired, positional
referencestringrequired, positional
indexintoptional

Make a variable FOLLOW one producer output, `node.slot`, at that producer's rate: the manager writes the variable on every frame that changes it, and nobody else may set it until the source is cleared with an empty reference. `index` picks one number out of a frame wider than one — a MIDI controller's `cc` is 128 of them — and a frame that holds one number needs none. The reference follows a node rename exactly as a param's does. A config-locked variable refuses; a value-locked one holds its value and takes nothing.

returns
{
  source: {
    reference,
    index
  } | null
}
goofi variable entry lock write
argumenttype
namestringrequired, positional
configbooloptional
valuebooloptional

Lock or unlock one variable on its own account: `config` freezes its name, its widget and its place in a group, `value` freezes its value alone. An axis not named keeps what it has, and its group's lock holds it besides. The system group's locks are goofi's own.

returns
{
  lock: {
    config,
    value
  }
}

the variable's own lock as stored

goofi variable entry rename write
argumenttype
namestringrequired, positional
tostringrequired, positional

Rename a variable, and rewrite every expression that reads it. `to` is a full `group.element`, so one op both renames an element and moves it to another group.

returns
{
  name
}

the name as stored

goofi variable group 3 ops 3 write
goofi variable group add write
argumenttype
groupstringoptional, positional

Create an empty variables group. Without a name, use the first free group0/group1/... name.

returns
{
  group
}

the created group name

goofi variable group rename write
argumenttype
fromstringrequired, positional
tostringrequired, positional

Rename a group, moving every member with it and rewriting every expression that reads one. A config-locked group refuses, and the system group always does.

returns
{
  group
}

the group as stored

goofi variable group lock write
argumenttype
groupstringrequired, positional
configbooloptional
valuebooloptional

Lock or unlock a whole group, reaching every member: `config` freezes every name, widget and the membership itself — nothing is added, renamed or removed — and `value` freezes every value. An axis not named keeps what it has. The system group's lock is goofi's own.

returns
{
  lock: {
    config,
    value
  }
}

the group's lock as stored

goofi undo · goofi redo · goofi compound 3 ops 3 effect
goofi undo effect

Undo this actor's last graph command. Each actor — a browser tab, a shell, the MCP — has its own stack.

returns
{
  changed: bool,
  can_undo: bool,
  can_redo: bool
}
goofi redo effect

Redo this actor's last undone graph command.

returns
{
  changed: bool,
  can_undo: bool,
  can_redo: bool
}
goofi compound effect
argumenttype
opsjsonrequired

Run several steps in order as ONE undo step and one settled decision: viewers see no intermediate document, and the unsaved dot moves once. `ops` is a list of `{op, payload}`; a step is a read or an undoable write, and an effect is refused — it runs as its own call. A refused step takes back the ones that already landed, so the call either happens whole or not at all. A read step sees the earlier steps' writes on the GRAPH — but the document settles only when the batch does, so `session state` and `session status` inside a batch answer the document the batch found. Read them after it, not inside it.

returns

the steps' own replies, as a bare JSON list in order