A deliberately narrow interface for reaching real people once an agent or operator has already decided what to say, to whom, and why. It provides calls, iMessage/SMS, Gmail, Discord, and per-channel history—not a campaign engine dressed up as a CLI.
The distinction matters. Outreach takes care of the channel mechanics that are awkward or unsafe to rebuild in every workflow: authenticated Gmail threading, Messages.app delivery checks, a live voice-call bridge, and a small JSON-shaped surface an agent can use predictably. It does not decide who deserves a message, infer consent, maintain a contact database, or quietly chase a reply.
The operating rule: decide and record the work in the workflow layer; use
outreachonly for the explicit communication action or the channel-specific evidence needed to make that decision.
| Outreach does | The calling workflow still owns |
|---|---|
| Checks whether each channel is ready | Contact selection, consent, and the reason to reach out |
| Sends one explicit message, email, Discord post, or voice call | Contact records, campaigns, and durable workflow state |
| Reads the requested SMS thread, Gmail thread/search, Discord history, or live-call state | Cross-channel briefings, interpretation, and follow-up policy |
| Manages the local voice daemon and records call transcripts | Retries, reply watchers, timers, and scheduling (for example, through Sundial) |
There are intentionally no campaign commands, contacts, calendar actions, outreach context, polling loops, callback agents, or local campaign files. An agent can read and write its own workflow state directly; this tool should stay a sharp boundary around communication infrastructure.
flowchart LR
A["Agent or operator<br/>makes the decision"] --> B["outreach CLI<br/>explicit command + JSON result"]
B --> C["Calls<br/>Twilio + Gemini Live"]
B --> D["Messages.app<br/>iMessage / SMS"]
B --> E["Gmail API"]
B --> F["Discord API"]
A -. "state, consent, follow-up" .-> G["Workflow / data repo / scheduler"]
B -. "call transcripts + Gmail OAuth token" .-> G
Calls are the only stateful channel. outreach call init starts a local daemon and webhook tunnel; call place connects Twilio Media Streams to Gemini Live; the remaining call commands inspect, steer, measure, or close that one session. SMS, Gmail, and Discord commands are direct channel operations.
This is communication infrastructure, so its constraints are part of the product:
- No implicit outreach. A send, post, or call happens only through an explicit command with a recipient and content/objective. The CLI has no automated follow-up or reply-watching loop.
- Read narrowly. SMS history is scoped to one phone number; Gmail reads/searches use the requested address, thread, or Gmail query. Address-mode Gmail history returns snippets unless
--verboseis requested. - Check before acting.
outreach healthreports readiness for the data repo, call daemon, Messages, Gmail, and Discord without creating a workspace or sending anything. - Treat identity as shareable voice context. The fields under
identityinoutreach/config.yamlcan be supplied to the voice agent. Put only information you would be comfortable having the assistant say. Its call instruction forbids pretending to be human and requires it to identify as the operator's assistant when asked. - Keep secrets out of source control. Credentials live in the ignored
.envfile. The Gmail OAuth token is stored in<data-repo>/outreach/gmail-token.json, outside this checkout; protect that data repo accordingly. Never put tokens, personal message history, or real recipient details in issues, examples, or commits. - Use delivery state honestly. When
--serviceis omitted, SMS selects iMessage or SMS from recent local history (unknown recipients default to SMS), then probes Messages.app's outcome. A failure or timeout is not an invitation to blindly resend.
- A current Node.js runtime (the dependency graph requires Node.js 20 or newer) and npm.
- macOS for the Messages.app channel. SMS history needs Full Disk Access for the terminal/agent host; sending needs the relevant macOS accessibility permission. Messages.app must be signed in.
- Optional channel credentials only for the channels you enable: Twilio + a Google Generative AI key for calls, Gmail OAuth credentials for email, and a Discord bot token/guild for Discord. Voice calls use a local
ngroktunnel by default, or a manually supplied public webhook URL. - An existing data repository for application configuration and call/Gmail artifacts. Outreach deliberately does not scaffold or manage that repository.
Clone the repository, create local-only configuration, then verify the channels you intend to use:
git clone https://github.com/fyang0507/outreach-cli.git
cd outreach-cli
npm install
cp .env.example .env
cp outreach.config.dev.yaml.example outreach.config.dev.yaml
# Edit .env and outreach.config.dev.yaml. Point data_repo_path at an existing workspace.
npm run build
node dist/cli.js healthoutreach.config.dev.yaml is ignored and is useful for a development checkout. In a shared workspace, the normal configuration lives at <data-repo>/outreach/config.yaml. The CLI resolves its data repo in this order:
OUTREACH_DATA_REPOoutreach.config.dev.yamlbeside this checkout (data_repo_path)- A
.agents/workspace.yamlfound by walking up from the current directory
During development, invoke the compiled CLI as node dist/cli.js …. If you install the executable on your PATH, use outreach … instead. npm run build also makes the packaged outreach and contact-operator skills discoverable from the configured agent workspace when possible; the build still succeeds when no workspace is configured.
Start with an observation, keep the decision in the caller, then make one explicit action:
# 1. Non-destructive: see which channels are ready and which config was resolved.
node dist/cli.js health
# 2. Read only the email context required for the decision.
node dist/cli.js email history --address "recipient@example.com" --limit 5
# 3. After the calling workflow has approved the message, send it.
node dist/cli.js email send \
--to "recipient@example.com" \
--subject "A brief follow-up" \
--body "Hello — following up on our earlier conversation."For a non-blocking update to an operator, Discord is often the quieter choice:
node dist/cli.js discord channels list
node dist/cli.js discord post --channel "updates" --silent \
--body "The requested work is complete; the result is in the run artifact."For voice, start and stop the local call infrastructure deliberately:
node dist/cli.js call init
node dist/cli.js call place --to +15551234567 \
--objective "Introduce yourself as an assistant, ask whether this is a good time, then say goodbye."
node dist/cli.js call status --id <callId>
node dist/cli.js call teardownThe number above is a placeholder. A real call requires the configured Twilio caller-ID setup; use --from-twilio when the Twilio number should be shown, and --call-operator only for an intentional escalation to the configured operator.
Operational command results are JSON, making them safe to compose into an agent workflow without parsing decorative terminal output. Run outreach <command> --help for the complete option list and inline descriptions.
| Area | Commands | Notes |
|---|---|---|
| Readiness | health |
Reports data-repo and channel readiness; does not scaffold or send. |
| SMS / iMessage | sms send --to <number> --body <text> [--service iMessage|SMS]sms history --phone <number> [--limit <n>] |
Defaults to history-informed service selection; messages are sent through Messages.app. |
| Gmail | email send --subject <text> --body <text> (--to <address>|--reply-to-id <messageId>) [--cc <addresses>] [--bcc <addresses>] [--no-reply-all] [--attach <paths...>]email history (--address <email>|--thread-id <threadId>) [--limit <n>] [--verbose]email search --query <gmail-query> [--limit <n>] |
--reply-to-id preserves Gmail threading and defaults to reply-all. |
| Discord | discord post --body <text> [--channel <id|name>] [--silent]discord channels listdiscord channels create --name <name> [--topic <text>] [--category <id|name>]discord history --channel <id|name> [--limit <n>] [--after <messageId>] [--before <messageId>] [--since <iso>] [--count] |
Intended for operational updates, not a reply-watching loop. |
| Voice calls | call init · call place (--to <number>|--call-operator) --objective <text> [--from-twilio] [--persona <text>] [--hangup-when <text>] [--max-duration <seconds>] [--wait-for-user]call listen --id <callId> · call steer --id <callId> --text <note> [--mode nudge|say] · call status --id <callId> · call hangup --id <callId> · call latency (--id <callId>|--latest) · call teardown [--force] |
Calls use the local daemon until teardown. steer is a live-session control, not a background agent. |
When passing long free text through a shell, quote it so the shell does not expand $, backticks, or !. If the entire command is itself wrapped in zsh -lc '…', use double-quoted inner values for text containing apostrophes, or run outreach directly when its PATH is already initialized.
The README is the human overview. The packaged skill tree is the agent-facing operating manual:
skills/outreach/SKILL.md— when the utility is appropriate and the canonical command surface.skills/outreach/call.md,sms.md,email.md, anddiscord.md— channel-specific behavior and caveats.skills/contact-operator/SKILL.md— a separate policy for when a headless agent should interrupt an operator; it prefers the quietest adequate channel.AGENTS.mdandCLAUDE.md— repository boundaries and contributor guidance.
For development and release checks:
npm test # builds, then runs the unit tests
node dist/cli.js --help
node dist/cli.js health
node dist/cli.js call place --help
node dist/cli.js sms send --help
node dist/cli.js email send --help
npm pack --dry-runLive channel tests can contact real people or services. Run them only with an explicit test target and permission, and always finish a voice test with call teardown.
This is a source-visible operational tool, not a published npm package; package.json marks it private to prevent accidental publication.
It is released under the MIT License. You may use, copy, modify, and distribute the software under its terms.