Skip to content

AllenThomasDev/poly

Repository files navigation

poly

A tmux pane orchestration tool with an HTTP API and extension system. Inspired by pi-mono.

Prerequisites

Node.js ≥ 20 and tmux.

Install

npm install

To put poly on your PATH:

npm link

Compatibility Policy

Poly is currently a greenfield v0.x project. We optimize for correctness and simplicity over backward compatibility. Breaking changes to actions, payloads, and extension APIs are allowed between minor releases.

Quick Start

poly server start       # Start the HTTP server (default: 127.0.0.1:4096)
poly session start      # Create the tmux session
poly action pane:new --argv vim   # Create a pane running vim
poly action pane:list             # List all tracked panes
poly session attach               # Attach to see your panes

CLI

Server

poly server start [--port 4096] [--hostname 127.0.0.1]
poly server stop
poly server status

poly is designed for localhost use. Binding to non-loopback hosts (for example --hostname 0.0.0.0) exposes an unauthenticated control API and is only safe in tightly controlled environments.

Session

Poly manages panes headlessly — you never see them unless you attach. Use poly session attach to visually inspect panes or debug.

poly session start    # Create the tmux session (required before pane:new)
poly session stop     # Kill session and all panes
poly session attach   # Attach to the tmux session

Actions

# Inline params
poly action pane:new --argv vim --argv .
poly action pane:close --paneId <id>

# Set a metadata key
poly action pane:set-meta --paneId <id> --key role --value agent

# Delete metadata keys (repeated flags for arrays)
poly action pane:delete-meta --paneId <id> --keys role --keys model

# JSON body (alternative — cannot be combined with inline params)
poly action pane:set-meta --body '{"paneId": "<id>", "key": "role", "value": "agent"}'

Events

poly emit pane:ready --paneId <id>
poly emit my-event --body '{"key": "value"}'

Discovery

poly list   # List all registered actions

Core Actions

Action Parameters Returns Description
pane:new id?, argv?, cwd?, env?, meta?, persist? { id, meta } Create a tracked pane
pane:close paneId {} Close a pane
pane:list (none) [{ id, meta }] List all tracked panes
pane:get-meta paneId { id, meta } Get pane metadata
pane:set-meta paneId, key, value { id, previousMeta, meta, changed } Set a single metadata key
pane:delete-meta paneId, keys { id, previousMeta, meta, changed } Delete one or more metadata keys
pane:update-meta paneId, set?, deleteKeys? { id, previousMeta, meta, changed } Atomically set and/or delete multiple keys
pane:activate paneId {} Focus/switch to a pane
shell:run command, cwd? {} Run a shell command in the background (output discarded; use pane:new to capture output)

Core Extensions

The agent:* actions ship with poly and are always available — no installation needed.

agent

Create and manage agent panes.

Action Description
agent:create Spawn an agent pane with initialized state metadata (agent.runtime, agent.status, etc.)
agent:message Send a message to an agent pane
agent:set-state Update normalized agent state metadata

agent:create accepts argv (required), plus:

Parameter Description
id? Custom pane ID
cwd? Working directory
env? Extra environment variables
meta? Additional metadata to merge
model? Model alias. For claude: haiku, sonnet, or opus. Injected as --model <value>.
permissions? "ask" (default) or "auto""auto" bypasses permission prompts
prompt? Initial message passed as a positional argument on startup

It emits agent:created on success.

HTTP API

POST /action/:name     Execute an action (JSON body)
POST /emit/:event      Emit an event (JSON body)
GET  /actions          List all registered actions
GET  /events           SSE stream (?include=pattern,other:*)
GET  /health           Server status

Caller identity is sent via the x-poly-pane header.

SSE Filtering

The ?include= query parameter controls which events are delivered:

GET /events?include=action:*,my-ext:*   # Action events + my-ext events
GET /events?include=*                   # All events
GET /events                             # No events (warning logged)

Supports exact match (action:success), prefix wildcard (action:*), or catch-all (*).

SSE Reconnection

SSE frames include monotonic integer IDs. Reconnect with the Last-Event-ID header to replay missed events from a ring buffer (last 1000 events).

Extensions

Extensions are package directories in ~/.poly/extensions/, loaded dynamically via jiti. Core and agent actions are built into the binary and always available.

Managing extensions

# Install from a git URL
poly ext install https://github.com/user/poly-ext-example

# List all extensions
poly ext list

# Remove a user extension
poly ext remove example

Writing an extension

Each extension is a directory in ~/.poly/extensions/ with a package.json (must have a main field) that exports a name and a default factory function:

~/.poly/extensions/
└── my-extension/
    ├── package.json    # Required; must include "main"
    ├── node_modules/
    └── src/
        └── index.ts
// src/index.ts
import { z } from "zod"
import type { ExtensionAPI } from "poly/types"

export const name = "my-ext"

export default function(poly: ExtensionAPI) {
  poly.registerAction({
    name: "greet",  // Registered as "my-ext:greet"
    parameters: z.object({ message: z.string() }),
    execute: async (params, ctx) => {
      return { echo: params.message }
    },
  })

  poly.on("action:success", async (event, ctx) => {
    // React to any successful action
  })

  // Emit custom events (auto-prefixed: "my-ext:something")
  // await poly.emit("something", { data: 123 })

  // Call other actions
  // const panes = await poly.executeAction("pane:list")
}

Extension names must match [a-z0-9][a-z0-9-]* and cannot use reserved names (action, server, pane, shell, extensions, session).

Discovery only loads directories with package.json + string main. Loose files are ignored. Poly does not auto-install extension dependencies; run npm install inside the extension directory before loading.

Extension API

Method Description
poly.cwd The working directory the server was started in
poly.registerAction(def) Register an action (auto-prefixed with extension name)
poly.on(event, handler) Subscribe to lifecycle or custom events
poly.emit(event, payload) Emit a custom event (auto-prefixed)
poly.executeAction(name, params) Call any registered action

See DESIGN.md for extension philosophy, patterns, and design rationale.

Agent State Sync

If you run OpenCode inside Poly panes, you can sync runtime state into pane metadata using the included plugin at .opencode/plugins/poly-agent-state.ts.

The plugin calls agent:set-state (from the bundled agent extension) over Poly HTTP for all state updates. If Poly is not on the default http://127.0.0.1:4096, set POLY_BASE_URL in the agent pane environment so the plugin targets the correct server.

The plugin listens for OpenCode lifecycle events and updates these generic keys:

  • agent.runtime (for example "opencode")
  • agent.sessionId
  • agent.status (for example running, idle, error)
  • agent.error
  • agent.lastEventAt

This keeps metadata provider-agnostic so other agent runtimes can reuse the same schema.

All shared runtime hook files now live under hooks/ with setup instructions in hooks/README.md.

Events

Event Payload Notes
action:start { actionName, params } Handler can return { cancel: true } to veto
action:success { actionName, params, result }
action:error { actionName, params, errorInfo }
handler:error { source, event, error } Fired when an event handler throws
pane:created { paneId } Fired for poly-tracked panes only
pane:exited { paneId, exitCode } Fired when pane process exits
pane:activated { paneId } Fired on focus change
session:started {} Fired when the tmux session is created
session:stopped {} Fired when the tmux session is closed
session:attached {} Fired when a client attaches
session:detached {} Fired when a client detaches
server:started {}
server:shutdown {}

Logging

The server emits structured JSON lines to stderr via log(). No action traces or request logs by default. If you want logging, write an extension:

export const name = "logger"

export default function(poly: ExtensionAPI) {
  poly.on("action:start",   async (event) => { console.log("[>]", event.actionName) })
  poly.on("action:success", async (event) => { console.log("[✓]", event.actionName) })
  poly.on("action:error",   async (event) => { console.error("[✗]", event.actionName) })
}

Environment Variables

Variable Default Description
POLY_PORT 4096 Server port (0 for random). Also propagated into the tmux session so CLI commands in panes resolve the correct server when the port differs from 4096.
POLY_HOSTNAME 127.0.0.1 Server bind address
POLY_TMUX_SOCKET poly Tmux socket name
POLY_TMUX_CONFIG src/.inner.tmux.conf Tmux config path
POLY_SESSION_NAME poly Tmux session name
POLY_EXTENSIONS_DIR ~/.poly/extensions Global extensions directory
POLY_EVENT_HANDLER_TIMEOUT_MS 5000 Max time (ms) an event handler can run before timeout
POLY_PANE (per pane) Pane's own ID

Security

v1 trust model

Poly v1 is intentionally a local, single-user development tool. It ships without authentication and assumes the server is bound to loopback (127.0.0.1) unless you explicitly opt out.

Poly is a local dev tool. Be aware:

  • No authentication. Anyone who can reach the server can execute actions.
  • No caller verification. The x-poly-pane header identifies callers but isn't verified.
  • Exclusive session management. Direct tmux commands inside the poly session create untracked panes. Use pane:new instead.

Never expose Poly directly to untrusted networks.

Further Reading

  • DESIGN.md — Extension philosophy, patterns, and design rationale

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors