@miadi/tide
v0.1.2
Published
Typed client boundary for the ironsilk `tide` runtime. Daemon Unix-socket transport with CLI fallback.
Readme
@miadi/tide
Typed client boundary between Miadi and the ironsilk tide runtime (historically
hermes-navigator). The single place the web app talks to tide — replacing the per-route
exec("/home/jgi/anaconda3/bin/tide …") calls.
Grounded in foundations/terminal-workspace-orchestration/ (Decision C) and
rispecs/tide-runtime/05-ui-surface.spec.md (daemon-socket transport).
Runtime dependency — install tide
This package is a client; it does not bundle the runtime. The tide CLI is the
console entry point of the ironsilk PyPI package ([project.scripts]
tide = hermes_navigator.cli:main). Install / upgrade it in the Python env the
Next.js server can reach:
pip install -U ironsilk # provides the `tide` CLI (currently 0.9.13)
tide --help # verifySource for the runtime lives at /usr/local/src/ironsilk/runtime/ (editable:
pip install -e .). @miadi/tide resolves the binary via TIDE_BIN →
which tide → legacy /home/jgi/anaconda3/bin/tide; set TIDE_BIN when tide
is outside the server's PATH. The daemon socket is resolved separately via
TIDE_SOCKET (see below).
Verbs
import { getContext, getTerminals, detect, peek, steerProposal, send, sendKeys, buildSendKeysPlan, pingDaemon, describeTransport, TideError } from "@miadi/tide"
const snapshot = await getContext() // R2 live context (daemon socket → CLI fallback)
const terminals = await getTerminals() // operator terminals inventory (CLI, read-only)
const match = await detect({ pane: "56" })// R1 domain detection for a pane/cwd → match | null (CLI)
const scroll = await peek("56", 80) // operator peek %56 (CLI)
const proposal = await steerProposal("56", "continue from checkpoint") // (CLI)
const sent = await send("56", "continue") // guarded --run, controller mode (CLI)
// Direct, simple steer transport — `tmux send-keys` via execFile (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 unsubmitted
const plan = buildSendKeysPlan("56", "ls -la", false) // pure dry-run plan (no execution)
const up = await pingDaemon() // daemon liveness (ping → pong)
const how = describeTransport() // { transport: "socket"|"cli", socketPath, socketAvailable }sendKeys — the operator cockpit's primary steer path
sendKeys(pane, message, { submit }) drives tmux send-keys directly through execFile
with argument arrays (no shell, no escaping, no Python spawn, no reviewed-proposal artifact) —
the simple, robust transport behind POST /api/tide/steer. submit (default true) controls
the trailing Enter (C-m): set it false to type content into the pane and leave it on the
prompt for a human to review and submit. buildSendKeysPlan(...) returns the exact tmux argv
pair without executing — used for the cockpit's inline command preview. The tmux binary is
resolved via TIDE_TMUX_BIN -> tmux on PATH.
getTerminals and detect are the read-only surface behind GET /api/tide/terminals
and GET /api/tide/detect — the typed half of operator-terminal triage and manual
domain assignment (jgwill/Miadi#382). The write-back half (assign a pane to a domain)
is deferred to the runtime + a guarded route.
Transport status
| Layer | Status |
|---|---|
| One-shot CLI (tide … --format json, execFile arg-arrays, no shell) | implemented |
| Daemon + Unix-domain-socket transport (hermes.navigator.daemon.v1) | implemented (src/socket.ts) |
| Schema-first contract (tide-api.schema.json → generated types) | planned |
How transport selection works
getContext() is transport-aware:
auto(default): use the daemon socket if it is up; on absence or daemon error, fall back to the CLI. No Python spawn on the hot path when the daemon is running (~µs socket round-trip vs ~200–500 ms CLI spawn).socket: daemon only — throwsTideDaemonErrorif unavailable.cli: force the CLI.
peek / steerProposal / send are CLI-only: the daemon exposes get_context / ping /
detect, not the operator mutation verbs. sendKeys is independent of both — it talks to tmux directly. They join the socket path when the runtime adds those
actions — verb signatures are the stable surface; only the transport underneath changes.
Wire protocol (mirrors hermes_navigator/daemon.py)
Newline-delimited JSON over a Unix socket. Request
{contract_version:"hermes.navigator.daemon.v1", request_id, action, payload} →
{contract_version, request_id, ok:true, data} (or ok:false, error:{code,message,retryable}}).
Socket path resolution
TIDE_SOCKET (explicit) → ${HNAVIG_HOME || HERMES_HOME || ~/.miadi/navigator}/daemon.sock
(mirrors the runtime's get_config_dir()). Binary resolution for the CLI fallback:
TIDE_BIN → which tide → legacy /home/jgi/anaconda3/bin/tide.
Operator-cockpit surface (next)
describeTransport() + pingDaemon() are the building blocks for a GET /api/tide/health
endpoint so a Hermes/Ava/Miadi session can see which transport is live and whether the daemon is
answering before it reads context or proposes a steer. See the foundations plan.
Tests
cd packages/tide && npm testCompiles src + test to /tmp/miadi-tide-test (CJS) and runs node --test. The suite proves
socket selection and CLI fallback with a fake Unix-socket daemon and a fake tide binary —
no live daemon, no live tide required.
App wiring (current)
Resolved as source via a tsconfig.json path alias
("@miadi/tide": ["./packages/tide/src/index.ts"]) — same pattern as @miadi/a2a-contracts,
no pnpm install, no lockfile change. Switch to a workspace:* dependency + dist/ build when a
real build step is introduced.
