@hitheo/agent-link
v0.3.0
Published
Agent Link Bridge — run Codex or Claude Code as the brain behind your Theo Personas. Pairs with your OpenCharts account, listens for hand-offs, does the work locally, and speaks the answer back through the persona.
Readme
Agent Link Bridge — @hitheo/agent-link
Run Codex or Claude Code as the brain behind your Theo Personas.
A persona is the face and voice your people talk to. When it is asked for something that needs your files, codebases, terminal or systems, it hands the task to your agent. The bridge is the small program on your computer that makes that reliable: it pairs with your OpenCharts account, listens for hand-offs, drives the runner you chose, reports what it is doing, and sends the answer back so the persona can speak it.
Nothing connects into your machine. The bridge polls OpenCharts over the same MCP server every install recipe uses, with a key that lives only on this computer.
The one paste. In the app (Personas → Start agent, or a persona's Your agent step) click Connect this computer: it shows one command with a five-minute, single-use pairing code. Paste it into a terminal and you are done:
npx -y @hitheo/agent-link@latest connect --code <code> --runner codex
# no Node.js? curl -fsSL https://www.opencharts.com/agent-link/install.sh | sh -s -- --code <code> --runner codexconnect collects the key the click parked (named after this computer), checks your plan, installs the background
service with the agent working in ~/OpenCharts (created if missing; --cwd <folder> to choose a project), and stops
— the page that issued the code reads connected on its own and offers Talk to your persona. There is no link to
open and nothing to confirm: the click in your signed-in browser was the authorization.
The longer form, step by step:
npx -y @hitheo/agent-link@latest login # pair this computer (opens the dashboard; click "Link this computer")
npx -y @hitheo/agent-link@latest install --runner codex --cwd ~/path/to/your/project # background service, starts with your computer,
# then opens your persona in the browser
npx -y @hitheo/agent-link@latest doctor # why would a link fail?install ends in the browser: it opens /personas/launch, which runs as you (the signed-in owner), waits for the
fresh service's first heartbeat, picks the persona that hands work to your agent — or creates one if you have none — and
lands on it with the call ready to start. open does the same later, on demand (--persona <id> to pick one). Before
it touches the machine, install checks the paired account's plan: Theo Personas are Pro + Teams, and on a plan
without them it refuses with the upgrade link instead of installing a service that would only meet a lock.
start runs the same loop in the foreground (it lives as long as the terminal; --open opens the page once connected).
The dashboard at https://www.opencharts.com/developers/agent-link shows the exact commands for your setup and reads
connected within a few seconds. Easier still: in the Personas tab, Start agent → Open in Codex hands Codex a
prompt that runs these steps for you.
No Node.js? One line installs the bridge as a single binary (no Node.js needed) and prints the same steps:
curl -fsSL https://www.opencharts.com/agent-link/install.sh | sh
# then: ~/.opencharts/agent-link/bin/opencharts-agent-link login (and install / doctor)Why a bridge (and not a pasted prompt)
The first Agent Link asked you to paste a "keep polling forever" prompt into your agent. Agents do not do that reliably: turn limits, approval prompts and context compaction end the loop, and nothing tells you it stopped. The bridge is a process, so:
- the persona reads connected only while the bridge is actually polling (a 45-second heartbeat);
- every hand-off gets an answer — a runner failure is reported honestly instead of leaving the persona waiting;
- one runner thread is kept per persona, so the persona remembers earlier hand-offs;
- a rejected key stops the loop with the reason, and the dashboard names the same key.
Runners
| --runner | Drives | Needs |
| -------------- | ------------------------------------------------------------- | ---------------------------------------------------------------- |
| codex | codex exec --json (one thread per persona via exec resume) | Codex CLI on PATH, or the Codex / ChatGPT desktop app (macOS) |
| claude-code | claude -p --output-format stream-json (--resume per persona) | Claude Code on PATH, signed in (claude login) |
| custom | Any command: task on stdin, answer on stdout | --command "<your command>" |
Codex and Claude Code use whatever sign-in they already have. The bridge never handles those credentials.
Sandbox
--sandbox read-only | workspace-write | danger-full-access (default workspace-write).
- Codex: passed straight through (
-s). Approvals arenever— a daemon cannot answer a prompt. - Claude Code:
read-only→--permission-mode defaultwith read tools;workspace-write→acceptEditsplus a Bash allow-list (git, npm, npx, node, python, …);danger-full-access→bypassPermissions. Override with--claude-permission-mode/--claude-allowed-tools a,b,c.
Commands and flags
connect --code <code> --runner <id> [--cwd <folder>] [--open] [--url <deployment>] (+ every start flag)
the one paste: collect the key the app's click parked, install the service (folder default ~/OpenCharts)
login [--runner codex] [--no-open] [--key oc_…] [--url <deployment>]
[--print-link] register a pairing code, print the link + code, exit (for an agent driving the setup)
[--wait <code>] collect the key for a code printed by --print-link
start --runner <id> [--cwd <dir>] [--persona <id>] [--name <text>] [--sandbox <mode>]
[--timeout <sec>] [--command <cmd>] [--codex-bin <p>] [--claude-bin <p>]
[--claude-permission-mode <m>] [--claude-allowed-tools <a,b>] [--once] [--quiet] [--open] [--url <deployment>]
install same flags as start — runs the loop as a background service that starts with your computer, then opens
your persona in the browser ([--no-open] to skip that)
open [--persona <id>] [--runner <id>] [--no-open] open your persona in the browser (or print the URL)
uninstall stop and remove the service (the paired key stays; `logout` forgets it)
status is the service installed and running? (+ the last log lines)
restart restart the service
doctor [--url <deployment>] [--command <cmd>] [--codex-bin <p>] [--claude-bin <p>]
logout--persona <id>restricts the bridge to one persona's hand-offs (default: any of yours). Onopenit names the persona to land on; without it the page uses your one delegating persona, the persona you started last, or a chooser.--nameis how the persona credits the agent ("Here's what Codex came back with…").--timeoutcaps one hand-off (default 300 s). Past it the runner is stopped and the persona gets what it had.--onceanswers a single request and exits (smoke checks, CI).--urltargets another deployment (local dev:--url http://localhost:3025). The apexopencharts.comis rewritten towww.— the apex redirects and drops the Authorization header.
The background service (install)
A pasted start lives as long as the terminal; install makes the bridge a service that starts with your computer
and restarts itself:
- macOS —
~/Library/LaunchAgents/com.opencharts.agent-link.plist(launchd,RunAtLoad+KeepAlive). - Linux —
~/.config/systemd/user/opencharts-agent-link.service(systemctl --user,Restart=always). - Windows — not yet; keep a terminal running
start, or add it to Task Scheduler at logon.
The service never runs out of the npx cache (npm prunes it). install copies the program it is running to
~/.opencharts/agent-link/bin/<version>/ — the self-contained cli.js (run by the node that ran install) or, when you
installed the single binary, the binary itself — and hands the service an explicit PATH (~/.local/bin,
~/.npm-global/bin, ~/.bun/bin, Homebrew, then yours), because launchd's own PATH would find neither codex nor
claude. Everything it wrote is recorded in service.json; logs go to bridge.log.
The launch page (/personas/launch)
The terminal can pair a key and start a service; it cannot know which browser you are signed into, which persona you
meant, or whether your plan includes Personas. So install (and open) hand the last mile to a page that runs as you:
https://www.opencharts.com/personas/launch?from=install&owner=<your user id>&runner=codex&host=<this computer>Nothing in that URL is secret. owner is the paired account's id, so the page can tell you when the browser is signed
into a different account than the bridge; runner and host only shape the copy ("Connecting Codex on Ari's Mac…");
persona (from --persona) names the persona to open. The page waits up to 20 s for the fresh service's heartbeat,
checks the plan, picks or creates the persona (a new one is <first name>'s agent, delegation on, published), and
lands on /personas/<id>?talk=1 — one tap on Start joins the call (browsers block audio without a click).
The bridge learns your account id on login (and backfills it on the first start / install for pairings made
before it did), so open needs no network call.
Files
~/.opencharts/agent-link/credentials.json the paired key + your account id (mode 0600) — never printed, never in argv or a shell profile
~/.opencharts/agent-link/state.json runner thread per persona
~/.opencharts/agent-link/service.json what `install` wrote (so status / restart / uninstall need no flags)
~/.opencharts/agent-link/bridge.log the service's output
~/.opencharts/agent-link/bin/ the installed program: <version>/cli.js or the binary; install.sh puts
`opencharts-agent-link` (or its symlink) here tooOverride the folder with OPENCHARTS_AGENT_LINK_HOME. logout deletes the credentials file; revoke the key too under
Settings → Integrations → MCP & API Keys if you no longer want it valid.
Other environment variables (all optional): OPENCHARTS_URL (the deployment; --url wins, then this, then the paired
credentials, then production), CODEX_BIN and CLAUDE_BIN (same as --codex-bin / --claude-bin), and
OPENCHARTS_AGENT_LINK_COMMAND (the custom runner's command when --command is not passed). A custom command receives
OPENCHARTS_PERSONA_ID and OPENCHARTS_PERSONA_NAME in its environment; its stderr lines become progress notes.
Pairing, precisely
logingenerates a UUID code, registers it with your computer's name and runner (POST /api/personas/agent-link/pair/start), and opens/developers/agent-link?pair=<code>.- You sign in and click Link this computer. That click mints an API key named
Agent Link · <your computer>and parks it under the code for five minutes. loginpollsGET /api/personas/agent-link/pair/poll?code=every two seconds and receives the key exactly once; the server deletes it in the same call.
The code is the only thing that crosses the wire before the key exists. It is 122 bits, single-use and time-boxed.
An agent driving the setup (Codex, through the OpenCharts plugin or the "Open in Codex" prompt) cannot hold a command
open for minutes, so login splits in two: login --print-link --runner codex registers the code and prints the link
and the code, then login --wait <code> waits for your click and stores the key. Nothing in between ever sees it.
Troubleshooting
doctorsays the server refused the key (401). It was revoked, expired, or minted for another deployment. Runloginagain. The dashboard shows the same rejected key and why.installsays your plan does not include Theo Personas. Personas are on Pro and Teams; the message carries the upgrade link.doctor'splanline reads the same signal.installfinished but no browser opened. Runopencharts-agent-link open(oropen --no-opento print the URL and paste it into the browser you are signed into).startsays Codex was not found. Install the CLI (npm i -g @openai/codex) or the Codex desktop app, or pass--codex-bin /path/to/codex.- The persona says "my agent isn't connected". The bridge is not running, or it is paired to a different account.
doctorshows who the key signs in as. - Claude Code answers "Failed to authenticate". Run
claude loginon this computer. - The answer sounds like "I'll check that now." The runner announced instead of answering. The bridge detects this for Codex and asks the same thread for the real answer; for Claude Code the appended system prompt forbids it.
Development
From the OpenCharts repo (no install needed inside apps/agent-link):
npx tsx apps/agent-link/src/cli.ts doctor --url http://localhost:3025
npx tsc --noEmit -p apps/agent-link/tsconfig.json
npx vitest run __tests__/agent-linksrc/shared.ts re-exports src/lib/personaAgentLink.ts from the web app — the pure rules module — and tsup inlines
it at build time (along with @modelcontextprotocol/sdk, so the package has zero runtime dependencies and install
can copy one file), so the bridge and the server can never disagree on the spoken-answer rules or the runner ids. The
version is stamped into the bundle (__BRIDGE_VERSION__) because the compiled binary has no package.json beside it.
Release
npm run build # dist/cli.js — what `npm publish` ships (prepublishOnly runs it)
npm run build:binaries # dist-bin/opencharts-agent-link-<os>-<arch> for every target + SHA256SUMS (bun build --compile)
npm run build:binaries -- host # just this computer's target, to smoke-test: dist-bin/opencharts-agent-link-darwin-arm64 doctorUpload every dist-bin/ file, under exactly those names, to a release of hitheoai/agent-link-releases. The web's
/download/agent-link/<os>-<arch> paths (and therefore install.sh) always point at the newest release's assets, so a
new bridge version needs no web deploy. bun is used from PATH when present, else through npx -y bun@latest.
