Skip to content

Commit 95895fe

Browse files
committed
gateway: setup wizard + docs for the channel baseline
One wizard case per new channel (email, signal, matrix, mattermost, msteams, googlechat, sms), merging into existing channel blocks as always. README: full channel table with transports and env keys, the webhook mount map, email dedup note, and a Media and voice section.
1 parent f49b32f commit 95895fe

2 files changed

Lines changed: 82 additions & 11 deletions

File tree

‎cmd/gateway.go‎

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ var gatewaySetupCmd = &cobra.Command{
9999
} else {
100100
cmd.Printf("Configured: %s\n", strings.Join(enabled, ", "))
101101
}
102-
choice := strings.ToLower(strings.TrimSpace(prompt(in, cmd, "Channel to add/update [telegram/discord/slack/github/whatsapp] (blank to finish): ")))
102+
choice := strings.ToLower(strings.TrimSpace(prompt(in, cmd, "Channel to add/update [telegram/discord/slack/email/signal/matrix/mattermost/msteams/googlechat/sms/github/whatsapp] (blank to finish): ")))
103103

104104
secrets := map[string]string{}
105105
if settings.Channels == nil {
@@ -136,8 +136,46 @@ var gatewaySetupCmd = &cobra.Command{
136136
secrets[gwconfig.EnvWhatsAppVerify] = secret(cmd, "Webhook verify token: ")
137137
secrets[gwconfig.EnvWhatsAppSecret] = secret(cmd, "App secret (verifies inbound; required to activate): ")
138138
ch.AllowFrom = allowList(in, cmd)
139+
case "email":
140+
cmd.Println("Use a DEDICATED mailbox (an app password for Gmail/Outlook), never your personal inbox.")
141+
secrets[gwconfig.EnvEmailAddress] = strings.TrimSpace(prompt(in, cmd, "Email address: "))
142+
secrets[gwconfig.EnvEmailPassword] = secret(cmd, "App password: ")
143+
secrets[gwconfig.EnvEmailIMAPHost] = strings.TrimSpace(prompt(in, cmd, "IMAP host (e.g. imap.gmail.com): "))
144+
secrets[gwconfig.EnvEmailSMTPHost] = strings.TrimSpace(prompt(in, cmd, "SMTP host (e.g. smtp.gmail.com): "))
145+
ch.AllowFrom = allowList(in, cmd)
146+
case "signal":
147+
cmd.Println("Requires a running signal-cli daemon in HTTP mode (see the docs); use a dedicated number.")
148+
secrets[gwconfig.EnvSignalNumber] = strings.TrimSpace(prompt(in, cmd, "Your Signal number (+E.164): "))
149+
if u := strings.TrimSpace(prompt(in, cmd, "signal-cli daemon URL (blank = http://127.0.0.1:8080): ")); u != "" {
150+
secrets[gwconfig.EnvSignalCLIURL] = u
151+
}
152+
ch.AllowFrom = allowList(in, cmd)
153+
case "matrix":
154+
cmd.Println("Plain rooms only for now (no end-to-end-encrypted rooms).")
155+
secrets[gwconfig.EnvMatrixHomeserver] = strings.TrimSpace(prompt(in, cmd, "Homeserver URL (e.g. https://matrix.org): "))
156+
secrets[gwconfig.EnvMatrixToken] = secret(cmd, "Access token: ")
157+
ch.AllowFrom = allowList(in, cmd)
158+
case "mattermost":
159+
secrets[gwconfig.EnvMattermostURL] = strings.TrimSpace(prompt(in, cmd, "Server URL (e.g. https://mm.example.com): "))
160+
secrets[gwconfig.EnvMattermostToken] = secret(cmd, "Bot access token: ")
161+
ch.AllowFrom = allowList(in, cmd)
162+
case "msteams":
163+
secrets[gwconfig.EnvTeamsAppID] = strings.TrimSpace(prompt(in, cmd, "Azure app (bot) ID: "))
164+
secrets[gwconfig.EnvTeamsAppPassword] = secret(cmd, "Client secret: ")
165+
secrets[gwconfig.EnvTeamsTenantID] = strings.TrimSpace(prompt(in, cmd, "Tenant ID: "))
166+
ch.AllowFrom = allowList(in, cmd)
167+
case "googlechat":
168+
secrets[gwconfig.EnvGoogleChatSAKey] = strings.TrimSpace(prompt(in, cmd, "Path to the service-account JSON key: "))
169+
ch.Audience = strings.TrimSpace(prompt(in, cmd, "Project number (JWT audience): "))
170+
ch.AllowFrom = allowList(in, cmd)
171+
case "sms":
172+
secrets[gwconfig.EnvTwilioAccountSID] = strings.TrimSpace(prompt(in, cmd, "Twilio Account SID: "))
173+
secrets[gwconfig.EnvTwilioAuthToken] = secret(cmd, "Auth token: ")
174+
secrets[gwconfig.EnvTwilioFromNumber] = strings.TrimSpace(prompt(in, cmd, "Your Twilio number (+E.164): "))
175+
ch.WebhookURL = strings.TrimSpace(prompt(in, cmd, "Exact public webhook URL (e.g. https://gw.example.com/webhook/sms): "))
176+
ch.AllowFrom = allowList(in, cmd)
139177
default:
140-
cmd.Println("Unknown channel; pick one of telegram/discord/slack/github/whatsapp.")
178+
cmd.Println("Unknown channel; pick one of telegram/discord/slack/email/signal/matrix/mattermost/msteams/googlechat/sms/github/whatsapp.")
141179
continue
142180
}
143181
settings.Channels[choice] = ch

‎docs/gateway/README.md‎

Lines changed: 42 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@
22

33
The same `memcode` binary that runs the interactive agent can run as a
44
long-lived, self-hosted **gateway**: it listens on the surfaces people already
5-
use (Telegram, Discord, Slack, GitHub, WhatsApp), turns each inbound message
6-
into an agent job, and posts the result back. Coding is one use of this loop,
5+
use (Telegram, Discord, Slack, Email, Signal, Matrix, Mattermost, Microsoft
6+
Teams, Google Chat, SMS, GitHub, WhatsApp), turns each inbound message into an
7+
agent job, and posts the result back. Coding is one use of this loop,
78
not what it's built around — an inbound message is just a task.
89

910
```
@@ -30,13 +31,28 @@ It routes each answer the way memcode splits configuration:
3031

3132
A channel is enabled when its secret is present.
3233

33-
| Channel | Secret(s) in `.env` | Transport |
34-
|----------|------------------------------------------------------------|-------------------|
35-
| Telegram | `TELEGRAM_BOT_TOKEN` | Bot API long-poll |
36-
| Discord | `DISCORD_BOT_TOKEN` | gateway websocket |
37-
| Slack | `SLACK_APP_TOKEN`, `SLACK_BOT_TOKEN` | Socket Mode |
38-
| GitHub | `GITHUB_WEBHOOK_SECRET` | inbound webhook |
39-
| WhatsApp | `WHATSAPP_ACCESS_TOKEN`, `WHATSAPP_VERIFY_TOKEN`, `WHATSAPP_APP_SECRET` | Meta Cloud API |
34+
| Channel | Secret(s) in `.env` | Transport |
35+
|-------------|------------------------------------------------------------|-------------------|
36+
| Telegram | `TELEGRAM_BOT_TOKEN` | Bot API long-poll |
37+
| Discord | `DISCORD_BOT_TOKEN` | gateway websocket |
38+
| Slack | `SLACK_APP_TOKEN`, `SLACK_BOT_TOKEN` | Socket Mode |
39+
| Email | `EMAIL_ADDRESS`, `EMAIL_PASSWORD`, `EMAIL_IMAP_HOST`, `EMAIL_SMTP_HOST` | IMAP poll + SMTP |
40+
| Signal | `SIGNAL_NUMBER` (+ optional `SIGNAL_CLI_URL`) | signal-cli daemon (SSE + JSON-RPC) |
41+
| Matrix | `MATRIX_HOMESERVER`, `MATRIX_ACCESS_TOKEN` | client-server /sync (no E2EE v1) |
42+
| Mattermost | `MATTERMOST_URL`, `MATTERMOST_TOKEN` | websocket + REST v4 |
43+
| MS Teams | `TEAMS_APP_ID`, `TEAMS_APP_PASSWORD`, `TEAMS_TENANT_ID` | Bot Framework webhook |
44+
| Google Chat | `GOOGLE_CHAT_SA_KEY` (path) + `googlechat.audience` | signed webhook + Chat REST |
45+
| SMS | `TWILIO_ACCOUNT_SID`, `TWILIO_AUTH_TOKEN`, `TWILIO_FROM_NUMBER` + `sms.webhook_url` | Twilio webhook + Messages API |
46+
| GitHub | `GITHUB_WEBHOOK_SECRET` | inbound webhook |
47+
| WhatsApp | `WHATSAPP_ACCESS_TOKEN`, `WHATSAPP_VERIFY_TOKEN`, `WHATSAPP_APP_SECRET` | Meta Cloud API |
48+
49+
Webhook-driven surfaces (Teams, Google Chat, SMS, GitHub, WhatsApp) mount on
50+
the shared listener (`webhook.addr`, default `:8787`) at
51+
`/webhook/{teams,googlechat,sms,github,whatsapp}` — expose it over HTTPS.
52+
Email dedup is keyed on `<mailbox>/<UIDVALIDITY>/<UID>` (the provider-side ack
53+
identity); Message-ID serves threading only. Signal requires a signal-cli
54+
daemon in native HTTP mode; Matrix v1 is plain rooms only (E2EE is a known
55+
follow-up).
4056

4157
### gateway.yaml
4258

@@ -136,6 +152,23 @@ projects, agents, channel knobs) on change, so an approval takes effect within
136152
seconds — no restart. Channel connections and schedules are wired at startup and
137153
do not hot-reload.
138154

155+
## Media and voice
156+
157+
Inbound attachments (photos, PDFs, documents) are downloaded into a
158+
content-addressed media spool (`~/.config/memcode/media`, pruned with the
159+
inbox) and ride the task into the engine as native image/document blocks.
160+
Everything downstream of the adapter addresses media by spool ID, never by
161+
path — the spool is the trust boundary.
162+
163+
Voice notes are transcribed gateway-side (OpenAI `gpt-4o-mini-transcribe`
164+
falling back to `whisper-1`, or Gemini — picked by whichever key is present)
165+
and the transcript becomes the task text; audio never reaches the engine.
166+
Without either key a voice-only message gets an honest "not configured" reply.
167+
Optionally, `channels.<name>.voice_replies: in_kind|always` (default `off`)
168+
synthesizes an OGG/Opus voice reply (OpenAI `gpt-4o-mini-tts` — the full text
169+
is always sent alongside, code blocks are never spoken, synthesis failures
170+
degrade to text).
171+
139172
## Import from OpenClaw
140173

141174
Already running OpenClaw? Bring your channels over with one command:

0 commit comments

Comments
 (0)