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 CHANGED
@@ -1,425 +1,433 @@
1
- # commune-ai
1
+ # @commune/sdk
2
2
 
3
- **Our email infrastructure**
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
- Build agents that receive emails, reply in threads, access conversation history, and search across all organizational emails. Most teams get their first agent responding in **~15 minutes**.
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
- ## What you get
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
- 1. **Webhook Integration** - Receive emails instantly as structured data
10
- 2. **Thread-aware Replies** - Reply in email conversations with full context
11
- 3. **User History** - Access complete conversation history per user
12
- 4. **Organization Search** - Semantic search across all emails (coming soon)
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
- ## Table of Contents
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-ai";
33
+ import { CommuneClient, createWebhookHandler, verifyCommuneWebhook } from "@commune/sdk";
101
34
 
102
- // Initialize client
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
- // Set up your server
108
- const app = express();
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
- // Handle incoming emails at your webhook endpoint
111
- app.post("/webhook", express.raw({ type: "*/*" }), createWebhookHandler({
112
- onEvent: async (message, context) => {
113
- console.log(`📧 New email: ${message.content}`);
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
- app.listen(3000, () => {
131
- console.log("Agent listening on port 3000");
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
- **Email structure:**
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 ID
141
- participants: Array<{
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
- content_html?: string;
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
- ## Reply in email threads
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
- Keep conversations organized by replying in the same email thread:
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
- app.post("/webhook", express.raw({ type: "*/*" }), createWebhookHandler({
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-ai";
189
- const client = new CommuneClient({
190
- apiKey: process.env.COMMUNE_API_KEY,
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
- // 1) Thread history - all messages in current conversation
194
- const threadMessages = await client.messages.listByConversation(
195
- message.conversation_id,
196
- { order: "asc", limit: 20 }
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
- // 2) User history - all emails from/to a specific person
200
- const userEmails = await client.messages.list({
201
- sender: "customer@example.com",
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
- // 3) Inbox history - all emails in your domain
207
- const inboxEmails = await client.messages.list({
208
- inbox_id: "inbox_123",
209
- limit: 100,
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
- // 4) Organization search - semantic search across all emails (coming soon)
213
- const searchResults = await client.search({
214
- query: "pricing questions from last week",
215
- limit: 10,
216
- threshold: 0.8,
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
- console.log(`Found ${searchResults.length} relevant emails`);
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
- ## Manage Inboxes Programmatically
211
+ ---
225
212
 
226
- Create and manage email inboxes for your agents programmatically.
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 Multiple Inboxes
217
+ ### 1) Create a subdomain for the agent
218
+ Use a subdomain like `agents.yourcompany.com` for deliverability and isolation.
229
219
 
230
- Set up different inboxes for different purposes:
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
- ```ts
233
- import { CommuneClient } from "commune-ai";
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
- const client = new CommuneClient({
236
- apiKey: process.env.COMMUNE_API_KEY,
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
- // Create inboxes for different departments
240
- async function setupInboxes(domainId: string) {
241
- const supportInbox = await client.inboxes.create(domainId, {
242
- localPart: "support", // Creates support@yourdomain.com
243
- agent: {
244
- id: "support-agent",
245
- name: "Support Bot",
246
- metadata: { department: "customer-success" }
247
- },
248
- status: "active",
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
- ### Set Webhooks for Specific Inboxes
244
+ > The SDK also supports programmatic domain/inbox creation, but the dashboard
245
+ > flow is the primary path for most teams.
266
246
 
267
- Configure webhooks per inbox to route emails to different handlers:
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
- ```ts
270
- async function configureWebhooks(domainId: string, inboxes: any) {
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
- ### List and Manage Inboxes
255
+ Use it in the `verify` function shown above.
286
256
 
287
- ```ts
288
- // List all inboxes for a domain
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
- // Delete an inbox
299
- await client.inboxes.remove(domainId, inboxId);
260
+ ```bash
261
+ export COMMUNE_API_KEY="your_key_from_dashboard"
300
262
  ```
301
263
 
302
264
  ---
303
265
 
304
- ## Handle different inbox emails
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
- Set up separate webhook endpoints for different inboxes:
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
- const client = new CommuneClient({
310
- apiKey: process.env.COMMUNE_API_KEY,
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
- const app = express();
295
+ ### 3) Read extracted data in your webhook
296
+ The webhook payload includes the structured output when extraction is enabled.
314
297
 
315
- // Sales inbox webhook - handles sales@yourdomain.com
316
- app.post("/webhook/sales", express.raw({ type: "*/*" }), createWebhookHandler({
298
+ ```ts
299
+ const handler = createWebhookHandler({
317
300
  onEvent: async (message, context) => {
318
- const sender = message.participants.find(p => p.role === "sender")?.identity;
319
- if (!sender) return;
320
-
321
- await client.messages.send({
322
- to: sender,
323
- text: "Thanks for your sales inquiry!",
324
- conversation_id: message.conversation_id,
325
- domainId: context.payload.domainId,
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
- // Marketing inbox webhook - handles marketing@yourdomain.com
332
- app.post("/webhook/marketing", express.raw({ type: "*/*" }), createWebhookHandler({
340
+ const handler = createWebhookHandler({
333
341
  onEvent: async (message, context) => {
334
- const sender = message.participants.find(p => p.role === "sender")?.identity;
335
- if (!sender) return;
336
-
337
- await client.messages.send({
338
- to: sender,
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
- app.listen(3000, () => {
348
- console.log("Agent running on port 3000");
348
+ await processInvoice(extracted);
349
+ },
349
350
  });
350
351
  ```
351
352
 
352
353
  ---
353
354
 
354
- ## Semantic Search (Coming Soon)
355
-
356
- Search across all emails in your organization using natural language queries.
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 { CommuneClient } from "commune-ai";
360
-
361
- const client = new CommuneClient({
362
- apiKey: process.env.COMMUNE_API_KEY,
363
- });
364
-
365
- // Find emails similar to a query
366
- const results = await client.search({
367
- query: "What were the pricing questions from last week?",
368
- limit: 10,
369
- threshold: 0.7, // Similarity threshold 0-1
370
- before: "2024-12-31", // Only search emails before this date
371
- sender: "customer@example.com", // Filter by sender (optional)
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
- ## Complete example
391
-
392
- A simple email agent that receives emails and replies:
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-ai";
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 app = express();
394
+ const client = new CommuneClient({ apiKey: process.env.COMMUNE_API_KEY! });
404
395
 
405
- app.post("/webhook", express.raw({ type: "*/*" }), createWebhookHandler({
396
+ const handler = createWebhookHandler({
406
397
  onEvent: async (message, context) => {
407
- // Get the sender's email address
408
- const sender = message.participants.find(p => p.role === "sender")?.identity;
409
- if (!sender) return;
410
-
411
- // Reply to the email
412
- await client.messages.send({
413
- to: sender,
414
- text: "Thanks for your email! I'll get back to you soon.",
415
- conversation_id: message.conversation_id,
416
- domainId: context.payload.domainId,
417
- inboxId: context.payload.inboxId,
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: emailPayload,
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 (coming soon)
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, SemanticSearchParams, SemanticSearchResult, SendMessagePayload, SvixHeaders, UnifiedMessage, } from './types.js';
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
@@ -1,4 +1,3 @@
1
1
  export { CommuneClient } from './client.js';
2
2
  export { verifyResendWebhook } from './webhooks.js';
3
- // export { verifySlackWebhook } from './webhooks.js'; // Slack support coming soon
4
3
  export { createWebhookHandler } from './listener.js';
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.1",
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"