@agentpulselabs/cli-plugin-pulse
v1.0.0
Published
AgentPulse CLI plugin for installing and operating the Pulse local connector (replaces the deprecated build-discipline Pulse plugin).
Maintainers
Readme
Pulse connector plugin
@agentpulselabs/cli-plugin-pulse contributes the agentpulse pulse command group.
Commands:
agentpulse pulse install
agentpulse pulse pair [--org <slug>] [--platform <url>] --root <rootId>=<path>
agentpulse pulse login [--platform <url>]
agentpulse pulse start
agentpulse pulse status
agentpulse pulse doctor
agentpulse pulse stop
agentpulse pulse restart
agentpulse pulse mirror on|off|status
agentpulse pulse hub-write on|off|statusReplaces the deprecated build-discipline Pulse plugin
Version 1.0.0 takes over the package name and the pulse topic from the deprecated build-discipline plugin (@agentpulselabs/cli-plugin-pulse 0.2.0, with pulse gate|next|start|status|submit|verify). Those commands are gone. The connector was previously developed as @agentpulselabs/cli-plugin-hub.
Remove the old plugin first for a clean replace, then install or link the connector. Always use the full scoped name; a bare pulse resolves to an unrelated npm package.
agentpulse plugins uninstall @agentpulselabs/cli-plugin-pulse
agentpulse plugins install @agentpulselabs/cli-plugin-pulse # once published
agentpulse plugins link <path-to-this-repo> # local development instead of installagentpulse plugins update does not move you to 1.0.0, because the old install is pinned to 0.2.0; run plugins install explicitly. After plugins unlink, no pulse plugin is left, so install it again.
A machine paired before the rename keeps its pairing, sign-in and state: the Keychain items, the ~/.config/agentpulse/hub directory, the service label and the wire identifiers did not change. The installed launchd or systemd service still holds the absolute paths of the old install, though. If the daemon path changed, or the node binary it names was removed (for example by a node upgrade), agentpulse pulse status, agentpulse pulse doctor and agentpulse pulse start report HUB_SERVICE_PATH_STALE. Run agentpulse pulse install once, then agentpulse pulse start.
How it works
The plugin manages a separate launchd (macOS) or systemd-user (Linux) companion. The oclif command process is never the daemon. Local configuration and state are owner-only. The OAuth tokens and the local Ed25519 key live only in Keychain or libsecret; if neither is available the connector fails closed.
Authentication is the platform's OAuth (Phase 1). agentpulse pulse pair and agentpulse pulse login run the same browser PKCE flow as agentpulse login (public client agentpulse-cli, loopback 127.0.0.1), but keep their own token store and never write the CLI's config.json. The CLI config is only read for the default platform URL and dev-org slug.
Pairing resolves the dev-org through /api/team/dev-orgs, mints a dev-org token through /api/team/dev-orgs/:id/cli-token, and registers the machine with POST /api/agent-hub/user/machines and a frozen least-authority grant. Every connector request carries Authorization: Bearer, X-Serverless-Authorization on Cloud Run dev-orgs, and the machine, session and policy-version headers.
If the platform can't be reached to refresh the sign-in, the daemon reports reconnecting, claims nothing and retries with backoff. When a refresh is refused (invalid_grant), it stops claiming work and agentpulse pulse status prints the commands to run. Today that refusal is expected about every 12 hours, until the platform fixes refresh for the agentpulse-cli client. A revoked machine stops for good; signing in again never revives it. The Ed25519 identity is kept locally for Phase 2 (DPoP) and is not used for authentication.
The daemon restarts itself by exiting with code 75; launchd (KeepAlive.SuccessfulExit=false) and systemd (Restart=on-failure) start it again. It does so when any file under its own src/ (data files such as mirror-contract.json included, dotfiles not) or its package.json changes (2 s after the last write, and only if the contents differ), when restart.request appears in its config directory, or when the cycle or a mirror pass makes no progress for 3 minutes (cycle_stalled in daemon-error.log). A code or request restart drains first: it claims no new job, lets running jobs finish and settle (health shows restarting with the job ids), and only aborts them, as HUB_DAEMON_RESTARTING, after 15 minutes. A stall aborts at once. A request older than the running process is dropped, since the restart it asked for already happened. agentpulse pulse restart writes that file and waits up to 60 s for a new process, so it needs no launchctl or systemctl; it refuses when the daemon has no fresh heartbeat. After more than 5 restarts in 5 minutes the daemon stays up instead, agentpulse pulse status reports restart_loop, and a pending restart is deferred until the window clears. pulse stop and SIGTERM always win and exit 0.
The connector accepts only registered intents:
workspace.readandworkspace.writegit.readandgit.writeworktree.createagent.cursor.invokeandagent.claude.invokehub.write, only afteragentpulse pulse hub-write on
It does not accept shell commands, absolute cloud paths, package installation, secret reads, permission changes, push, release, production actions, or remote grant expansion.
Acting on the local Agent Hub from Pulse
agentpulse pulse hub-write on lets Pulse post to the local Agent Hub as you: a chat message, a NUDGE or STATUS? to agents, a decision or change request on a stream, accepting a delivery, or a reply to a bus ask. It is off on a new pairing. on and off replace this machine's cloud grant with hub.write added or removed (nothing else changes) and store the new policy version in the daemon config; the daemon adopts it on its next heartbeat. status reads the grant from Pulse.
Each action arrives as a typed hub.* job and becomes exactly one POST to the hub on 127.0.0.1 with the hub token, as the principal user, to /api/chat/post, /api/message, /api/decision, /api/accept or /bus/ack, and nothing else. If the hub may have taken a write whose result was never recorded (the daemon stopped mid-request, or the hub did not answer), the action ends as hub_write_unconfirmed instead of being sent twice; check the hub before sending it again. A hub that is not running gives hub_unreachable; a write the hub rejects gives hub_refused with its reason.
Mirroring the local Agent Hub
agentpulse pulse mirror on lets the paired daemon mirror your local Agent Hub into Pulse, so its deliveries, streams, agents, graph, chat (task, project, one-to-one and bus threads), inbox and Cursor rule packs show there read-only. It is off on a new install. on and off change only mirror.localHub in the daemon config, and the daemon picks the change up within a heartbeat. off stops pushing; items already mirrored stay in Pulse. status shows the last sync, how many items are mirrored, and how many were not mirrored, by kind and reason.
The mirror needs the agent-hub:connector:mirror scope. A sign-in from before this version lacks it; run agentpulse pulse login once.
How it reads and what it sends:
- It finds the hub from the
com.agentpulse.agents-hublaunch agent (ormirror.hub.dirandmirror.hub.portin the daemon config) and only sends GETs to an allow-list of endpoints on127.0.0.1, with the hub'sstate/hub.token. It never writes to the hub, never runshub.mjs, and never opensstate/files other than the token. - It follows the hub's
/api/streamfeed, re-reads only the affected endpoints about 300 ms after a change, runs a full reconcile 60 seconds after the last one ended, and reconnects with backoff. - The four graph windows (which also carry edges and bus messages) are the hub's slowest read. After a change they are re-read in a pass of their own, at most once every 3 seconds counted from the end of the last one. The hub announces each of the connector's own graph reads as a change, so a hub change that arrives during one of those reads or within 1.5 seconds after it is most likely an echo, so it re-reads the windows only after 10 seconds; any other change brings the 3 second gap back. Task briefs follow in the next pass, four at a time and for at most 3 seconds.
agentpulse pulse mirror statusshows how long the last pass took. - Only new or changed items are sent, in batches of at most 500 items and 2 MiB. Items that disappear are deleted in Pulse on the full reconcile, and only for sources that were read completely.
- Absolute paths are rewritten before upload: paths in a known repo become repo-relative, workspace and home paths lose their prefix, and any other path keeps only its last segment. A home folder never shows its user name. An item that still carries a path, a token-looking string or the hub token is withheld and counted, never sent.
- Rule packs include only
*.mdcunder.cursor/rules/, plusAGENTS.mdandCLAUDE.md, from the workspace, each repo and your user folder. Files named like.env*,*secret*or*token*, symlinks that leave the pack, and oversized files are skipped. - Transcripts, diffs, raw provider output, the hub token, Keychain and OAuth material are never read for the mirror or sent.
Local checks
npm test
npm run check