@miadi/tide
v0.6.0
Published
Typed client boundary for the ironsilk `tide` runtime. Daemon Unix-socket transport with CLI fallback.
Readme
@miadi/tide
Read and steer the agents working in your terminals, from TypeScript.
In the Miadi stack, @miadi/tide is how a program sees and directs agents at work. It lists
the terminals on a machine, reads what the agent in a pane is doing, and sends that agent a
message when the caller is allowed to. It works with tmux and with
herdr. Every send passes a steer gate first, so a
program cannot type into a pane unless a controller has been named for it. Miadi's operator
cockpit is built on this package.
Works with:
@miadi/tide-contract: the schema this client and the tide runtime share.@miadi/annotate-core: marks an agent's reply before a review is sent back to its pane.- the
tideCLI from theironsilkPython package: the runtime this client talks to.
Install
npm install @miadi/tide
pip install -U ironsilk # provides the `tide` CLI (0.9.27+)
tide --help # verifyThe package ships TypeScript source (src/index.ts), so compile it with your bundler. In
Next.js, add @miadi/tide to transpilePackages.
Verbs
import { getContext, getTerminals, detect, peek, steerProposal, send, sendKeys, buildSendKeysPlan, pingDaemon, describeTransport, TideError } from "@miadi/tide"
const snapshot = await getContext() // live context of every terminal (daemon socket → CLI fallback)
const terminals = await getTerminals() // operator terminals inventory (read-only)
const match = await detect({ pane: "56" })// domain detection for a pane or cwd → match | null
const scroll = await peek("56", 80) // last 80 lines of pane %56
const proposal = await steerProposal("56", "continue from checkpoint") // reviewed proposal, nothing sent
const sent = await send("56", "continue") // guarded send through the tide runtime
// Direct steer through `tmux send-keys` (no shell, no Python):
const keyed = await sendKeys("56", "continue") // types text + Enter
const noEnter = await sendKeys("56", "ls -la", { submit: false })// types text, leaves it for a person to submit
const plan = buildSendKeysPlan("56", "ls -la", false) // the tmux argv that would run, nothing executed
const up = await pingDaemon() // daemon liveness (ping → pong)
const how = describeTransport() // { transport: "socket"|"cli", socketPath, socketAvailable }sendKeys is the primary steer path. It runs tmux send-keys through execFile with
argument arrays, so nothing is interpreted by a shell. submit (default true) controls the
trailing Enter. buildSendKeysPlan returns the exact command without running it, for a
preview.
pingDaemon() and describeTransport() tell a caller which transport is live and whether the
daemon answers, before it reads context or proposes a steer.
The steer gate
Every steer that executes (sendKeys on tmux or herdr, sendKeysViaHerdr, runInHerdrPane)
passes three checks, the same ones the Python runtime applies to tide operator send --run:
- Live target. The pane is in the current multiplexer inventory (
tmux list-panes -aorherdr pane list). - Controller posture.
${MIADI_HOME:-~/.miadi/navigator}/config.ymlsaysmode: controller, and its lease has not expired. An expired lease reads as observer. - Interactive terminal. stdin is a TTY, or
TIDE_ALLOW_NON_INTERACTIVE_RUN=1is set by a service that has reviewed the request itself.
A refused steer throws TideError naming every failed check and never touches the
multiplexer. Plan builders (buildSendKeysPlan, buildHerdrSendKeysPlan) are not gated.
evaluateSteerGate, requireSteerGate and readNavigatorPosture expose the gate for health
checks. A server route that has already authenticated a person can pass
humanConsentSteerGate("<route and gate name>") as the gate option: posture and terminal then
count as satisfied, and the live-target check still applies. No other gate option is accepted
from outside the package, so options built from JSON or a request cannot bypass the gate.
herdr
sendKeys(pane, message, { multiplexer: "herdr" }) types with herdr pane send-text and
submits with herdr pane send-keys … Enter. peek(pane, lines, { multiplexer: "herdr" })
reads herdr scrollback.
herdr pane ids (w1:p2, alphanumeric past p9 as in w2:pE; legacy 1-2 accepted) reset
when herdr restarts, and a stale id fails with pane_not_found. Name a pane with
renameHerdrPane and pass paneLabel: the client resolves the label to the current id
before every action. Labels are compared after NFC normalization, a duplicate label is refused
rather than guessed, and { workspace: "w7" } limits the search to one workspace. Sending a
herdr id down the tmux path, or the reverse, fails.
Other herdr verbs:
| verb | returns |
|---|---|
| runInHerdrPane, readHerdrPane, renameHerdrPane | run a command, read recent text, set a label |
| waitForHerdrOutput, waitForHerdrAgentStatus | wait for output or an agent status change |
| listHerdrPanes, listHerdrWorkspaces, listHerdrTabs | inventory lists |
| getHerdrInventory({ includeTabs? }) | workspaces and panes in one call |
| getHerdrSnapshot() | the whole inventory in one socket round trip: workspaces, tabs, panes, agents, focus, server version |
| describeHerdrCapabilities() | what the running herdr advertises, derived from its schema. Never throws: a failed probe returns { available: false, reason } |
describeHerdrCapabilities() reports lifecycle_authorized: false even when herdr advertises
workspace.close: this client does not close workspaces. Tested against herdr 0.7.4.
Transports
| transport | used by |
|---|---|
| daemon Unix socket (miadi.tide.daemon.v2) | getContext when the daemon is up |
| tide CLI (tide … --format json, execFile, no shell) | getContext fallback, getTerminals, detect, peek, steerProposal, send |
| tmux directly | sendKeys |
| herdr CLI | the herdr verbs and multiplexer: "herdr" |
getContext({ transport }) accepts auto (default: socket, falling back to the CLI on
absence or error), socket (throws TideDaemonError when unavailable) or cli. The socket
answers in microseconds where a CLI spawn costs 200–500 ms. Verb signatures stay the same when
a verb moves to another transport.
The daemon speaks newline-delimited JSON. Request
{contract_version:"miadi.tide.daemon.v2", request_id, action, payload}, reply
{contract_version, request_id, ok:true, data} or {…, ok:false, error:{code,message,retryable}}.
Annotating a reply
The web half of the plannotator review loop that tan <pane> runs in a terminal:
| verb | does |
|---|---|
| fetchReply(pane, { pick, host }) | copy the agent's last reply from a pane into a review document |
| listBlocks(doc) | the document's blocks |
| mark(doc, { block \| quote, text, kind, author, author_name }) | add a signed mark |
| exportReview(doc) | the review as text |
| deliver(pane, doc, { submit }) | send the review back to the pane it came from |
The package holds no review logic of its own. It drives the review script and a headless
plannotator-tui, so a review begun in the browser can be finished in the terminal and the
other way round. A document remembers the pane it was fetched from, and deliver refuses any
other pane. A block mark is always a comment. looks_good and delete need a quote.
Configuration
| variable | default | used for |
|---|---|---|
| TIDE_BIN | which tide, then /home/jgi/anaconda3/bin/tide | the tide CLI |
| TIDE_SOCKET | ${MIADI_HOME:-~/.miadi/navigator}/daemon.sock | the daemon socket |
| MIADI_HOME | ~/.miadi/navigator | socket and config.yml (controller posture) |
| TIDE_TMUX_BIN | tmux on PATH | sendKeys |
| TIDE_HERDR_BIN | herdr on PATH | the herdr verbs |
| TIDE_ALLOW_NON_INTERACTIVE_RUN | unset | 1 lets a reviewed service pass the terminal check |
| PLANNOTATOR_TMUX_REVIEW_SCRIPT | /opt/gaia/linux_migration/11-plannotator-tmux-review.sh | the review script |
| PLANNOTATOR_TUI_BIN | which plannotator-tui, then ~/.cargo/bin/plannotator-tui | the headless annotator |
| PLANNOTATOR_TMUX_REVIEW_DIR | ${XDG_STATE_HOME:-~/.local/state}/plannotator-tmux-review | review documents (web runs under web/) |
| PLANNOTATOR_STORE_DIR | ~/.plannotator/clients/plannotator-tui/annotations | plannotator's mark records |
