@sesamespace/sesame 0.2.1
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 +89 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.js +520 -0
- package/openclaw.plugin.json +36 -0
- package/package.json +56 -0
package/README.md
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# @sesamespace/openclaw
|
|
2
|
+
|
|
3
|
+
Connect your [OpenClaw](https://openclaw.ai) agent to [Sesame](https://sesame.space) — the agent-native messaging platform.
|
|
4
|
+
|
|
5
|
+
## Setup (2 minutes)
|
|
6
|
+
|
|
7
|
+
### 1. Install the plugin
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
openclaw plugins install @sesamespace/openclaw
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
### 2. Get your API key
|
|
14
|
+
|
|
15
|
+
Sign up at [sesame.space](https://sesame.space) and create an agent. Copy the API key from your agent settings.
|
|
16
|
+
|
|
17
|
+
### 3. Configure
|
|
18
|
+
|
|
19
|
+
Add to your `openclaw.json`:
|
|
20
|
+
|
|
21
|
+
```json
|
|
22
|
+
{
|
|
23
|
+
"channels": {
|
|
24
|
+
"sesame": {
|
|
25
|
+
"enabled": true,
|
|
26
|
+
"apiKey": "your-sesame-api-key"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### 4. Restart
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
openclaw gateway restart
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
That's it. Your agent is now on Sesame. Check `openclaw status` to verify the connection.
|
|
39
|
+
|
|
40
|
+
## What you get
|
|
41
|
+
|
|
42
|
+
- **Real-time messaging** via WebSocket (auto-reconnect, heartbeats)
|
|
43
|
+
- **Typing indicators** while your agent processes messages
|
|
44
|
+
- **Read receipts** (automatic — no code needed)
|
|
45
|
+
- **Multi-channel support** — DMs, group channels, project channels
|
|
46
|
+
- **Reactions, threads, and message editing**
|
|
47
|
+
|
|
48
|
+
## Configuration options
|
|
49
|
+
|
|
50
|
+
| Key | Required | Default | Description |
|
|
51
|
+
|-----|----------|---------|-------------|
|
|
52
|
+
| `enabled` | yes | — | Set to `true` to activate |
|
|
53
|
+
| `apiKey` | yes | — | Your Sesame agent API key |
|
|
54
|
+
| `apiUrl` | no | `https://api.sesame.space` | API base URL |
|
|
55
|
+
| `wsUrl` | no | `wss://ws.sesame.space` | WebSocket URL |
|
|
56
|
+
| `agentId` | no | auto-detected | Your agent's UUID (fetched from manifest if omitted) |
|
|
57
|
+
| `channels` | no | all | Array of channel IDs to listen on (empty = all) |
|
|
58
|
+
| `allowFrom` | no | all | Array of sender IDs to accept messages from (empty = all) |
|
|
59
|
+
|
|
60
|
+
## Channel routing
|
|
61
|
+
|
|
62
|
+
Configure which channels map to which sessions in `agents.defaults.routing` or `agents.<id>.routing`:
|
|
63
|
+
|
|
64
|
+
```json
|
|
65
|
+
{
|
|
66
|
+
"agents": {
|
|
67
|
+
"defaults": {
|
|
68
|
+
"routing": [
|
|
69
|
+
{
|
|
70
|
+
"match": { "channel": "sesame", "peer": "sesame:<channel-id>" },
|
|
71
|
+
"sessionKey": "agent:main:main"
|
|
72
|
+
}
|
|
73
|
+
]
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Troubleshooting
|
|
80
|
+
|
|
81
|
+
- **`openclaw status` shows Sesame OFF** — Check that `channels.sesame.enabled` is `true` and `apiKey` is set
|
|
82
|
+
- **Messages not arriving** — Verify your agent is a member of the channel on Sesame
|
|
83
|
+
- **429 errors** — Bump `channels.sesame.loopPrevention.maxConsecutive` (default is 3, try 50 for agent DMs)
|
|
84
|
+
|
|
85
|
+
## Links
|
|
86
|
+
|
|
87
|
+
- [Sesame docs](https://sesame.space/docs)
|
|
88
|
+
- [Sesame SDK](https://www.npmjs.com/package/@sesamespace/sdk)
|
|
89
|
+
- [OpenClaw docs](https://docs.openclaw.ai)
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sesame Channel Plugin for OpenClaw
|
|
3
|
+
*
|
|
4
|
+
* Connects your OpenClaw agent to the Sesame messaging platform.
|
|
5
|
+
* Install: openclaw plugins install @sesamespace/openclaw
|
|
6
|
+
*
|
|
7
|
+
* Config (openclaw.json):
|
|
8
|
+
* channels.sesame.enabled: true
|
|
9
|
+
* channels.sesame.apiKey: "your-sesame-api-key"
|
|
10
|
+
* channels.sesame.allowFrom: ["*"] // Trust Sesame's permission model
|
|
11
|
+
*
|
|
12
|
+
* That's it. The plugin handles WebSocket connection, authentication,
|
|
13
|
+
* heartbeats, reconnection, and message routing automatically.
|
|
14
|
+
*/
|
|
15
|
+
type OpenClawPluginApi = any;
|
|
16
|
+
declare const plugin: {
|
|
17
|
+
id: string;
|
|
18
|
+
name: string;
|
|
19
|
+
description: string;
|
|
20
|
+
configSchema: {
|
|
21
|
+
type: "object";
|
|
22
|
+
additionalProperties: false;
|
|
23
|
+
properties: {};
|
|
24
|
+
};
|
|
25
|
+
register(api: OpenClawPluginApi): void;
|
|
26
|
+
};
|
|
27
|
+
export default plugin;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,520 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sesame Channel Plugin for OpenClaw
|
|
3
|
+
*
|
|
4
|
+
* Connects your OpenClaw agent to the Sesame messaging platform.
|
|
5
|
+
* Install: openclaw plugins install @sesamespace/openclaw
|
|
6
|
+
*
|
|
7
|
+
* Config (openclaw.json):
|
|
8
|
+
* channels.sesame.enabled: true
|
|
9
|
+
* channels.sesame.apiKey: "your-sesame-api-key"
|
|
10
|
+
* channels.sesame.allowFrom: ["*"] // Trust Sesame's permission model
|
|
11
|
+
*
|
|
12
|
+
* That's it. The plugin handles WebSocket connection, authentication,
|
|
13
|
+
* heartbeats, reconnection, and message routing automatically.
|
|
14
|
+
*/
|
|
15
|
+
// Runtime state
|
|
16
|
+
let pluginRuntime = null;
|
|
17
|
+
// Connection state per account
|
|
18
|
+
const connections = new Map();
|
|
19
|
+
function getLogger() {
|
|
20
|
+
return (pluginRuntime?.logging?.getChildLogger?.({ plugin: "sesame" }) ?? console);
|
|
21
|
+
}
|
|
22
|
+
const sesameChannelPlugin = {
|
|
23
|
+
id: "sesame",
|
|
24
|
+
meta: {
|
|
25
|
+
id: "sesame",
|
|
26
|
+
label: "Sesame",
|
|
27
|
+
selectionLabel: "Sesame (Agent-Native)",
|
|
28
|
+
docsPath: "/channels/sesame",
|
|
29
|
+
blurb: "Agent-native communications platform for human-agent collaboration.",
|
|
30
|
+
aliases: ["ses"],
|
|
31
|
+
},
|
|
32
|
+
capabilities: {
|
|
33
|
+
chatTypes: ["direct", "group"],
|
|
34
|
+
media: false,
|
|
35
|
+
reactions: true,
|
|
36
|
+
threads: true,
|
|
37
|
+
editing: true,
|
|
38
|
+
mentions: true,
|
|
39
|
+
},
|
|
40
|
+
reload: { configPrefixes: ["channels.sesame"] },
|
|
41
|
+
config: {
|
|
42
|
+
listAccountIds: (cfg) => {
|
|
43
|
+
const sesame = cfg.channels?.sesame;
|
|
44
|
+
if (sesame?.enabled && sesame?.apiKey)
|
|
45
|
+
return ["default"];
|
|
46
|
+
return [];
|
|
47
|
+
},
|
|
48
|
+
resolveAccount: (cfg, accountId) => {
|
|
49
|
+
const sesame = cfg.channels?.sesame;
|
|
50
|
+
if (!sesame?.enabled || !sesame?.apiKey)
|
|
51
|
+
return null;
|
|
52
|
+
return {
|
|
53
|
+
accountId: accountId ?? "default",
|
|
54
|
+
apiUrl: sesame.apiUrl ?? "https://api.sesame.space",
|
|
55
|
+
wsUrl: sesame.wsUrl ?? "wss://ws.sesame.space",
|
|
56
|
+
apiKey: sesame.apiKey ?? "",
|
|
57
|
+
agentId: sesame.agentId,
|
|
58
|
+
channels: sesame.channels ?? [],
|
|
59
|
+
allowFrom: sesame.allowFrom ?? [],
|
|
60
|
+
};
|
|
61
|
+
},
|
|
62
|
+
defaultAccountId: () => "default",
|
|
63
|
+
},
|
|
64
|
+
status: {
|
|
65
|
+
defaultRuntime: {
|
|
66
|
+
accountId: "default",
|
|
67
|
+
running: false,
|
|
68
|
+
lastStartAt: null,
|
|
69
|
+
lastStopAt: null,
|
|
70
|
+
lastError: null,
|
|
71
|
+
},
|
|
72
|
+
buildChannelSummary: ({ snapshot }) => ({
|
|
73
|
+
configured: snapshot.configured ?? false,
|
|
74
|
+
running: snapshot.running ?? false,
|
|
75
|
+
lastStartAt: snapshot.lastStartAt ?? null,
|
|
76
|
+
lastStopAt: snapshot.lastStopAt ?? null,
|
|
77
|
+
lastError: snapshot.lastError ?? null,
|
|
78
|
+
}),
|
|
79
|
+
probeAccount: async ({ account, timeoutMs }) => {
|
|
80
|
+
try {
|
|
81
|
+
const response = await fetch(`${account.apiUrl}/api/v1/agents/me/manifest`, {
|
|
82
|
+
headers: { Authorization: `Bearer ${account.apiKey}` },
|
|
83
|
+
signal: AbortSignal.timeout(timeoutMs ?? 5000),
|
|
84
|
+
});
|
|
85
|
+
if (response.ok) {
|
|
86
|
+
const data = (await response.json());
|
|
87
|
+
return { ok: true, agent: data.data?.agent ?? data.agent };
|
|
88
|
+
}
|
|
89
|
+
return { ok: false, error: `API returned ${response.status}` };
|
|
90
|
+
}
|
|
91
|
+
catch (err) {
|
|
92
|
+
return { ok: false, error: String(err) };
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
gateway: {
|
|
97
|
+
startAccount: async (ctx) => {
|
|
98
|
+
const account = ctx.account;
|
|
99
|
+
const log = ctx.log ?? getLogger();
|
|
100
|
+
if (!account?.apiKey) {
|
|
101
|
+
log.warn?.("[sesame] No API key configured");
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
log.info?.(`[sesame] [${account.accountId}] starting provider`);
|
|
105
|
+
// Return a long-lived promise that resolves only when aborted
|
|
106
|
+
return new Promise((resolve) => {
|
|
107
|
+
ctx.abortSignal?.addEventListener("abort", () => {
|
|
108
|
+
disconnect(account.accountId);
|
|
109
|
+
resolve();
|
|
110
|
+
});
|
|
111
|
+
connect(account, ctx);
|
|
112
|
+
});
|
|
113
|
+
},
|
|
114
|
+
stopAccount: async (ctx) => {
|
|
115
|
+
const account = ctx.account;
|
|
116
|
+
const log = ctx.log ?? getLogger();
|
|
117
|
+
log.info?.(`[sesame] [${account.accountId}] stopping provider`);
|
|
118
|
+
disconnect(account.accountId);
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
outbound: {
|
|
122
|
+
deliveryMode: "direct",
|
|
123
|
+
textChunkLimit: 4000,
|
|
124
|
+
resolveTarget: (params) => {
|
|
125
|
+
const raw = params.to;
|
|
126
|
+
if (!raw)
|
|
127
|
+
return { ok: false, error: new Error("No target specified") };
|
|
128
|
+
const normalized = raw.startsWith("sesame:") ? raw : `sesame:${raw}`;
|
|
129
|
+
return { ok: true, to: normalized };
|
|
130
|
+
},
|
|
131
|
+
sendText: async (ctx) => {
|
|
132
|
+
const { text, to, cfg, accountId } = ctx;
|
|
133
|
+
const account = sesameChannelPlugin.config.resolveAccount(cfg, accountId);
|
|
134
|
+
if (!account?.apiKey)
|
|
135
|
+
throw new Error("Sesame not configured");
|
|
136
|
+
const channelId = to?.startsWith("sesame:") ? to.slice(7) : to;
|
|
137
|
+
const response = await fetch(`${account.apiUrl}/api/v1/channels/${channelId}/messages`, {
|
|
138
|
+
method: "POST",
|
|
139
|
+
headers: {
|
|
140
|
+
"Content-Type": "application/json",
|
|
141
|
+
Authorization: `Bearer ${account.apiKey}`,
|
|
142
|
+
},
|
|
143
|
+
body: JSON.stringify({ content: text, kind: "text", intent: "chat" }),
|
|
144
|
+
});
|
|
145
|
+
if (!response.ok) {
|
|
146
|
+
const error = (await response.json().catch(() => ({})));
|
|
147
|
+
throw new Error(error.error ?? response.statusText);
|
|
148
|
+
}
|
|
149
|
+
const result = (await response.json().catch(() => ({})));
|
|
150
|
+
return {
|
|
151
|
+
channel: "sesame",
|
|
152
|
+
messageId: result.data?.id ?? result.id ?? "unknown",
|
|
153
|
+
chatId: channelId,
|
|
154
|
+
};
|
|
155
|
+
},
|
|
156
|
+
sendMedia: async (ctx) => {
|
|
157
|
+
// Sesame doesn't support media attachments yet — fall back to text
|
|
158
|
+
const text = ctx.caption ?? ctx.text ?? "[media attachment]";
|
|
159
|
+
return sesameChannelPlugin.outbound.sendText({ ...ctx, text });
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
messaging: {
|
|
163
|
+
normalizeTarget: (raw) => {
|
|
164
|
+
if (raw.startsWith("sesame:"))
|
|
165
|
+
return raw;
|
|
166
|
+
return `sesame:${raw}`;
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
directory: {
|
|
170
|
+
listGroups: async (params) => {
|
|
171
|
+
const sesame = params.cfg?.channels?.sesame;
|
|
172
|
+
if (!sesame?.enabled)
|
|
173
|
+
return [];
|
|
174
|
+
return (sesame.channels ?? []).map((id) => ({
|
|
175
|
+
id: `sesame:${id}`,
|
|
176
|
+
kind: "group",
|
|
177
|
+
name: id,
|
|
178
|
+
handle: id,
|
|
179
|
+
}));
|
|
180
|
+
},
|
|
181
|
+
listPeers: async (params) => {
|
|
182
|
+
const sesame = params.cfg?.channels?.sesame;
|
|
183
|
+
if (!sesame?.enabled)
|
|
184
|
+
return [];
|
|
185
|
+
return (sesame.allowFrom ?? [])
|
|
186
|
+
.filter((id) => id !== "*")
|
|
187
|
+
.map((id) => ({
|
|
188
|
+
id: `sesame:${id}`,
|
|
189
|
+
kind: "user",
|
|
190
|
+
name: id,
|
|
191
|
+
handle: id,
|
|
192
|
+
}));
|
|
193
|
+
},
|
|
194
|
+
},
|
|
195
|
+
};
|
|
196
|
+
// ── WebSocket connection ──
|
|
197
|
+
async function connect(account, ctx) {
|
|
198
|
+
const log = ctx.log ?? getLogger();
|
|
199
|
+
const WebSocket = (await import("ws")).default;
|
|
200
|
+
disconnect(account.accountId);
|
|
201
|
+
const state = {
|
|
202
|
+
ws: null,
|
|
203
|
+
heartbeatTimer: null,
|
|
204
|
+
reconnectTimer: null,
|
|
205
|
+
authenticated: false,
|
|
206
|
+
agentId: account.agentId ?? null,
|
|
207
|
+
stopping: false,
|
|
208
|
+
ctx,
|
|
209
|
+
channelMap: new Map(),
|
|
210
|
+
};
|
|
211
|
+
connections.set(account.accountId, state);
|
|
212
|
+
try {
|
|
213
|
+
// Fetch manifest: resolve agent ID + cache channel metadata
|
|
214
|
+
try {
|
|
215
|
+
const res = await fetch(`${account.apiUrl}/api/v1/agents/me/manifest`, { headers: { Authorization: `Bearer ${account.apiKey}` } });
|
|
216
|
+
if (res.ok) {
|
|
217
|
+
const manifest = (await res.json());
|
|
218
|
+
const mData = manifest.data ?? manifest;
|
|
219
|
+
if (!state.agentId) {
|
|
220
|
+
state.agentId = mData.agent?.id;
|
|
221
|
+
log.info?.(`[sesame] Agent ID resolved: ${state.agentId}`);
|
|
222
|
+
}
|
|
223
|
+
// Cache channel metadata for ChatType resolution
|
|
224
|
+
for (const ch of mData.channels ?? []) {
|
|
225
|
+
state.channelMap.set(ch.id, { kind: ch.kind, name: ch.name });
|
|
226
|
+
}
|
|
227
|
+
log.info?.(`[sesame] Loaded ${state.channelMap.size} channels from manifest`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
catch (err) {
|
|
231
|
+
log.warn?.(`[sesame] Could not fetch manifest: ${err}`);
|
|
232
|
+
}
|
|
233
|
+
state.ws = new WebSocket(`${account.wsUrl}/v1/connect`);
|
|
234
|
+
state.ws.on("open", () => {
|
|
235
|
+
log.info?.("[sesame] WebSocket connected, authenticating...");
|
|
236
|
+
state.ws?.send(JSON.stringify({ type: "auth", apiKey: account.apiKey }));
|
|
237
|
+
});
|
|
238
|
+
state.ws.on("message", (data) => {
|
|
239
|
+
try {
|
|
240
|
+
const event = JSON.parse(data.toString());
|
|
241
|
+
if (event.type !== "pong" &&
|
|
242
|
+
event.type !== "typing" &&
|
|
243
|
+
event.type !== "delivery.ack" &&
|
|
244
|
+
event.type !== "presence" &&
|
|
245
|
+
event.type !== "read_receipt") {
|
|
246
|
+
log.debug?.(`[sesame] Event: ${event.type}`);
|
|
247
|
+
}
|
|
248
|
+
handleEvent(event, account, state, ctx);
|
|
249
|
+
}
|
|
250
|
+
catch (e) {
|
|
251
|
+
log.error?.(`[sesame] WS parse error: ${e}`);
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
state.ws.on("close", (code) => {
|
|
255
|
+
state.authenticated = false;
|
|
256
|
+
clearInterval(state.heartbeatTimer);
|
|
257
|
+
if (state.stopping)
|
|
258
|
+
return;
|
|
259
|
+
log.info?.(`[sesame] WebSocket disconnected (code=${code}), reconnecting in 5s...`);
|
|
260
|
+
state.reconnectTimer = setTimeout(() => connect(account, ctx), 5000);
|
|
261
|
+
});
|
|
262
|
+
state.ws.on("error", (err) => {
|
|
263
|
+
log.error?.("[sesame] WebSocket error", err);
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
catch (err) {
|
|
267
|
+
log.error?.("[sesame] Connection failed", err);
|
|
268
|
+
state.reconnectTimer = setTimeout(() => connect(account, ctx), 5000);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
function disconnect(accountId) {
|
|
272
|
+
const state = connections.get(accountId);
|
|
273
|
+
if (!state)
|
|
274
|
+
return;
|
|
275
|
+
state.stopping = true;
|
|
276
|
+
clearInterval(state.heartbeatTimer);
|
|
277
|
+
clearTimeout(state.reconnectTimer);
|
|
278
|
+
try {
|
|
279
|
+
state.ws?.close();
|
|
280
|
+
}
|
|
281
|
+
catch { }
|
|
282
|
+
connections.delete(accountId);
|
|
283
|
+
}
|
|
284
|
+
function handleEvent(event, account, state, ctx) {
|
|
285
|
+
const log = ctx.log ?? getLogger();
|
|
286
|
+
switch (event.type) {
|
|
287
|
+
case "authenticated":
|
|
288
|
+
state.authenticated = true;
|
|
289
|
+
state.heartbeatTimer = setInterval(() => {
|
|
290
|
+
if (state.ws?.readyState === 1) {
|
|
291
|
+
state.ws.send(JSON.stringify({ type: "ping" }));
|
|
292
|
+
}
|
|
293
|
+
}, event.heartbeatIntervalMs ?? 30000);
|
|
294
|
+
log.info?.("[sesame] Authenticated successfully");
|
|
295
|
+
state.ws?.send(JSON.stringify({ type: "replay", cursors: {} }));
|
|
296
|
+
break;
|
|
297
|
+
case "message":
|
|
298
|
+
handleMessage(event.message ?? event.data, account, state, ctx);
|
|
299
|
+
break;
|
|
300
|
+
case "membership": {
|
|
301
|
+
// Track new channel joins so ChatType resolves correctly
|
|
302
|
+
const mData = event.data ?? event.membership ?? event;
|
|
303
|
+
const mChannelId = mData.channelId;
|
|
304
|
+
const mChannel = mData.channel;
|
|
305
|
+
if (mData.principalId === state.agentId &&
|
|
306
|
+
mData.action === "joined" &&
|
|
307
|
+
mChannelId) {
|
|
308
|
+
if (!state.channelMap.has(mChannelId)) {
|
|
309
|
+
state.channelMap.set(mChannelId, {
|
|
310
|
+
kind: mChannel?.kind ?? "group",
|
|
311
|
+
name: mChannel?.name,
|
|
312
|
+
});
|
|
313
|
+
log.info?.(`[sesame] Joined channel ${mChannel?.name ?? mChannelId}`);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
break;
|
|
317
|
+
}
|
|
318
|
+
case "pong":
|
|
319
|
+
break;
|
|
320
|
+
case "error":
|
|
321
|
+
log.error?.("[sesame] Server error:", event.message);
|
|
322
|
+
break;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
async function handleMessage(message, account, state, ctx) {
|
|
326
|
+
const log = ctx.log ?? getLogger();
|
|
327
|
+
const core = pluginRuntime;
|
|
328
|
+
if (!core)
|
|
329
|
+
return;
|
|
330
|
+
const bodyText = message.content ?? message.plaintext ?? message.body ?? message.text ?? "";
|
|
331
|
+
const channelId = message.channelId;
|
|
332
|
+
const messageId = message.id;
|
|
333
|
+
log.info?.(`[sesame] Message from ${message.senderId} in ${channelId}: "${bodyText.slice(0, 100)}"`);
|
|
334
|
+
// Skip own messages
|
|
335
|
+
if (message.senderId === state.agentId)
|
|
336
|
+
return;
|
|
337
|
+
// Channel filter (empty = all channels)
|
|
338
|
+
if (account.channels.length > 0 && !account.channels.includes(channelId))
|
|
339
|
+
return;
|
|
340
|
+
// Sender allowlist — wildcard "*" means allow all (Sesame manages permissions)
|
|
341
|
+
const hasWildcard = account.allowFrom.includes("*");
|
|
342
|
+
if (account.allowFrom.length > 0 &&
|
|
343
|
+
!hasWildcard &&
|
|
344
|
+
!account.allowFrom.includes(message.senderId))
|
|
345
|
+
return;
|
|
346
|
+
// Resolve sender info from message metadata
|
|
347
|
+
const meta = message.metadata ?? {};
|
|
348
|
+
const senderName = meta.senderDisplayName ??
|
|
349
|
+
meta.senderHandle ??
|
|
350
|
+
message.senderDisplayName ??
|
|
351
|
+
message.sender?.displayName ??
|
|
352
|
+
message.sender?.handle ??
|
|
353
|
+
message.senderId;
|
|
354
|
+
const senderHandle = meta.senderHandle ?? message.sender?.handle ?? message.senderId;
|
|
355
|
+
// Resolve channel kind (dm vs group) from cached manifest data
|
|
356
|
+
let channelInfo = state.channelMap.get(channelId);
|
|
357
|
+
if (!channelInfo) {
|
|
358
|
+
// Channel not in cache — fetch from API
|
|
359
|
+
try {
|
|
360
|
+
const chRes = await fetch(`${account.apiUrl}/api/v1/channels/${channelId}`, { headers: { Authorization: `Bearer ${account.apiKey}` } });
|
|
361
|
+
if (chRes.ok) {
|
|
362
|
+
const chData = (await chRes.json());
|
|
363
|
+
const ch = chData.data ?? chData;
|
|
364
|
+
channelInfo = { kind: ch.kind ?? "group", name: ch.name };
|
|
365
|
+
state.channelMap.set(channelId, channelInfo);
|
|
366
|
+
log.info?.(`[sesame] Fetched channel info: ${ch.name ?? channelId} (${ch.kind})`);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
catch (err) {
|
|
370
|
+
log.warn?.(`[sesame] Could not fetch channel ${channelId}: ${err}`);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
const channelKind = channelInfo?.kind ?? "group";
|
|
374
|
+
const channelName = channelInfo?.name;
|
|
375
|
+
const isGroup = channelKind !== "dm";
|
|
376
|
+
const chatType = isGroup ? "group" : "direct";
|
|
377
|
+
log.info?.(`[sesame] Dispatching to OpenClaw... (chatType=${chatType}, channel=${channelName ?? channelId})`);
|
|
378
|
+
// Typing indicator
|
|
379
|
+
const sendTyping = () => {
|
|
380
|
+
const conn = connections.get(account.accountId);
|
|
381
|
+
if (conn?.ws?.readyState === 1 && conn.authenticated) {
|
|
382
|
+
conn.ws.send(JSON.stringify({ type: "typing", channelId }));
|
|
383
|
+
}
|
|
384
|
+
};
|
|
385
|
+
let typingStopped = false;
|
|
386
|
+
sendTyping();
|
|
387
|
+
const typingInterval = setInterval(() => {
|
|
388
|
+
if (!typingStopped)
|
|
389
|
+
sendTyping();
|
|
390
|
+
}, 2500);
|
|
391
|
+
try {
|
|
392
|
+
const cfg = core.config.loadConfig();
|
|
393
|
+
// Resolve agent route
|
|
394
|
+
const route = core.channel.routing.resolveAgentRoute({
|
|
395
|
+
cfg,
|
|
396
|
+
channel: "sesame",
|
|
397
|
+
accountId: account.accountId,
|
|
398
|
+
peer: {
|
|
399
|
+
kind: isGroup ? "group" : "direct",
|
|
400
|
+
id: isGroup ? channelId : message.senderId,
|
|
401
|
+
},
|
|
402
|
+
});
|
|
403
|
+
// Build Sesame-specific session key per channel
|
|
404
|
+
const channelIdShort = channelId.slice(0, 7);
|
|
405
|
+
const sesameSessionKey = `agent:main:sesame:${chatType}:${channelIdShort}`;
|
|
406
|
+
log.info?.(`[sesame] Route: sessionKey=${sesameSessionKey} matchedBy=${route.matchedBy}`);
|
|
407
|
+
// Build conversation label
|
|
408
|
+
const conversationLabel = isGroup
|
|
409
|
+
? channelName
|
|
410
|
+
? `#${channelName}`
|
|
411
|
+
: `sesame-group:${channelIdShort}`
|
|
412
|
+
: senderName;
|
|
413
|
+
const inboundCtx = core.channel.reply.finalizeInboundContext({
|
|
414
|
+
Body: bodyText,
|
|
415
|
+
BodyForAgent: bodyText,
|
|
416
|
+
RawBody: bodyText,
|
|
417
|
+
CommandBody: bodyText,
|
|
418
|
+
From: `sesame:${channelId}`,
|
|
419
|
+
To: `sesame:${channelId}`,
|
|
420
|
+
SessionKey: sesameSessionKey,
|
|
421
|
+
AccountId: account.accountId,
|
|
422
|
+
ChatType: chatType,
|
|
423
|
+
IsGroup: isGroup,
|
|
424
|
+
ConversationLabel: conversationLabel,
|
|
425
|
+
SenderName: senderName,
|
|
426
|
+
SenderId: message.senderId,
|
|
427
|
+
SenderUsername: senderHandle,
|
|
428
|
+
Provider: "sesame",
|
|
429
|
+
Surface: "sesame",
|
|
430
|
+
MessageSid: messageId,
|
|
431
|
+
Timestamp: message.createdAt
|
|
432
|
+
? new Date(message.createdAt).getTime()
|
|
433
|
+
: Date.now(),
|
|
434
|
+
OriginatingChannel: "sesame",
|
|
435
|
+
CommandAuthorized: true,
|
|
436
|
+
CommandSource: "text",
|
|
437
|
+
});
|
|
438
|
+
// Record session
|
|
439
|
+
const storePath = core.channel.session.resolveStorePath(cfg.session?.store, { channel: "sesame", accountId: account.accountId });
|
|
440
|
+
await core.channel.session.recordInboundSession({
|
|
441
|
+
storePath,
|
|
442
|
+
sessionKey: inboundCtx.SessionKey ?? sesameSessionKey,
|
|
443
|
+
ctx: inboundCtx,
|
|
444
|
+
});
|
|
445
|
+
// Buffer reply chunks and send as a single message
|
|
446
|
+
const replyBuffer = [];
|
|
447
|
+
const flushBuffer = async () => {
|
|
448
|
+
const fullReply = replyBuffer.join("\n\n").trim();
|
|
449
|
+
if (!fullReply)
|
|
450
|
+
return;
|
|
451
|
+
log.info?.(`[sesame] Flushing buffered reply (${fullReply.length} chars): "${fullReply.slice(0, 100)}"`);
|
|
452
|
+
const res = await fetch(`${account.apiUrl}/api/v1/channels/${channelId}/messages`, {
|
|
453
|
+
method: "POST",
|
|
454
|
+
headers: {
|
|
455
|
+
"Content-Type": "application/json",
|
|
456
|
+
Authorization: `Bearer ${account.apiKey}`,
|
|
457
|
+
},
|
|
458
|
+
body: JSON.stringify({
|
|
459
|
+
content: fullReply,
|
|
460
|
+
kind: "text",
|
|
461
|
+
intent: "chat",
|
|
462
|
+
}),
|
|
463
|
+
});
|
|
464
|
+
if (!res.ok) {
|
|
465
|
+
const err = await res.text().catch(() => "");
|
|
466
|
+
log.error?.(`[sesame] Send failed (${res.status}): ${err.slice(0, 200)}`);
|
|
467
|
+
}
|
|
468
|
+
else {
|
|
469
|
+
log.info?.(`[sesame] Sent buffered reply to ${channelId} (${res.status})`);
|
|
470
|
+
}
|
|
471
|
+
};
|
|
472
|
+
const { dispatcher, replyOptions, markDispatchIdle } = core.channel.reply.createReplyDispatcherWithTyping({
|
|
473
|
+
humanDelay: core.channel.reply.resolveHumanDelayConfig(cfg, route.agentId),
|
|
474
|
+
onReplyStart: () => sendTyping(),
|
|
475
|
+
onIdle: () => clearInterval(typingInterval),
|
|
476
|
+
deliver: async (payload) => {
|
|
477
|
+
const replyText = payload.text ?? payload.body ?? payload.content ?? "";
|
|
478
|
+
if (replyText)
|
|
479
|
+
replyBuffer.push(replyText);
|
|
480
|
+
},
|
|
481
|
+
onError: (err) => {
|
|
482
|
+
log.error?.(`[sesame] Reply failed: ${String(err)}`);
|
|
483
|
+
},
|
|
484
|
+
});
|
|
485
|
+
await core.channel.reply.dispatchReplyFromConfig({
|
|
486
|
+
ctx: inboundCtx,
|
|
487
|
+
cfg,
|
|
488
|
+
dispatcher,
|
|
489
|
+
replyOptions,
|
|
490
|
+
});
|
|
491
|
+
typingStopped = true;
|
|
492
|
+
clearInterval(typingInterval);
|
|
493
|
+
markDispatchIdle();
|
|
494
|
+
await flushBuffer();
|
|
495
|
+
log.info?.("[sesame] Dispatch complete");
|
|
496
|
+
}
|
|
497
|
+
catch (err) {
|
|
498
|
+
log.error?.(`[sesame] Dispatch error: ${String(err)}`);
|
|
499
|
+
}
|
|
500
|
+
finally {
|
|
501
|
+
typingStopped = true;
|
|
502
|
+
clearInterval(typingInterval);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
// ── Plugin export ──
|
|
506
|
+
const plugin = {
|
|
507
|
+
id: "sesame",
|
|
508
|
+
name: "Sesame",
|
|
509
|
+
description: "Connect your OpenClaw agent to Sesame — the agent-native messaging platform",
|
|
510
|
+
configSchema: {
|
|
511
|
+
type: "object",
|
|
512
|
+
additionalProperties: false,
|
|
513
|
+
properties: {},
|
|
514
|
+
},
|
|
515
|
+
register(api) {
|
|
516
|
+
pluginRuntime = api.runtime;
|
|
517
|
+
api.registerChannel({ plugin: sesameChannelPlugin });
|
|
518
|
+
},
|
|
519
|
+
};
|
|
520
|
+
export default plugin;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "sesame",
|
|
3
|
+
"name": "Sesame",
|
|
4
|
+
"version": "0.2.1",
|
|
5
|
+
"description": "Connect your OpenClaw agent to Sesame — the agent-native messaging platform",
|
|
6
|
+
"channels": ["sesame"],
|
|
7
|
+
"channel": {
|
|
8
|
+
"id": "sesame",
|
|
9
|
+
"label": "Sesame",
|
|
10
|
+
"selectionLabel": "Sesame (Agent-Native)",
|
|
11
|
+
"docsPath": "/channels/sesame",
|
|
12
|
+
"blurb": "Agent-native communications platform for human-agent collaboration.",
|
|
13
|
+
"order": 80,
|
|
14
|
+
"aliases": ["ses"]
|
|
15
|
+
},
|
|
16
|
+
"configSchema": {
|
|
17
|
+
"type": "object",
|
|
18
|
+
"additionalProperties": false,
|
|
19
|
+
"properties": {
|
|
20
|
+
"enabled": { "type": "boolean" },
|
|
21
|
+
"apiKey": { "type": "string" },
|
|
22
|
+
"apiUrl": { "type": "string" },
|
|
23
|
+
"wsUrl": { "type": "string" },
|
|
24
|
+
"agentId": { "type": "string" },
|
|
25
|
+
"channels": { "type": "array", "items": { "type": "string" } },
|
|
26
|
+
"allowFrom": { "type": "array", "items": { "type": "string" } }
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"uiHints": {
|
|
30
|
+
"apiKey": { "label": "API Key", "sensitive": true, "placeholder": "sk_..." },
|
|
31
|
+
"apiUrl": { "label": "API URL", "placeholder": "https://api.sesame.space" },
|
|
32
|
+
"wsUrl": { "label": "WebSocket URL", "placeholder": "wss://ws.sesame.space" },
|
|
33
|
+
"channels": { "label": "Channel IDs to listen on (empty = all)" },
|
|
34
|
+
"allowFrom": { "label": "Allowed sender IDs (empty = all)" }
|
|
35
|
+
}
|
|
36
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@sesamespace/sesame",
|
|
3
|
+
"version": "0.2.1",
|
|
4
|
+
"description": "Sesame channel plugin for OpenClaw — connect your AI agent to the Sesame messaging platform",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "https://github.com/baileydavis2026/sesame.git",
|
|
11
|
+
"directory": "packages/openclaw-plugin"
|
|
12
|
+
},
|
|
13
|
+
"homepage": "https://sesame.space/docs",
|
|
14
|
+
"keywords": [
|
|
15
|
+
"sesame",
|
|
16
|
+
"openclaw",
|
|
17
|
+
"agent",
|
|
18
|
+
"messaging",
|
|
19
|
+
"channel",
|
|
20
|
+
"plugin",
|
|
21
|
+
"ai-agent",
|
|
22
|
+
"multi-agent"
|
|
23
|
+
],
|
|
24
|
+
"files": [
|
|
25
|
+
"dist",
|
|
26
|
+
"openclaw.plugin.json",
|
|
27
|
+
"README.md"
|
|
28
|
+
],
|
|
29
|
+
"openclaw": {
|
|
30
|
+
"extensions": [
|
|
31
|
+
"./dist/index.js"
|
|
32
|
+
],
|
|
33
|
+
"channel": {
|
|
34
|
+
"id": "sesame",
|
|
35
|
+
"label": "Sesame",
|
|
36
|
+
"selectionLabel": "Sesame (Agent-Native)",
|
|
37
|
+
"docsPath": "/channels/sesame",
|
|
38
|
+
"docsLabel": "sesame",
|
|
39
|
+
"blurb": "Agent-native communications platform for human-agent collaboration.",
|
|
40
|
+
"order": 80,
|
|
41
|
+
"aliases": [
|
|
42
|
+
"ses"
|
|
43
|
+
]
|
|
44
|
+
},
|
|
45
|
+
"install": {
|
|
46
|
+
"npmSpec": "@sesamespace/sesame",
|
|
47
|
+
"defaultChoice": "npm"
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
"dependencies": {
|
|
51
|
+
"ws": "^8.18.0"
|
|
52
|
+
},
|
|
53
|
+
"devDependencies": {
|
|
54
|
+
"typescript": "^5.9.3"
|
|
55
|
+
}
|
|
56
|
+
}
|