dilaya-cli
v0.1.3
Published
Dilaya agent-loop CLI — a dumb (AI-free) poller that wakes Claude only when there's work.
Downloads
89
Readme
dilaya
A dumb (AI-free) poller that wakes a Claude agent only when there's work.
Each Dilaya app (a Postgres schema) can have one agent: a prompt that runs on demand instead of an AI session sitting idle 24/7. This CLI is the local half of that loop:
- It asks the app's MCP server one cheap question on a fixed interval — "is there work?" (a DynamoDB-backed read; Aurora stays scaled to zero, no model tokens spent).
- Only when the answer is yes does it launch an interactive
claudeinside a detached tmux session, running from the agent's project dir. - The agent does all available work, optionally has a back-and-forth via the
await-worktool, decides how it should next be woken (set-agent-mode), then marks itself idle. The poller reaps the idle session.
Because the agent runs in tmux (not headless claude -p), you can attach to the live
session to watch it work or message it: dilaya agent attach.
You pay for Claude only while there's actual work. The poller itself is free.
Why a separate CLI
The long-lived poll token must never enter the LLM's context. So the agent (in Claude
Code) only ever handles a single-use, 15-minute setup token; the CLI exchanges that for a
long-lived, opaque poll token that it stores locally (chmod 600) and that only ever
reveals "work or not". The user never has to touch the CLI by hand — Claude Code drives the
whole install via get-agent-setup-instructions.
Install
npm i -g dilaya-cli # or from source: npm i -g <path-to>/dilaya-cli
dilaya --version # package is `dilaya-cli`; the command it installs is `dilaya`Requires Node ≥ 18, plus claude and tmux on PATH (the agent runs inside tmux).
Usage
Normally you don't run these yourself — ask the app's agent to "install the agent loop on this machine" and Claude Code runs them for you. For reference:
# 1. Exchange a setup token (from the MCP tool get-agent-setup-token) for a poll token.
dilaya agent setup --schema myapp \
--url https://<host>/myapp/agent/token \
--setup-token <single-use-token> \
--project-dir ~/.dilaya-agents/myapp \
--interval 30s
# 2. Install + start it as a background service (launchd on macOS, systemd-user on Linux).
dilaya agent install --schema myapp --interval 30s
# Inspect / control
dilaya agent status --schema myapp # service + poller + tmux session state
dilaya agent attach --schema myapp # attach to the live agent (type to talk; Ctrl-b then d to detach)
dilaya agent logs --schema myapp # tail the agent log
dilaya agent stop --schema myapp # stop poller (and kill any running session)
dilaya agent uninstall --schema myapp # stop + remove the service
# What the service actually runs (foreground poll loop):
dilaya agent run --schema myappHow a wake works
poll returns { shouldWake, mode, lifecycle }. When there's work and no session is live,
the poller starts one interactive claude (no -p) inside tmux, from the project dir:
mode: "new"— start a fresh Claude session (claude --permission-mode auto --session-id <uuid> /dilaya-agent). Cheaper; the agent starts clean. Used when the agent last finished its task.mode: "continue"— resume the previous session (--resume <id>), keeping context. Used when the agent was mid-task or expecting a reply.
The agent itself picks the next mode by calling set-agent-mode before it goes idle.
Interactive claude never self-exits, so the poller decides when a run is over from
lifecycle:
running— the agent calledbegin-agent-run; the poller leaves it alone. It never time-kills a working agent (coding work can take hours) — but it does watch for a dead turn: while a turn is in flight, interactiveclauderepaints its spinner every second, so the pane snapshot (tmux capture-pane, read-only) changes on every poll. If the pane stays byte-identical for ~3 min whilerunning(e.g. an API error killed the turn and the session fell back to its prompt), the run would otherwise hang forever: the poller reaps the session and re-wakes with--resume, sobegin-agent-runreturnsrecovered_from_crashand the agent reconciles. Same recovery (rate-limited to once per ~5 min) if the lifecycle saysrunningbut the tmux session is gone entirely.idle— the agent calledset-agent-mode; the poller reaps the tmux session after a short grace (~12s) — unless you're attached (attached sessions are never reaped; an attached session is also never treated as a dead turn).
Only one session per schema exists at a time: the loop checks for a live session before
starting another, so a second agent can never run concurrently. A session that starts but
never reports running (e.g. claude failed to launch) is reaped after a startup timeout
(~90s) and retried.
Files it writes
Per schema, under ~/.config/dilaya/<schema>/:
| File | Purpose |
| --- | --- |
| config.json | schema, poll URL, poll token (chmod 600), project dir, interval, last session id |
| poller.pid | liveness of the poll loop |
| agent.log | poll + agent activity (what logs tails) |
The running agent itself lives in a detached tmux session (dilaya-<schema> on a
dedicated tmux socket dilaya), not a file — dilaya agent attach connects to it.
Service definition: ~/Library/LaunchAgents/com.dilaya.agent.<schema>.plist (macOS) or
~/.config/systemd/user/dilaya-agent-<schema>.service (Linux).
Caveats
- Login-scoped. Starts at login (not pre-login boot); laptop sleep pauses polling.
On Linux run
loginctl enable-lingerto keep the service alive after logout. - Requires tmux — the agent runs inside it (
installpreflights it). The poller talks to tmux on a dedicated socket (-L dilaya), so it never touches your default tmux server. - The spawned
claudeuses your existing MCP auth; if that token expires, ticks fail until you re-login. - The poller runs
claude --permission-mode auto(interactive, inside tmux), so the project dir's.claude/settings.local.jsonmust allowlist every tool the agent uses — auto mode runs allowlisted tools without prompting, and deny rules still win. Override the mode withdilaya agent setup --permission-mode <mode>. - One agent per schema; run
setup/installonce per app you want a loop for.
Build (from source)
npm install
npm run typecheck
npm test # node:test suite (via tsx), no network
npm run build # esbuild → dist/index.js (with shebang)Tests live in test/ and run on Node's built-in test runner through tsx
(no extra runtime deps); they cover the pure logic — interval parsing, settings
merging, and the poll/exchange HTTP client (with fetch stubbed). CI
(.github/workflows/ci.yml) runs typecheck → test → build
on every pull request and push to main, across Node 18/20/22.
Releasing
Publishing to npm is automated. To cut a release:
- Bump
versioninpackage.json, then commit and push tomain. - Create a GitHub Release whose tag matches that version (
v0.1.0or0.1.0).
The Publish to npm workflow (.github/workflows/publish.yml)
then verifies the tag matches package.json, type-checks, and runs npm publish. It needs
an NPM_TOKEN repository secret — an npm automation token (it bypasses 2FA in CI):
gh secret set NPM_TOKEN --repo hereya/dilaya-cli # paste the npm automation tokenThe GitHub repo is private; the npm package (dilaya-cli, unscoped) is published public
so npm i -g dilaya-cli works for everyone. It installs the dilaya command.
