commune-ai 0.2.1 → 0.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +334 -326
- package/dist/client.d.ts +14 -5
- package/dist/client.js +10 -18
- package/dist/index.d.ts +1 -1
- package/dist/index.js +0 -1
- package/dist/listener.js +1 -0
- package/dist/types.d.ts +9 -0
- package/dist/webhooks.js +0 -27
- package/package.json +23 -2
package/README.md
CHANGED
|
@@ -1,425 +1,433 @@
|
|
|
1
|
-
# commune
|
|
1
|
+
# @commune/sdk
|
|
2
2
|
|
|
3
|
-
**
|
|
3
|
+
Commune is the **communication infrastructure for agents**. It gives your agent a **unified inbox**
|
|
4
|
+
for **email + Slack**, so your agent can talk to humans where they already work. Most teams
|
|
5
|
+
get a working integration in **~15 minutes**.
|
|
4
6
|
|
|
5
|
-
|
|
7
|
+
## Why Commune exists (what it enables)
|
|
8
|
+
Agents are powerful, but users already live in **email and Slack**. Commune bridges that gap so:
|
|
9
|
+
- your agent is reachable where humans already work
|
|
10
|
+
- you don’t have to build deliverability, threading, or Slack plumbing
|
|
11
|
+
- you can ship an agent‑first experience in minutes, not weeks
|
|
6
12
|
|
|
7
|
-
|
|
13
|
+
In practice, Commune lets you:
|
|
14
|
+
- give an agent a real inbox on your domain
|
|
15
|
+
- respond in the correct email or Slack thread every time
|
|
16
|
+
- use **conversation state** to make smarter, context‑aware replies
|
|
8
17
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
18
|
+
## How it works (mental model)
|
|
19
|
+
1) Commune receives inbound email/Slack events.
|
|
20
|
+
2) Commune normalizes them into a **UnifiedMessage**.
|
|
21
|
+
3) Commune sends the UnifiedMessage to your webhook.
|
|
22
|
+
4) Your agent replies using one API call.
|
|
13
23
|
|
|
14
|
-
> By default, the SDK talks to the hosted Commune API. If you self‑host
|
|
15
|
-
> pass `baseUrl` to the client.
|
|
24
|
+
> By default, the SDK talks to the hosted Commune API. If you self‑host,\n> pass `baseUrl` to the client.
|
|
16
25
|
|
|
17
26
|
---
|
|
18
27
|
|
|
19
|
-
##
|
|
20
|
-
|
|
21
|
-
- [Install](#install)
|
|
22
|
-
- [How to Setup (Dashboard Steps)](#how-to-setup-dashboard-steps)
|
|
23
|
-
- [1. Create and Verify Domain](#1-create-and-verify-domain)
|
|
24
|
-
- [2. Create Inbox](#2-create-inbox)
|
|
25
|
-
- [3. Create API Key](#3-create-api-key)
|
|
26
|
-
- [4. Configure Webhook](#4-configure-webhook)
|
|
27
|
-
- [5. Environment Variables](#5-environment-variables)
|
|
28
|
-
- [Receive emails](#receive-emails)
|
|
29
|
-
- [Reply in email threads](#reply-in-email-threads)
|
|
30
|
-
- [Access conversation history](#access-conversation-history)
|
|
31
|
-
- [Manage Inboxes Programmatically](#manage-inboxes-programmatically)
|
|
32
|
-
- [Handle different types of emails](#handle-different-types-of-emails)
|
|
33
|
-
- [Semantic Search (Coming Soon)](#semantic-search-coming-soon)
|
|
34
|
-
- [Complete example](#complete-example)
|
|
35
|
-
|
|
36
|
-
---
|
|
37
|
-
|
|
38
|
-
## Install
|
|
39
|
-
```bash
|
|
40
|
-
npm install commune-ai
|
|
41
|
-
# or
|
|
42
|
-
yarn add commune-ai
|
|
43
|
-
# or
|
|
44
|
-
pnpm add commune-ai
|
|
45
|
-
```
|
|
46
|
-
|
|
47
|
-
---
|
|
48
|
-
|
|
49
|
-
## How to Setup (Dashboard Steps)
|
|
50
|
-
|
|
51
|
-
**Before you can receive emails, you need to set up your domain, inbox, and API key in the Commune dashboard.**
|
|
52
|
-
|
|
53
|
-
### 1. Create and Verify Domain
|
|
54
|
-
1. Go to your [Commune dashboard](https://your-dashboard.com)
|
|
55
|
-
2. Sign up and create an organization
|
|
56
|
-
3. Go to **Domains** → Click **"Add Domain"**
|
|
57
|
-
4. Enter a subdomain (e.g., `agent.yourcompany.com`)
|
|
58
|
-
5. Click **"Create"** - The dashboard will show DNS records to add
|
|
59
|
-
6. Add the DNS records shown in the dashboard to your DNS provider
|
|
60
|
-
7. Click **"Verify"** in the dashboard (wait 5-10 minutes for DNS propagation)
|
|
61
|
-
|
|
62
|
-
### 2. Create Inbox
|
|
63
|
-
1. In the dashboard, go to **Inboxes**
|
|
64
|
-
2. Click **"Create Inbox"**
|
|
65
|
-
3. Enter a local part (e.g., `support`, `help`, `agent`)
|
|
66
|
-
4. This creates an email address like `support@agent.yourcompany.com`
|
|
67
|
-
5. **Copy the Domain ID and Inbox ID** - you'll need these for your webhook
|
|
68
|
-
|
|
69
|
-
### 3. Create API Key
|
|
70
|
-
1. Go to **API Keys** in the dashboard
|
|
71
|
-
2. Click **"Create Key"**
|
|
72
|
-
3. Enter a name (e.g., "production-agent")
|
|
73
|
-
4. **Copy the API key immediately** (it only shows once!)
|
|
74
|
-
5. Store it securely as an environment variable
|
|
75
|
-
|
|
76
|
-
### 4. Configure Webhook
|
|
77
|
-
1. In your domain settings, go to **Webhooks**
|
|
78
|
-
2. Set the webhook endpoint URL to your server's webhook handler
|
|
79
|
-
3. Configure webhook events (usually "email.received")
|
|
80
|
-
4. **Optional:** Set a webhook secret for verification
|
|
81
|
-
|
|
82
|
-
### 5. Environment Variables
|
|
83
|
-
```bash
|
|
84
|
-
# Required
|
|
85
|
-
COMMUNE_API_KEY=cmk_your_api_key_from_dashboard
|
|
86
|
-
|
|
87
|
-
# Optional (for self-hosting)
|
|
88
|
-
COMMUNE_BASE_URL=https://your-self-hosted-api.com
|
|
89
|
-
```
|
|
90
|
-
|
|
91
|
-
---
|
|
92
|
-
|
|
93
|
-
## Receive emails
|
|
94
|
-
|
|
95
|
-
Emails are sent to your webhook endpoint. Here's how to handle them:
|
|
28
|
+
## Quickstart (end‑to‑end in one file)
|
|
29
|
+
This is the simplest full flow: receive webhook → run agent → reply in thread.
|
|
96
30
|
|
|
97
31
|
```ts
|
|
98
|
-
import "dotenv/config";
|
|
99
32
|
import express from "express";
|
|
100
|
-
import { CommuneClient, createWebhookHandler } from "commune
|
|
33
|
+
import { CommuneClient, createWebhookHandler, verifyCommuneWebhook } from "@commune/sdk";
|
|
101
34
|
|
|
102
|
-
|
|
103
|
-
const client = new CommuneClient({
|
|
104
|
-
apiKey: process.env.COMMUNE_API_KEY,
|
|
105
|
-
});
|
|
35
|
+
const client = new CommuneClient({ apiKey: process.env.COMMUNE_API_KEY! });
|
|
106
36
|
|
|
107
|
-
|
|
108
|
-
|
|
37
|
+
const handler = createWebhookHandler({
|
|
38
|
+
verify: ({ rawBody, headers }) => {
|
|
39
|
+
const signature = headers["x-commune-signature"];
|
|
40
|
+
const timestamp = headers["x-commune-timestamp"];
|
|
41
|
+
if (!signature || !timestamp) return false;
|
|
109
42
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
// Find sender's email
|
|
116
|
-
const sender = message.participants.find(p => p.role === "sender")?.identity;
|
|
117
|
-
if (!sender) return;
|
|
118
|
-
|
|
119
|
-
// Reply to the email
|
|
120
|
-
await client.messages.send({
|
|
121
|
-
to: sender,
|
|
122
|
-
text: "Thanks for your email! I'll help with that.",
|
|
123
|
-
conversation_id: message.conversation_id,
|
|
124
|
-
domainId: context.payload.domainId,
|
|
125
|
-
inboxId: context.payload.inboxId,
|
|
43
|
+
return verifyCommuneWebhook({
|
|
44
|
+
rawBody,
|
|
45
|
+
timestamp,
|
|
46
|
+
signature,
|
|
47
|
+
secret: process.env.COMMUNE_WEBHOOK_SECRET!,
|
|
126
48
|
});
|
|
127
49
|
},
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
50
|
+
onEvent: async (message, context) => {
|
|
51
|
+
// Example inbound payload (unified across email + Slack):
|
|
52
|
+
// message = {
|
|
53
|
+
// channel: "email",
|
|
54
|
+
// conversation_id: "thread_id",
|
|
55
|
+
// participants: [{ role: "sender", identity: "user@example.com" }],
|
|
56
|
+
// content: "Can you help with pricing?"
|
|
57
|
+
// }
|
|
58
|
+
|
|
59
|
+
// --- Run your agent here (1–2 line LLM call) ---
|
|
60
|
+
const prompt = `Reply to: ${message.content}`;
|
|
61
|
+
const agentReply = await llm.complete(prompt); // replace with your LLM client
|
|
62
|
+
|
|
63
|
+
// Email reply (same thread)
|
|
64
|
+
if (message.channel === "email") {
|
|
65
|
+
const sender = message.participants.find(p => p.role === "sender")?.identity;
|
|
66
|
+
if (!sender) return;
|
|
67
|
+
|
|
68
|
+
await client.messages.send({
|
|
69
|
+
channel: "email",
|
|
70
|
+
to: sender,
|
|
71
|
+
text: agentReply,
|
|
72
|
+
conversation_id: message.conversation_id,
|
|
73
|
+
domainId: context.payload.domainId,
|
|
74
|
+
inboxId: context.payload.inboxId,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Slack reply (same thread)
|
|
79
|
+
if (message.channel === "slack") {
|
|
80
|
+
const channelId = message.metadata.slack_channel_id;
|
|
81
|
+
if (!channelId) return;
|
|
82
|
+
|
|
83
|
+
await client.messages.send({
|
|
84
|
+
channel: "slack",
|
|
85
|
+
to: channelId,
|
|
86
|
+
text: agentReply,
|
|
87
|
+
conversation_id: message.conversation_id, // thread_ts
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
},
|
|
132
91
|
});
|
|
92
|
+
|
|
93
|
+
const app = express();
|
|
94
|
+
app.post("/commune/webhook", express.raw({ type: "*/*" }), handler);
|
|
95
|
+
app.listen(3000, () => console.log("listening on 3000"));
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## 0) Install
|
|
99
|
+
```bash
|
|
100
|
+
npm install @commune/sdk
|
|
133
101
|
```
|
|
134
102
|
|
|
135
|
-
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## Unified inbox (what your webhook receives)
|
|
106
|
+
Every inbound email or Slack message arrives in this shape:
|
|
107
|
+
|
|
136
108
|
```ts
|
|
137
|
-
interface UnifiedMessage {
|
|
138
|
-
channel: "email";
|
|
109
|
+
export interface UnifiedMessage {
|
|
110
|
+
channel: "email" | "slack";
|
|
139
111
|
message_id: string;
|
|
140
|
-
conversation_id: string; // email thread
|
|
141
|
-
participants:
|
|
142
|
-
role: "sender" | "to" | "cc" | "bcc";
|
|
143
|
-
identity: string; // email address
|
|
144
|
-
}>;
|
|
112
|
+
conversation_id: string; // email thread or Slack thread_ts
|
|
113
|
+
participants: { role: string; identity: string }[];
|
|
145
114
|
content: string;
|
|
146
|
-
|
|
147
|
-
metadata: {
|
|
148
|
-
subject?: string;
|
|
149
|
-
created_at: string;
|
|
150
|
-
// ... more email metadata
|
|
151
|
-
};
|
|
115
|
+
metadata: { slack_channel_id?: string; ... };
|
|
152
116
|
}
|
|
153
117
|
```
|
|
154
118
|
|
|
155
119
|
---
|
|
156
120
|
|
|
157
|
-
##
|
|
121
|
+
## API key (required)
|
|
122
|
+
All `/api/*` requests require an API key. Create one in the dashboard and reuse it in your client.
|
|
158
123
|
|
|
159
|
-
|
|
124
|
+
```bash
|
|
125
|
+
export COMMUNE_API_KEY="your_key_from_dashboard"
|
|
126
|
+
export COMMUNE_WEBHOOK_SECRET="your_webhook_secret"
|
|
127
|
+
```
|
|
160
128
|
|
|
161
129
|
```ts
|
|
162
|
-
|
|
163
|
-
onEvent: async (message, context) => {
|
|
164
|
-
const sender = message.participants.find(p => p.role === "sender")?.identity;
|
|
165
|
-
if (!sender) return;
|
|
166
|
-
|
|
167
|
-
// Reply in the same thread
|
|
168
|
-
await client.messages.send({
|
|
169
|
-
to: sender,
|
|
170
|
-
text: "Thanks for your question!",
|
|
171
|
-
conversation_id: message.conversation_id, // keeps it in thread
|
|
172
|
-
domainId: context.payload.domainId,
|
|
173
|
-
inboxId: context.payload.inboxId,
|
|
174
|
-
});
|
|
175
|
-
},
|
|
176
|
-
}));
|
|
130
|
+
const client = new CommuneClient({ apiKey: process.env.COMMUNE_API_KEY! });
|
|
177
131
|
```
|
|
178
132
|
|
|
179
133
|
---
|
|
180
134
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
## Access conversation history
|
|
184
|
-
|
|
185
|
-
Give your agent full context by accessing different types of email history.
|
|
135
|
+
## Context (conversation state)
|
|
136
|
+
Commune stores conversation state so your agent can respond with context.
|
|
186
137
|
|
|
187
138
|
```ts
|
|
188
|
-
import { CommuneClient } from "commune
|
|
189
|
-
const client = new CommuneClient({
|
|
190
|
-
|
|
139
|
+
import { CommuneClient } from "@commune/sdk";
|
|
140
|
+
const client = new CommuneClient({ apiKey: process.env.COMMUNE_API_KEY! });
|
|
141
|
+
|
|
142
|
+
// Thread history (email thread or Slack thread)
|
|
143
|
+
const thread = await client.messages.listByConversation(message.conversation_id, {
|
|
144
|
+
order: "asc",
|
|
145
|
+
limit: 50,
|
|
191
146
|
});
|
|
192
147
|
|
|
193
|
-
//
|
|
194
|
-
const
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
);
|
|
148
|
+
// All messages from a user
|
|
149
|
+
const userHistory = await client.messages.list({
|
|
150
|
+
sender: "user@example.com",
|
|
151
|
+
limit: 25,
|
|
152
|
+
});
|
|
198
153
|
|
|
199
|
-
//
|
|
200
|
-
const
|
|
201
|
-
|
|
154
|
+
// All messages in a specific inbox
|
|
155
|
+
const inboxMessages = await client.messages.list({
|
|
156
|
+
inbox_id: "i_xxx",
|
|
157
|
+
channel: "email",
|
|
202
158
|
limit: 50,
|
|
203
|
-
order: "desc", // newest first
|
|
204
159
|
});
|
|
160
|
+
```
|
|
205
161
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
## Cross‑channel behavior (email + Slack in one handler)
|
|
165
|
+
The same `UnifiedMessage` shape works for both channels. You only branch on `channel`.
|
|
166
|
+
|
|
167
|
+
```ts
|
|
168
|
+
import express from "express";
|
|
169
|
+
import { CommuneClient, createWebhookHandler } from "@commune/sdk";
|
|
170
|
+
|
|
171
|
+
// Hosted API is default. If self-hosted, pass { baseUrl: "https://your-api" }
|
|
172
|
+
const client = new CommuneClient({ apiKey: process.env.COMMUNE_API_KEY! });
|
|
211
173
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
174
|
+
const handler = createWebhookHandler({
|
|
175
|
+
onEvent: async (message, context) => {
|
|
176
|
+
if (message.channel === "email") {
|
|
177
|
+
const sender = message.participants.find(p => p.role === "sender")?.identity;
|
|
178
|
+
if (!sender) return;
|
|
179
|
+
|
|
180
|
+
await client.messages.send({
|
|
181
|
+
channel: "email",
|
|
182
|
+
to: sender,
|
|
183
|
+
text: "Got it — thanks for the message.",
|
|
184
|
+
conversation_id: message.conversation_id,
|
|
185
|
+
domainId: context.payload.domainId,
|
|
186
|
+
inboxId: context.payload.inboxId,
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (message.channel === "slack") {
|
|
191
|
+
const channelId = message.metadata.slack_channel_id;
|
|
192
|
+
if (!channelId) return;
|
|
193
|
+
|
|
194
|
+
await client.messages.send({
|
|
195
|
+
channel: "slack",
|
|
196
|
+
to: channelId,
|
|
197
|
+
text: "Replying in thread ✅",
|
|
198
|
+
conversation_id: message.conversation_id,
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
},
|
|
217
202
|
});
|
|
218
203
|
|
|
219
|
-
|
|
204
|
+
const app = express();
|
|
205
|
+
app.post("/commune/webhook", express.raw({ type: "*/*" }), handler);
|
|
206
|
+
app.listen(3000);
|
|
220
207
|
```
|
|
221
208
|
|
|
222
209
|
---
|
|
223
210
|
|
|
224
|
-
|
|
211
|
+
---
|
|
225
212
|
|
|
226
|
-
|
|
213
|
+
## Setup instructions (dashboard-first)
|
|
214
|
+
Domain setup and inbox creation are done in the **Commune dashboard**. You then copy the
|
|
215
|
+
IDs into your code.
|
|
227
216
|
|
|
228
|
-
### Create
|
|
217
|
+
### 1) Create a subdomain for the agent
|
|
218
|
+
Use a subdomain like `agents.yourcompany.com` for deliverability and isolation.
|
|
229
219
|
|
|
230
|
-
|
|
220
|
+
### 2) Create and verify the domain in the dashboard
|
|
221
|
+
The dashboard guides you through DNS (SPF/DKIM/MX) and verification.
|
|
231
222
|
|
|
232
|
-
|
|
233
|
-
|
|
223
|
+
### 3) Create inboxes for your agents in the dashboard
|
|
224
|
+
Each inbox represents an agent address (e.g. `support@agents.yourcompany.com`).
|
|
234
225
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
226
|
+
### 4) Use IDs from the webhook payload
|
|
227
|
+
The webhook payload already includes:
|
|
228
|
+
- `domainId` (e.g. `d_xxx`)
|
|
229
|
+
- `inboxId` (e.g. `i_xxx`)
|
|
238
230
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
const salesInbox = await client.inboxes.create(domainId, {
|
|
252
|
-
localPart: "sales", // Creates sales@yourdomain.com
|
|
253
|
-
agent: {
|
|
254
|
-
id: "sales-agent",
|
|
255
|
-
name: "Sales Bot",
|
|
256
|
-
metadata: { department: "sales" }
|
|
257
|
-
},
|
|
258
|
-
status: "active",
|
|
259
|
-
});
|
|
260
|
-
|
|
261
|
-
return { supportInbox, salesInbox };
|
|
262
|
-
}
|
|
231
|
+
Use them when replying:
|
|
232
|
+
|
|
233
|
+
```ts
|
|
234
|
+
await client.messages.send({
|
|
235
|
+
channel: "email",
|
|
236
|
+
to: "user@example.com",
|
|
237
|
+
text: "Thanks — replying in thread.",
|
|
238
|
+
conversation_id: message.conversation_id,
|
|
239
|
+
domainId: context.payload.domainId,
|
|
240
|
+
inboxId: context.payload.inboxId,
|
|
241
|
+
});
|
|
263
242
|
```
|
|
264
243
|
|
|
265
|
-
|
|
244
|
+
> The SDK also supports programmatic domain/inbox creation, but the dashboard
|
|
245
|
+
> flow is the primary path for most teams.
|
|
266
246
|
|
|
267
|
-
|
|
247
|
+
### 5) Set your webhook secret
|
|
248
|
+
When you configure the inbox webhook in the dashboard, Commune shows a **webhook secret**.
|
|
249
|
+
Store it as:
|
|
268
250
|
|
|
269
|
-
```
|
|
270
|
-
|
|
271
|
-
// Support inbox webhook
|
|
272
|
-
await client.inboxes.setWebhook(domainId, inboxes.supportInbox.id, {
|
|
273
|
-
endpoint: "https://your-api.com/webhooks/support",
|
|
274
|
-
events: ["email.received"],
|
|
275
|
-
});
|
|
276
|
-
|
|
277
|
-
// Sales inbox webhook
|
|
278
|
-
await client.inboxes.setWebhook(domainId, inboxes.salesInbox.id, {
|
|
279
|
-
endpoint: "https://your-api.com/webhooks/sales",
|
|
280
|
-
events: ["email.received"],
|
|
281
|
-
});
|
|
282
|
-
}
|
|
251
|
+
```bash
|
|
252
|
+
export COMMUNE_WEBHOOK_SECRET="your_webhook_secret"
|
|
283
253
|
```
|
|
284
254
|
|
|
285
|
-
|
|
255
|
+
Use it in the `verify` function shown above.
|
|
286
256
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
const inboxes = await client.inboxes.list(domainId);
|
|
290
|
-
console.log(`Found ${inboxes.length} inboxes`);
|
|
291
|
-
|
|
292
|
-
// Update an inbox
|
|
293
|
-
await client.inboxes.update(domainId, inboxId, {
|
|
294
|
-
localPart: "help", // Change from "support" to "help"
|
|
295
|
-
status: "active",
|
|
296
|
-
});
|
|
257
|
+
### 6) Create an API key in the dashboard
|
|
258
|
+
Use the dashboard to create an API key, then set it as:
|
|
297
259
|
|
|
298
|
-
|
|
299
|
-
|
|
260
|
+
```bash
|
|
261
|
+
export COMMUNE_API_KEY="your_key_from_dashboard"
|
|
300
262
|
```
|
|
301
263
|
|
|
302
264
|
---
|
|
303
265
|
|
|
304
|
-
##
|
|
266
|
+
## Structured extraction (per inbox)
|
|
267
|
+
You can attach a **JSON schema** to a specific inbox so Commune extracts structured data from inbound emails.
|
|
305
268
|
|
|
306
|
-
|
|
269
|
+
### 1) Add a schema to an inbox (dashboard)
|
|
270
|
+
In **Dashboard → Inboxes**, open an inbox and add a Structured Extraction schema. Save it and enable extraction.
|
|
307
271
|
|
|
272
|
+
### 2) Set the schema via SDK (optional)
|
|
308
273
|
```ts
|
|
309
|
-
|
|
310
|
-
|
|
274
|
+
await client.inboxes.setExtractionSchema({
|
|
275
|
+
domainId: "domain-123",
|
|
276
|
+
inboxId: "inbox-456",
|
|
277
|
+
schema: {
|
|
278
|
+
name: "invoice_extraction",
|
|
279
|
+
description: "Extract invoice details",
|
|
280
|
+
enabled: true,
|
|
281
|
+
schema: {
|
|
282
|
+
type: "object",
|
|
283
|
+
properties: {
|
|
284
|
+
invoiceNumber: { type: "string" },
|
|
285
|
+
amount: { type: "number" },
|
|
286
|
+
dueDate: { type: "string" }
|
|
287
|
+
},
|
|
288
|
+
required: ["invoiceNumber", "amount"],
|
|
289
|
+
additionalProperties: false
|
|
290
|
+
}
|
|
291
|
+
}
|
|
311
292
|
});
|
|
293
|
+
```
|
|
312
294
|
|
|
313
|
-
|
|
295
|
+
### 3) Read extracted data in your webhook
|
|
296
|
+
The webhook payload includes the structured output when extraction is enabled.
|
|
314
297
|
|
|
315
|
-
|
|
316
|
-
|
|
298
|
+
```ts
|
|
299
|
+
const handler = createWebhookHandler({
|
|
317
300
|
onEvent: async (message, context) => {
|
|
318
|
-
const
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
inboxId: context.payload.inboxId,
|
|
327
|
-
});
|
|
301
|
+
const extracted = context.payload.extractedData
|
|
302
|
+
|| message.metadata?.extracted_data
|
|
303
|
+
|| null;
|
|
304
|
+
|
|
305
|
+
if (extracted) {
|
|
306
|
+
// use structured fields in your agent workflow
|
|
307
|
+
console.log("Extracted:", extracted);
|
|
308
|
+
}
|
|
328
309
|
},
|
|
329
|
-
})
|
|
310
|
+
});
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
> Tip: Keep your schema minimal (only the fields you need). You can evolve it over time.
|
|
314
|
+
|
|
315
|
+
### Example: Invoice extraction (end-to-end)
|
|
316
|
+
```ts
|
|
317
|
+
import { CommuneClient, createWebhookHandler } from "@commune/sdk";
|
|
318
|
+
|
|
319
|
+
const client = new CommuneClient({ apiKey: process.env.COMMUNE_API_KEY! });
|
|
320
|
+
|
|
321
|
+
await client.inboxes.setExtractionSchema({
|
|
322
|
+
domainId: "domain-123",
|
|
323
|
+
inboxId: "inbox-456",
|
|
324
|
+
schema: {
|
|
325
|
+
name: "invoice_extraction",
|
|
326
|
+
enabled: true,
|
|
327
|
+
schema: {
|
|
328
|
+
type: "object",
|
|
329
|
+
properties: {
|
|
330
|
+
invoiceNumber: { type: "string" },
|
|
331
|
+
amount: { type: "number" },
|
|
332
|
+
vendor: { type: "string" }
|
|
333
|
+
},
|
|
334
|
+
required: ["invoiceNumber", "amount"],
|
|
335
|
+
additionalProperties: false
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
});
|
|
330
339
|
|
|
331
|
-
|
|
332
|
-
app.post("/webhook/marketing", express.raw({ type: "*/*" }), createWebhookHandler({
|
|
340
|
+
const handler = createWebhookHandler({
|
|
333
341
|
onEvent: async (message, context) => {
|
|
334
|
-
const
|
|
335
|
-
if (!
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
text: "Thanks for subscribing to our newsletter!",
|
|
340
|
-
conversation_id: message.conversation_id,
|
|
341
|
-
domainId: context.payload.domainId,
|
|
342
|
-
inboxId: context.payload.inboxId,
|
|
343
|
-
});
|
|
344
|
-
},
|
|
345
|
-
}));
|
|
342
|
+
const extracted = context.payload.extractedData;
|
|
343
|
+
if (!extracted) return;
|
|
344
|
+
|
|
345
|
+
console.log("Invoice:", extracted.invoiceNumber);
|
|
346
|
+
console.log("Amount:", extracted.amount);
|
|
346
347
|
|
|
347
|
-
|
|
348
|
-
|
|
348
|
+
await processInvoice(extracted);
|
|
349
|
+
},
|
|
349
350
|
});
|
|
350
351
|
```
|
|
351
352
|
|
|
352
353
|
---
|
|
353
354
|
|
|
354
|
-
##
|
|
355
|
-
|
|
356
|
-
|
|
355
|
+
## Webhook verification (Commune → your app)
|
|
356
|
+
Commune signs outbound webhooks using your **inbox webhook secret**. Verify the
|
|
357
|
+
signature before processing the request.
|
|
357
358
|
|
|
358
359
|
```ts
|
|
359
|
-
import {
|
|
360
|
-
|
|
361
|
-
const
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
360
|
+
import { createWebhookHandler, verifyCommuneWebhook } from "@commune/sdk";
|
|
361
|
+
|
|
362
|
+
const handler = createWebhookHandler({
|
|
363
|
+
verify: ({ rawBody, headers }) => {
|
|
364
|
+
const signature = headers["x-commune-signature"];
|
|
365
|
+
const timestamp = headers["x-commune-timestamp"];
|
|
366
|
+
if (!signature || !timestamp) return false;
|
|
367
|
+
|
|
368
|
+
return verifyCommuneWebhook({
|
|
369
|
+
rawBody,
|
|
370
|
+
timestamp,
|
|
371
|
+
signature,
|
|
372
|
+
secret: process.env.COMMUNE_WEBHOOK_SECRET!,
|
|
373
|
+
});
|
|
374
|
+
},
|
|
375
|
+
onEvent: async (message) => {
|
|
376
|
+
// handle verified message
|
|
377
|
+
},
|
|
372
378
|
});
|
|
373
|
-
|
|
374
|
-
console.log(`Found ${results.length} relevant emails:`);
|
|
375
|
-
for (const result of results) {
|
|
376
|
-
console.log(`Match (${result.similarity.toFixed(2)}): ${result.message.content}`);
|
|
377
|
-
console.log(`Highlights: ${result.highlights.join(", ")}`);
|
|
378
|
-
}
|
|
379
379
|
```
|
|
380
380
|
|
|
381
|
-
**What you get:**
|
|
382
|
-
- **Natural language queries** - Search like "pricing issues from enterprise customers"
|
|
383
|
-
- **Similarity scores** - Ranked results by relevance (0-1)
|
|
384
|
-
- **Highlighted matches** - Text snippets showing why the email matched
|
|
385
|
-
- **Filtering options** - By date range, sender, or other criteria
|
|
386
|
-
- **Fast results** - AI-powered semantic matching
|
|
387
|
-
|
|
388
381
|
---
|
|
389
382
|
|
|
390
|
-
##
|
|
391
|
-
|
|
392
|
-
|
|
383
|
+
## Full example (single file)
|
|
384
|
+
A complete copy‑paste example that:
|
|
385
|
+
- receives webhook
|
|
386
|
+
- replies by email
|
|
387
|
+
- replies in Slack thread
|
|
388
|
+
- fetches conversation history
|
|
393
389
|
|
|
394
390
|
```ts
|
|
395
|
-
import "dotenv/config";
|
|
396
391
|
import express from "express";
|
|
397
|
-
import { CommuneClient, createWebhookHandler } from "commune
|
|
398
|
-
|
|
399
|
-
const client = new CommuneClient({
|
|
400
|
-
apiKey: process.env.COMMUNE_API_KEY,
|
|
401
|
-
});
|
|
392
|
+
import { CommuneClient, createWebhookHandler } from "@commune/sdk";
|
|
402
393
|
|
|
403
|
-
const
|
|
394
|
+
const client = new CommuneClient({ apiKey: process.env.COMMUNE_API_KEY! });
|
|
404
395
|
|
|
405
|
-
|
|
396
|
+
const handler = createWebhookHandler({
|
|
406
397
|
onEvent: async (message, context) => {
|
|
407
|
-
//
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
398
|
+
// 1) Email reply (same thread)
|
|
399
|
+
if (message.channel === "email") {
|
|
400
|
+
const sender = message.participants.find(p => p.role === "sender")?.identity;
|
|
401
|
+
if (!sender) return;
|
|
402
|
+
|
|
403
|
+
await client.messages.send({
|
|
404
|
+
channel: "email",
|
|
405
|
+
to: sender,
|
|
406
|
+
text: "Thanks! We received your email.",
|
|
407
|
+
conversation_id: message.conversation_id,
|
|
408
|
+
domainId: context.payload.domainId,
|
|
409
|
+
inboxId: context.payload.inboxId,
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// 2) Slack reply (same thread)
|
|
416
|
+
if (message.channel === "slack") {
|
|
417
|
+
const channelId = message.metadata.slack_channel_id;
|
|
418
|
+
if (!channelId) return;
|
|
419
|
+
|
|
420
|
+
await client.messages.send({
|
|
421
|
+
channel: "slack",
|
|
422
|
+
to: channelId,
|
|
423
|
+
text: "Thanks! Replying in thread.",
|
|
424
|
+
conversation_id: message.conversation_id,
|
|
425
|
+
});
|
|
426
|
+
}
|
|
419
427
|
},
|
|
420
|
-
}));
|
|
421
|
-
|
|
422
|
-
app.listen(3000, () => {
|
|
423
|
-
console.log("Email agent running on port 3000");
|
|
424
428
|
});
|
|
429
|
+
|
|
430
|
+
const app = express();
|
|
431
|
+
app.post("/commune/webhook", express.raw({ type: "*/*" }), handler);
|
|
432
|
+
app.listen(3000, () => console.log("listening on 3000"));
|
|
425
433
|
```
|
package/dist/client.d.ts
CHANGED
|
@@ -19,11 +19,6 @@ export declare class CommuneClient {
|
|
|
19
19
|
verify: (domainId: string) => Promise<Record<string, unknown>>;
|
|
20
20
|
records: (domainId: string) => Promise<unknown[]>;
|
|
21
21
|
status: (domainId: string) => Promise<Record<string, unknown>>;
|
|
22
|
-
createWebhook: (domainId: string, payload?: {
|
|
23
|
-
endpoint?: string;
|
|
24
|
-
events?: string[];
|
|
25
|
-
}) => Promise<Record<string, unknown>>;
|
|
26
|
-
saveWebhookSecret: (domainId: string, secret: string) => Promise<Record<string, unknown>>;
|
|
27
22
|
};
|
|
28
23
|
inboxes: {
|
|
29
24
|
list: (domainId: string) => Promise<InboxEntry[]>;
|
|
@@ -46,6 +41,20 @@ export declare class CommuneClient {
|
|
|
46
41
|
endpoint: string;
|
|
47
42
|
events?: string[];
|
|
48
43
|
}) => Promise<InboxEntry>;
|
|
44
|
+
setExtractionSchema: (payload: {
|
|
45
|
+
domainId: string;
|
|
46
|
+
inboxId: string;
|
|
47
|
+
schema: {
|
|
48
|
+
name: string;
|
|
49
|
+
description?: string;
|
|
50
|
+
enabled?: boolean;
|
|
51
|
+
schema: Record<string, any>;
|
|
52
|
+
};
|
|
53
|
+
}) => Promise<InboxEntry>;
|
|
54
|
+
removeExtractionSchema: (payload: {
|
|
55
|
+
domainId: string;
|
|
56
|
+
inboxId: string;
|
|
57
|
+
}) => Promise<InboxEntry>;
|
|
49
58
|
};
|
|
50
59
|
messages: {
|
|
51
60
|
send: (payload: SendMessagePayload) => Promise<Record<string, unknown>>;
|
package/dist/client.js
CHANGED
|
@@ -38,15 +38,6 @@ export class CommuneClient {
|
|
|
38
38
|
status: async (domainId) => {
|
|
39
39
|
return this.request(`/api/domains/${encodeURIComponent(domainId)}/status`);
|
|
40
40
|
},
|
|
41
|
-
createWebhook: async (domainId, payload) => {
|
|
42
|
-
return this.request(`/api/domains/${encodeURIComponent(domainId)}/webhook`, {
|
|
43
|
-
method: 'POST',
|
|
44
|
-
json: payload,
|
|
45
|
-
});
|
|
46
|
-
},
|
|
47
|
-
saveWebhookSecret: async (domainId, secret) => {
|
|
48
|
-
return this.request(`/api/domains/${encodeURIComponent(domainId)}/webhook/secret`, { method: 'POST', json: { secret } });
|
|
49
|
-
},
|
|
50
41
|
};
|
|
51
42
|
this.inboxes = {
|
|
52
43
|
list: async (domainId) => {
|
|
@@ -70,14 +61,20 @@ export class CommuneClient {
|
|
|
70
61
|
setWebhook: async (domainId, inboxId, payload) => {
|
|
71
62
|
return this.request(`/api/domains/${encodeURIComponent(domainId)}/inboxes/${encodeURIComponent(inboxId)}/webhook`, { method: 'POST', json: payload });
|
|
72
63
|
},
|
|
64
|
+
setExtractionSchema: async (payload) => {
|
|
65
|
+
const { domainId, inboxId, schema } = payload;
|
|
66
|
+
return this.request(`/api/domains/${encodeURIComponent(domainId)}/inboxes/${encodeURIComponent(inboxId)}/extraction-schema`, { method: 'PUT', json: schema });
|
|
67
|
+
},
|
|
68
|
+
removeExtractionSchema: async (payload) => {
|
|
69
|
+
const { domainId, inboxId } = payload;
|
|
70
|
+
return this.request(`/api/domains/${encodeURIComponent(domainId)}/inboxes/${encodeURIComponent(inboxId)}/extraction-schema`, { method: 'DELETE' });
|
|
71
|
+
},
|
|
73
72
|
};
|
|
74
73
|
this.messages = {
|
|
75
74
|
send: async (payload) => {
|
|
76
|
-
// Always send as email (Slack support coming soon)
|
|
77
|
-
const emailPayload = { ...payload, channel: 'email' };
|
|
78
75
|
return this.request('/api/messages/send', {
|
|
79
76
|
method: 'POST',
|
|
80
|
-
json:
|
|
77
|
+
json: payload,
|
|
81
78
|
});
|
|
82
79
|
},
|
|
83
80
|
list: async (params) => {
|
|
@@ -99,10 +96,8 @@ export class CommuneClient {
|
|
|
99
96
|
})}`);
|
|
100
97
|
},
|
|
101
98
|
};
|
|
102
|
-
// Semantic search across all organizational emails
|
|
99
|
+
// Semantic search across all organizational emails
|
|
103
100
|
this.search = async (params) => {
|
|
104
|
-
// Placeholder for future semantic search implementation
|
|
105
|
-
// This will search across all emails in the organization
|
|
106
101
|
return this.request(`/api/search${buildQuery({
|
|
107
102
|
q: params.query,
|
|
108
103
|
limit: params.limit || 10,
|
|
@@ -117,9 +112,6 @@ export class CommuneClient {
|
|
|
117
112
|
return this.request(`/api/attachments/${encodeURIComponent(attachmentId)}`);
|
|
118
113
|
},
|
|
119
114
|
};
|
|
120
|
-
if (!options.apiKey) {
|
|
121
|
-
throw new Error('API key is required. Get one from your Commune dashboard at https://commune.ai');
|
|
122
|
-
}
|
|
123
115
|
this.baseUrl = (options.baseUrl || DEFAULT_BASE_URL).replace(/\/$/, '');
|
|
124
116
|
this.apiKey = options.apiKey;
|
|
125
117
|
this.headers = options.headers;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { CommuneClient } from './client.js';
|
|
2
|
-
export type { ApiError, ApiResponse, AttachmentRecord, Channel, ConversationListParams, CreateDomainPayload, Direction, DomainEntry, DomainWebhook, InboxEntry, InboxWebhook, InboundEmailWebhookPayload, MessageListParams, MessageMetadata, Participant, ParticipantRole,
|
|
2
|
+
export type { ApiError, ApiResponse, AttachmentRecord, Channel, ConversationListParams, CreateDomainPayload, Direction, DomainEntry, DomainWebhook, InboxEntry, InboxWebhook, InboundEmailWebhookPayload, MessageListParams, MessageMetadata, Participant, ParticipantRole, SendMessagePayload, SvixHeaders, UnifiedMessage, } from './types.js';
|
|
3
3
|
export { verifyResendWebhook } from './webhooks.js';
|
|
4
4
|
export { createWebhookHandler } from './listener.js';
|
|
5
5
|
export type { CommuneWebhookEvent, CommuneWebhookHandlerContext, CreateWebhookHandlerOptions, } from './listener.js';
|
package/dist/index.js
CHANGED
package/dist/listener.js
CHANGED
|
@@ -49,6 +49,7 @@ export const createWebhookHandler = ({ onEvent, verify }) => {
|
|
|
49
49
|
const headers = isWebRequest
|
|
50
50
|
? {
|
|
51
51
|
'x-commune-signature': req.headers.get('x-commune-signature') || undefined,
|
|
52
|
+
'x-commune-timestamp': req.headers.get('x-commune-timestamp') || undefined,
|
|
52
53
|
}
|
|
53
54
|
: getHeadersFromNode(req);
|
|
54
55
|
if (verify && !verify({ rawBody, headers })) {
|
package/dist/types.d.ts
CHANGED
|
@@ -17,6 +17,7 @@ export interface MessageMetadata {
|
|
|
17
17
|
message_id?: string | null;
|
|
18
18
|
provider?: 'resend' | 'email' | string;
|
|
19
19
|
raw?: unknown;
|
|
20
|
+
extracted_data?: Record<string, any>;
|
|
20
21
|
}
|
|
21
22
|
export interface UnifiedMessage {
|
|
22
23
|
_id?: string;
|
|
@@ -51,6 +52,7 @@ export interface DomainWebhook {
|
|
|
51
52
|
export interface InboxWebhook {
|
|
52
53
|
endpoint?: string;
|
|
53
54
|
events?: string[];
|
|
55
|
+
secret?: string;
|
|
54
56
|
}
|
|
55
57
|
export interface InboxEntry {
|
|
56
58
|
id: string;
|
|
@@ -62,6 +64,12 @@ export interface InboxEntry {
|
|
|
62
64
|
metadata?: Record<string, unknown>;
|
|
63
65
|
};
|
|
64
66
|
webhook?: InboxWebhook;
|
|
67
|
+
extractionSchema?: {
|
|
68
|
+
name: string;
|
|
69
|
+
description?: string;
|
|
70
|
+
schema: Record<string, any>;
|
|
71
|
+
enabled: boolean;
|
|
72
|
+
};
|
|
65
73
|
createdAt?: string;
|
|
66
74
|
status?: string;
|
|
67
75
|
}
|
|
@@ -131,6 +139,7 @@ export interface InboundEmailWebhookPayload {
|
|
|
131
139
|
event: unknown;
|
|
132
140
|
email: unknown;
|
|
133
141
|
message: UnifiedMessage;
|
|
142
|
+
extractedData?: Record<string, any>;
|
|
134
143
|
}
|
|
135
144
|
export interface ApiError {
|
|
136
145
|
message?: string;
|
package/dist/webhooks.js
CHANGED
|
@@ -8,30 +8,3 @@ export const verifyResendWebhook = (payload, headers, secret) => {
|
|
|
8
8
|
};
|
|
9
9
|
return webhook.verify(payload, svixHeaders);
|
|
10
10
|
};
|
|
11
|
-
// Slack webhook verification (coming soon)
|
|
12
|
-
// export const verifySlackWebhook = ({
|
|
13
|
-
// rawBody,
|
|
14
|
-
// timestamp,
|
|
15
|
-
// signature,
|
|
16
|
-
// signingSecret,
|
|
17
|
-
// }: {
|
|
18
|
-
// rawBody: string;
|
|
19
|
-
// timestamp: string;
|
|
20
|
-
// signature: string;
|
|
21
|
-
// signingSecret: string;
|
|
22
|
-
// }) => {
|
|
23
|
-
// const baseString = `v0:${timestamp}:${rawBody}`;
|
|
24
|
-
// const hmac = crypto
|
|
25
|
-
// .createHmac('sha256', signingSecret)
|
|
26
|
-
// .update(baseString, 'utf8')
|
|
27
|
-
// .digest('hex');
|
|
28
|
-
//
|
|
29
|
-
// const expected = `v0=${hmac}`;
|
|
30
|
-
// const expectedBuffer = Buffer.from(expected, 'utf8');
|
|
31
|
-
// const signatureBuffer = Buffer.from(signature, 'utf8');
|
|
32
|
-
// if (expectedBuffer.length !== signatureBuffer.length) {
|
|
33
|
-
// return false;
|
|
34
|
-
// }
|
|
35
|
-
//
|
|
36
|
-
// return crypto.timingSafeEqual(expectedBuffer, signatureBuffer);
|
|
37
|
-
// };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "commune-ai",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.4",
|
|
4
4
|
"description": "Our email infrastructure - webhooks, threads, history, and semantic search",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -15,9 +15,30 @@
|
|
|
15
15
|
"dist",
|
|
16
16
|
"README.md"
|
|
17
17
|
],
|
|
18
|
+
"keywords": [
|
|
19
|
+
"email",
|
|
20
|
+
"slack",
|
|
21
|
+
"ai",
|
|
22
|
+
"agent",
|
|
23
|
+
"webhook",
|
|
24
|
+
"communication",
|
|
25
|
+
"inbox",
|
|
26
|
+
"api"
|
|
27
|
+
],
|
|
28
|
+
"license": "MIT",
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "https://github.com/commune-ai/commune"
|
|
32
|
+
},
|
|
33
|
+
"homepage": "https://github.com/commune-ai/commune#readme",
|
|
34
|
+
"bugs": {
|
|
35
|
+
"url": "https://github.com/commune-ai/commune/issues"
|
|
36
|
+
},
|
|
18
37
|
"scripts": {
|
|
19
38
|
"build": "tsc -p tsconfig.json",
|
|
20
|
-
"clean": "rm -rf dist"
|
|
39
|
+
"clean": "rm -rf dist",
|
|
40
|
+
"prepublishOnly": "npm run clean && npm run build",
|
|
41
|
+
"prepack": "npm run build"
|
|
21
42
|
},
|
|
22
43
|
"dependencies": {
|
|
23
44
|
"svix": "^1.44.0"
|