@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 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)
@@ -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
+ }