openclaw-channel-basicops 0.1.0
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/DESIGN.md +374 -0
- package/LICENSE +21 -0
- package/README.md +103 -0
- package/dist/basicops-mcp.js +108 -0
- package/dist/index.js +272 -0
- package/openclaw.plugin.json +11 -0
- package/package.json +29 -0
package/DESIGN.md
ADDED
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
# BasicOps ↔ OpenClaw Messaging Channel — Design / Spec (Draft)
|
|
2
|
+
|
|
3
|
+
Status: draft (pre-coding)
|
|
4
|
+
|
|
5
|
+
This document specifies the architecture and implementation plan for a custom **OpenClaw channel plugin** that connects **BasicOps Messaging (all message containers via MCP/API)** to OpenClaw as a first-class messaging surface.
|
|
6
|
+
|
|
7
|
+
Key requirement (Justin): **The agent should be able to receive and respond when @mentioned on ANY BasicOps message type.**
|
|
8
|
+
|
|
9
|
+
## 1) Goals
|
|
10
|
+
|
|
11
|
+
- Add **BasicOps as an OpenClaw messaging channel** (like Telegram/WhatsApp).
|
|
12
|
+
- Support **outbound** messages: OpenClaw → BasicOps (post a message into the chosen BasicOps thread).
|
|
13
|
+
- Support **inbound** messages: BasicOps → OpenClaw (webhook events become inbound messages in the correct OpenClaw session).
|
|
14
|
+
- Provide a clear **identity + conversation mapping** strategy.
|
|
15
|
+
- Keep secrets safe (API tokens, webhook secrets), and validate webhook authenticity.
|
|
16
|
+
|
|
17
|
+
## 2) Non-goals (initially)
|
|
18
|
+
|
|
19
|
+
- Full parity with rich chat apps (typing indicators, read receipts, edits/deletes).
|
|
20
|
+
- Perfect threading semantics across all BasicOps entities.
|
|
21
|
+
- Complex file upload and media transformations (phase later).
|
|
22
|
+
|
|
23
|
+
## 3) Key product decision: “What is a conversation?”
|
|
24
|
+
|
|
25
|
+
### 3.1 Reality check: BasicOps now has real Messaging (via MCP/API)
|
|
26
|
+
|
|
27
|
+
BasicOps now supports multiple **message containers** (not just notes/comments). We will treat each container as a first-class conversation surface and normalize them into OpenClaw sessions.
|
|
28
|
+
|
|
29
|
+
From the BasicOps MCP server, the relevant messaging tools include:
|
|
30
|
+
|
|
31
|
+
- Project messages: `list_messages_in_project`, `create_message_in_project`
|
|
32
|
+
- Task messages: `list_messages_in_task`, `create_message_in_task`
|
|
33
|
+
- Channels: `list_messages_in_channel`, `create_message_in_channel`
|
|
34
|
+
- Chats (1:1 or generic chats): `list_messages_in_chat`, `create_message_in_chat`
|
|
35
|
+
- Groupchats: `list_messages_in_groupchat`, `create_message_in_groupchat`
|
|
36
|
+
|
|
37
|
+
Supporting tools (needed for richer behaviors / future phases):
|
|
38
|
+
- Message CRUD: `get_message`, `update_message`, `delete_message`
|
|
39
|
+
- Replies: `create_reply_in_message`, `list_replies_in_message`, `update_reply`, `delete_reply`
|
|
40
|
+
- Reactions: `toggle_reaction_in_message`, `list_reactions_in_message`, `toggle_reaction_in_reply`, `list_reactions_in_reply`
|
|
41
|
+
|
|
42
|
+
### 3.2 Normalized conversation key
|
|
43
|
+
|
|
44
|
+
Define a stable normalized key:
|
|
45
|
+
|
|
46
|
+
- `project:<projectId>`
|
|
47
|
+
- `task:<taskId>`
|
|
48
|
+
- `channel:<channelId>`
|
|
49
|
+
- `chat:<chatId>`
|
|
50
|
+
- `groupchat:<groupchatId>`
|
|
51
|
+
|
|
52
|
+
This becomes the “target” in OpenClaw and the lookup key for session mapping.
|
|
53
|
+
|
|
54
|
+
### 3.3 Mention-driven routing (hard requirement)
|
|
55
|
+
|
|
56
|
+
Mention semantics:
|
|
57
|
+
- A mention is `@<username>` in BasicOps.
|
|
58
|
+
|
|
59
|
+
Policy:
|
|
60
|
+
- The connector should **only invoke/route to the agent when the agent is mentioned** (configurable).
|
|
61
|
+
- The connector should still be able to fetch recent context in the thread (e.g., last N messages) before responding.
|
|
62
|
+
|
|
63
|
+
## 4) OpenClaw integration surface
|
|
64
|
+
|
|
65
|
+
### 4.1 Plugin type
|
|
66
|
+
|
|
67
|
+
- Implement as an OpenClaw **channel plugin** registered via:
|
|
68
|
+
- `api.registerChannel({ plugin })`
|
|
69
|
+
|
|
70
|
+
Reference: OpenClaw docs `docs/tools/plugin.md` → “Register a messaging channel”.
|
|
71
|
+
|
|
72
|
+
### 4.2 Config location + shape
|
|
73
|
+
|
|
74
|
+
OpenClaw expects channel config under `channels.<id>` (not `plugins.entries`).
|
|
75
|
+
|
|
76
|
+
Proposed config:
|
|
77
|
+
|
|
78
|
+
```json5
|
|
79
|
+
{
|
|
80
|
+
channels: {
|
|
81
|
+
basicops: {
|
|
82
|
+
accounts: {
|
|
83
|
+
default: {
|
|
84
|
+
enabled: true,
|
|
85
|
+
|
|
86
|
+
// REST (optional) + MCP (preferred)
|
|
87
|
+
apiBaseUrl: "https://api.basicops.com",
|
|
88
|
+
apiToken: "...",
|
|
89
|
+
|
|
90
|
+
// MCP settings (supplemental: context fetch, message send, recovery)
|
|
91
|
+
mcp: {
|
|
92
|
+
transport: "sse",
|
|
93
|
+
baseUrl: "https://app.basicops.com/mcp/sse",
|
|
94
|
+
// auth handled by OpenClaw secrets/persona integration
|
|
95
|
+
persona: "justin"
|
|
96
|
+
},
|
|
97
|
+
|
|
98
|
+
// Webhooks (required for Telegram-like responsiveness)
|
|
99
|
+
webhooks: {
|
|
100
|
+
enabled: true,
|
|
101
|
+
publicUrl: "https://<gateway-public>/channels/basicops/webhook/<pathToken>",
|
|
102
|
+
secret: "...", // optional if BasicOps does not sign; still useful for our own token
|
|
103
|
+
// BasicOps event types (observed via MCP list_webhooks)
|
|
104
|
+
eventTypes: [
|
|
105
|
+
"basicops.task.message.created",
|
|
106
|
+
"basicops.project.message.created",
|
|
107
|
+
"basicops.chat.message.created",
|
|
108
|
+
"basicops.groupchat.message.created",
|
|
109
|
+
"basicops.channel.message.created",
|
|
110
|
+
"basicops.task.reply.created",
|
|
111
|
+
"basicops.project.reply.created",
|
|
112
|
+
"basicops.chat.reply.created",
|
|
113
|
+
"basicops.groupchat.reply.created",
|
|
114
|
+
"basicops.channel.reply.created"
|
|
115
|
+
]
|
|
116
|
+
},
|
|
117
|
+
|
|
118
|
+
// Mention policy
|
|
119
|
+
agentUsername: "Gus", // the BasicOps username to match after '@'
|
|
120
|
+
respondOnlyWhenMentioned: true,
|
|
121
|
+
mentionRegexOverride: null, // optional
|
|
122
|
+
|
|
123
|
+
// What containers to listen to
|
|
124
|
+
listen: {
|
|
125
|
+
project: true,
|
|
126
|
+
task: true,
|
|
127
|
+
channel: true,
|
|
128
|
+
chat: true,
|
|
129
|
+
groupchat: true
|
|
130
|
+
},
|
|
131
|
+
|
|
132
|
+
// Context fetching
|
|
133
|
+
context: {
|
|
134
|
+
fetchLastNMessages: 20
|
|
135
|
+
},
|
|
136
|
+
|
|
137
|
+
// optional: mapping store
|
|
138
|
+
mapping: {
|
|
139
|
+
provider: "sqlite" | "json",
|
|
140
|
+
path: "..."
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Notes:
|
|
150
|
+
- `accounts.<id>` pattern matches OpenClaw’s multi-account model.
|
|
151
|
+
- `conversationMode` is required for correctness unless we infer from targets.
|
|
152
|
+
|
|
153
|
+
### 4.3 Channel metadata
|
|
154
|
+
|
|
155
|
+
```ts
|
|
156
|
+
meta: {
|
|
157
|
+
id: "basicops",
|
|
158
|
+
label: "BasicOps",
|
|
159
|
+
selectionLabel: "BasicOps Messaging",
|
|
160
|
+
docsPath: "/channels/basicops",
|
|
161
|
+
blurb: "BasicOps messaging connector",
|
|
162
|
+
aliases: ["bo", "basicops-messaging"],
|
|
163
|
+
}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
## 5) High-level architecture
|
|
167
|
+
|
|
168
|
+
**Inbound requirement:** This connector MUST be **push-based** (webhooks) for near-real-time responsiveness (Telegram-like). Polling is not an acceptable primary mechanism; it is only a last-resort fallback for specific edge cases (e.g., missed events, replay/backfill).
|
|
169
|
+
|
|
170
|
+
### 5.1 Components
|
|
171
|
+
|
|
172
|
+
1) **OpenClaw Channel Plugin (this repo)**
|
|
173
|
+
- Outbound adapter(s): `sendText` (and later replies/reactions/attachments)
|
|
174
|
+
- Inbound adapter: **MCP event ingest** (preferred) and/or webhook receiver
|
|
175
|
+
- Mention router: detect `@agentUsername` and decide whether to forward to agent
|
|
176
|
+
- Routing/mapping: BasicOps conversation ↔ OpenClaw session
|
|
177
|
+
|
|
178
|
+
2) **BasicOps MCP server (preferred for messaging)**
|
|
179
|
+
- Tooling to list/create messages in:
|
|
180
|
+
- projects, tasks, channels, chats, groupchats
|
|
181
|
+
- Tooling to read/update/delete messages + replies + reactions
|
|
182
|
+
|
|
183
|
+
3) **BasicOps REST API (optional / supplemental)**
|
|
184
|
+
- Used when MCP lacks an operation, or for identity enrichment if needed.
|
|
185
|
+
|
|
186
|
+
4) **Mapping Store**
|
|
187
|
+
- Persist mapping between:
|
|
188
|
+
- `basicopsConversationKey` ↔ `openclawSessionKey`
|
|
189
|
+
- `basicopsUserId` ↔ sender metadata
|
|
190
|
+
- optional: dedupe keys for inbound events
|
|
191
|
+
|
|
192
|
+
### 5.2 Data flow
|
|
193
|
+
|
|
194
|
+
**Outbound (OpenClaw → BasicOps)**
|
|
195
|
+
1. Agent produces `message.send(channel="basicops", target=..., message=...)`.
|
|
196
|
+
2. OpenClaw calls channel plugin `outbound.sendText({ account, target, text })`.
|
|
197
|
+
3. Plugin resolves `target` into a BasicOps destination:
|
|
198
|
+
- projectId/taskId or conversationId
|
|
199
|
+
4. Plugin POSTs to the appropriate BasicOps endpoint to create a note/comment/message.
|
|
200
|
+
5. Plugin returns `{ ok: true }` or `{ ok: false, error }`.
|
|
201
|
+
|
|
202
|
+
**Inbound (BasicOps → OpenClaw)**
|
|
203
|
+
|
|
204
|
+
Primary path (Webhooks; required):
|
|
205
|
+
1. BasicOps emits webhook payloads to the connector’s public URL.
|
|
206
|
+
2. **Payload shape (from existing Webhooks Shim MVP):** BasicOps may batch events:
|
|
207
|
+
- top-level `body.events[]`
|
|
208
|
+
- each event has `{ event: string, records: any[] }`
|
|
209
|
+
|
|
210
|
+
**Observed messaging event names (via MCP `list_webhooks`):**
|
|
211
|
+
- `basicops.task.message.created`
|
|
212
|
+
- `basicops.project.message.created`
|
|
213
|
+
- `basicops.chat.message.created`
|
|
214
|
+
- `basicops.groupchat.message.created`
|
|
215
|
+
- `basicops.channel.message.created`
|
|
216
|
+
- replies:
|
|
217
|
+
- `basicops.task.reply.created`
|
|
218
|
+
- `basicops.project.reply.created`
|
|
219
|
+
- `basicops.chat.reply.created`
|
|
220
|
+
- `basicops.groupchat.reply.created`
|
|
221
|
+
- `basicops.channel.reply.created`
|
|
222
|
+
|
|
223
|
+
(The older shim example also shows non-message events like `basicops:task:created`.)
|
|
224
|
+
3. Validate authenticity.
|
|
225
|
+
- If BasicOps provides signatures: validate HMAC + timestamp/replay.
|
|
226
|
+
- If BasicOps does **not** provide signatures (current shim does not verify any): use an unguessable URL path token + optional allowlist + strict JSON validation + dedupe.
|
|
227
|
+
4. Normalize each record into a common message model:
|
|
228
|
+
- `containerType` (project/task/channel/chat/groupchat)
|
|
229
|
+
- `containerId`
|
|
230
|
+
- `messageId`
|
|
231
|
+
- `sender` (userId + display)
|
|
232
|
+
- `text` (and later attachments)
|
|
233
|
+
5. Detect mention:
|
|
234
|
+
- if `respondOnlyWhenMentioned=true`, only forward messages containing `@<agentUsername>`.
|
|
235
|
+
6. Find/create the OpenClaw session key for `containerType:containerId`.
|
|
236
|
+
7. Forward into that OpenClaw session with sender metadata.
|
|
237
|
+
|
|
238
|
+
Fallback/backfill (allowed but not primary):
|
|
239
|
+
- MCP can be used to backfill context, resolve missing sender names, or recover from missed webhooks.
|
|
240
|
+
- Short-interval polling is only acceptable for recovery or diagnostics, not as the main inbound mechanism.
|
|
241
|
+
|
|
242
|
+
## 6) Routing + identity mapping
|
|
243
|
+
|
|
244
|
+
### 6.1 Conversation mapping
|
|
245
|
+
|
|
246
|
+
We need stable keys:
|
|
247
|
+
|
|
248
|
+
- `basicopsConversationKey` (string):
|
|
249
|
+
- `project:<projectId>` OR `task:<taskId>`
|
|
250
|
+
- `openclawSessionKey`:
|
|
251
|
+
- Prefer deterministic mapping like: `basicops:<accountId>:<basicopsConversationKey>`
|
|
252
|
+
- Or use OpenClaw session creation + store the mapping.
|
|
253
|
+
|
|
254
|
+
**Recommendation:** deterministic session keys when OpenClaw allows it; otherwise persist mapping.
|
|
255
|
+
|
|
256
|
+
### 6.2 User identity
|
|
257
|
+
|
|
258
|
+
Map BasicOps users into OpenClaw sender metadata:
|
|
259
|
+
|
|
260
|
+
- `sender.provider = "basicops"`
|
|
261
|
+
- `sender.id = <basicopsUserId>`
|
|
262
|
+
- `sender.label/name = <displayName>`
|
|
263
|
+
|
|
264
|
+
If webhook payload does not include display name, fetch `/v1/user/<id>` and cache.
|
|
265
|
+
|
|
266
|
+
## 7) Webhook security
|
|
267
|
+
|
|
268
|
+
Target: Telegram-like responsiveness with production-grade verification.
|
|
269
|
+
|
|
270
|
+
Reality (per existing Webhooks Shim MVP): current shim code does **not** verify signatures, implying BasicOps webhooks may not currently ship an HMAC signature header.
|
|
271
|
+
|
|
272
|
+
Therefore the spec supports two modes:
|
|
273
|
+
|
|
274
|
+
1) **Signed webhooks (ideal, if BasicOps provides it)**
|
|
275
|
+
- Require a `webhooks.secret`.
|
|
276
|
+
- Validate request signature (e.g., HMAC SHA256 of raw body) + timestamp/replay.
|
|
277
|
+
|
|
278
|
+
2) **Unsigned webhooks (acceptable fallback)**
|
|
279
|
+
- Use an unguessable path token, e.g. `/channels/basicops/webhook/<random>`.
|
|
280
|
+
- Optionally add IP allowlisting at the reverse proxy.
|
|
281
|
+
- Strict JSON schema validation.
|
|
282
|
+
- Deduplicate by event id / hash.
|
|
283
|
+
|
|
284
|
+
Reference MVP:
|
|
285
|
+
- Receiver: `basicops-ops/scripts/webhook-receiver.mjs`
|
|
286
|
+
- Example webhook creation: `basicops-ops/scripts/create-project-tag-webhook.mjs`
|
|
287
|
+
|
|
288
|
+
## 8) Reliability & retries
|
|
289
|
+
|
|
290
|
+
### 8.1 Outbound
|
|
291
|
+
- Retry on transient failures (429/5xx) with backoff.
|
|
292
|
+
- Log request ids / BasicOps response ids.
|
|
293
|
+
- Return actionable error strings (status + body snippet).
|
|
294
|
+
|
|
295
|
+
### 8.2 Inbound
|
|
296
|
+
- Acknowledge webhooks quickly (2xx) and process asynchronously if supported.
|
|
297
|
+
- Deduplicate events if BasicOps sends retries:
|
|
298
|
+
- Keep a small cache of event ids/hashes.
|
|
299
|
+
|
|
300
|
+
## 9) Observability
|
|
301
|
+
|
|
302
|
+
- Add `status` adapter (optional) to surface:
|
|
303
|
+
- token validity check
|
|
304
|
+
- webhook configured state
|
|
305
|
+
- last inbound event time
|
|
306
|
+
- Structured logs:
|
|
307
|
+
- `accountId`, `conversationKey`, `basicopsEventId`
|
|
308
|
+
|
|
309
|
+
## 10) OpenClaw “what changed since Feb 15?” (investigation)
|
|
310
|
+
|
|
311
|
+
Local OpenClaw version on this host:
|
|
312
|
+
- **OpenClaw 2026.3.13 (61d171a)**
|
|
313
|
+
|
|
314
|
+
Key plugin/channel API notes from current `docs/tools/plugin.md`:
|
|
315
|
+
- Channel plugins still register via `api.registerChannel({ plugin })`.
|
|
316
|
+
- Config remains under `channels.<id>`.
|
|
317
|
+
- **Newer/clarified capabilities** since earlier docs:
|
|
318
|
+
- `meta.preferOver`, `meta.detailLabel`, `meta.systemImage` (richer UI metadata)
|
|
319
|
+
- **Channel onboarding hooks** are documented and appear to be first-class now:
|
|
320
|
+
- `plugin.onboarding.configure`, `configureInteractive`, `configureWhenConfigured`
|
|
321
|
+
- These can be used to guide users through token + webhook setup.
|
|
322
|
+
|
|
323
|
+
No breaking change was observed in the minimal channel registration pattern; however, the onboarding hooks are worth using to reduce setup friction.
|
|
324
|
+
|
|
325
|
+
(If you want a stricter diff vs Feb 15, we can compare the OpenClaw npm package changelog or git tags between those dates—this host only shows the currently installed docs.)
|
|
326
|
+
|
|
327
|
+
## 11) Implementation plan (phased)
|
|
328
|
+
|
|
329
|
+
### Phase 0 — Confirm webhook event schema + mention semantics
|
|
330
|
+
- Confirm BasicOps webhook events for **message created** across all containers.
|
|
331
|
+
- Confirm the BasicOps username to treat as the agent mention target (e.g., `@Gus`).
|
|
332
|
+
- Collect:
|
|
333
|
+
- example webhook payloads for each message container (project/task/channel/chat/groupchat)
|
|
334
|
+
- signature scheme (headers, algorithm, timestamp/replay)
|
|
335
|
+
- how to fetch “recent context” in-thread via MCP (tool args + paging)
|
|
336
|
+
|
|
337
|
+
### Phase 1 — Outbound-only MVP
|
|
338
|
+
- Replace placeholder `/v1/messages/send` with real endpoint(s).
|
|
339
|
+
- Implement:
|
|
340
|
+
- input validation
|
|
341
|
+
- request building
|
|
342
|
+
- error handling
|
|
343
|
+
- Add unit tests for payload mapping.
|
|
344
|
+
|
|
345
|
+
### Phase 2 — Inbound webhook
|
|
346
|
+
- Add webhook handler entrypoint (OpenClaw gateway route / channel inbound adapter depending on SDK expectations).
|
|
347
|
+
- Implement signature verification.
|
|
348
|
+
- Implement mapping store (sqlite recommended).
|
|
349
|
+
- Route inbound events into sessions.
|
|
350
|
+
|
|
351
|
+
### Phase 3 — Quality
|
|
352
|
+
- Deduplication
|
|
353
|
+
- richer metadata (sender name, links back to BasicOps)
|
|
354
|
+
- health/status commands
|
|
355
|
+
|
|
356
|
+
### Phase 4 — Rich features
|
|
357
|
+
- attachments (download/thumbnail endpoints exist for tasks)
|
|
358
|
+
- reactions (if supported)
|
|
359
|
+
- edits/deletes
|
|
360
|
+
|
|
361
|
+
## 12) Open questions / required inputs
|
|
362
|
+
|
|
363
|
+
1) What is “messaging” in BasicOps for this connector?
|
|
364
|
+
- project notes, task comments, or separate messaging API?
|
|
365
|
+
2) What is the webhook payload schema + signature scheme?
|
|
366
|
+
3) Do we need multi-workspace support (multiple BasicOps orgs) per OpenClaw account?
|
|
367
|
+
4) What should `target` look like from OpenClaw?
|
|
368
|
+
- raw id (`123`), or typed (`project:123`)?
|
|
369
|
+
|
|
370
|
+
---
|
|
371
|
+
|
|
372
|
+
Appendix: Scaffold location
|
|
373
|
+
- `/home/gus/.openclaw/workspace/basicops-openclaw-channel/`
|
|
374
|
+
- `src/index.ts` registers channel + has outbound stub (currently placeholder endpoint).
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# openclaw-channel-basicops
|
|
2
|
+
|
|
3
|
+
BasicOps messaging channel connector for OpenClaw.
|
|
4
|
+
|
|
5
|
+
- **Inbound:** BasicOps Webhooks (push-based, Telegram-like responsiveness)
|
|
6
|
+
- **Outbound:** BasicOps MCP tools (create message in project/task/channel/chat/groupchat)
|
|
7
|
+
- **Agent triggering:** mention-gated (`@<agentUsername>`) by default
|
|
8
|
+
|
|
9
|
+
## Install (npm)
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm i openclaw-channel-basicops
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Enable in OpenClaw
|
|
16
|
+
|
|
17
|
+
### 1) Load the plugin
|
|
18
|
+
|
|
19
|
+
Add to your OpenClaw config:
|
|
20
|
+
|
|
21
|
+
```json5
|
|
22
|
+
{
|
|
23
|
+
plugins: {
|
|
24
|
+
load: {
|
|
25
|
+
paths: [
|
|
26
|
+
// if installed into the same environment as the gateway
|
|
27
|
+
"./node_modules/openclaw-channel-basicops/dist/index.js"
|
|
28
|
+
]
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### 2) Configure the channel
|
|
35
|
+
|
|
36
|
+
```json5
|
|
37
|
+
{
|
|
38
|
+
channels: {
|
|
39
|
+
basicops: {
|
|
40
|
+
accounts: {
|
|
41
|
+
default: {
|
|
42
|
+
enabled: true,
|
|
43
|
+
|
|
44
|
+
// BasicOps MCP (MVP): paste an OAuth access token
|
|
45
|
+
mcp: {
|
|
46
|
+
accessToken: "...",
|
|
47
|
+
baseUrl: "https://app.basicops.com/mcp/sse"
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
// Mention routing
|
|
51
|
+
agentUsername: "Gus",
|
|
52
|
+
respondOnlyWhenMentioned: true,
|
|
53
|
+
|
|
54
|
+
// Webhook auth (recommended): unguessable path token
|
|
55
|
+
webhooks: {
|
|
56
|
+
enabled: true,
|
|
57
|
+
pathToken: "<random-long-token>"
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Configure BasicOps Webhooks
|
|
67
|
+
|
|
68
|
+
Point BasicOps webhooks at:
|
|
69
|
+
|
|
70
|
+
- `https://<your-gateway-public>/channels/basicops/webhook/<pathToken>`
|
|
71
|
+
|
|
72
|
+
Subscribe to these event types (observed via MCP `list_webhooks`):
|
|
73
|
+
|
|
74
|
+
- `basicops.task.message.created`
|
|
75
|
+
- `basicops.project.message.created`
|
|
76
|
+
- `basicops.chat.message.created`
|
|
77
|
+
- `basicops.groupchat.message.created`
|
|
78
|
+
- `basicops.channel.message.created`
|
|
79
|
+
- `basicops.task.reply.created`
|
|
80
|
+
- `basicops.project.reply.created`
|
|
81
|
+
- `basicops.chat.reply.created`
|
|
82
|
+
- `basicops.groupchat.reply.created`
|
|
83
|
+
- `basicops.channel.reply.created`
|
|
84
|
+
|
|
85
|
+
## Sending messages from OpenClaw
|
|
86
|
+
|
|
87
|
+
Use typed targets:
|
|
88
|
+
|
|
89
|
+
- `task:<id>`
|
|
90
|
+
- `project:<id>`
|
|
91
|
+
- `channel:<id>`
|
|
92
|
+
- `chat:<id>`
|
|
93
|
+
- `groupchat:<id>`
|
|
94
|
+
|
|
95
|
+
Example (tool call semantics vary by your agent tooling):
|
|
96
|
+
|
|
97
|
+
- send to a task thread: `target="task:2103656"`
|
|
98
|
+
|
|
99
|
+
## Notes / Roadmap
|
|
100
|
+
|
|
101
|
+
- OAuth onboarding (no more pasted tokens)
|
|
102
|
+
- Signature verification if/when BasicOps provides webhook signatures
|
|
103
|
+
- Rich features: attachments, reactions, edits/deletes
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// Minimal BasicOps MCP-over-SSE client (ported from skills/basicops-mcp/scripts/basicops_mcp.py)
|
|
2
|
+
//
|
|
3
|
+
// Notes:
|
|
4
|
+
// - This intentionally keeps things simple: each tool call performs an SSE handshake
|
|
5
|
+
// to obtain a message endpoint, then POSTs a single JSON-RPC request.
|
|
6
|
+
// - For production we may want connection reuse, retries, and token refresh.
|
|
7
|
+
const DEFAULT_SSE_URL = "https://app.basicops.com/mcp/sse";
|
|
8
|
+
const DEFAULT_PROTOCOL_VERSION = "2024-11-05";
|
|
9
|
+
async function handshake(opts) {
|
|
10
|
+
const initPayload = {
|
|
11
|
+
jsonrpc: "2.0",
|
|
12
|
+
id: 1,
|
|
13
|
+
method: "initialize",
|
|
14
|
+
params: {
|
|
15
|
+
protocolVersion: opts.protocolVersion,
|
|
16
|
+
capabilities: {},
|
|
17
|
+
clientInfo: { name: "OpenClaw", version: "0.1" },
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
const res = await fetch(opts.baseUrl, {
|
|
21
|
+
method: "POST",
|
|
22
|
+
headers: {
|
|
23
|
+
Authorization: `Bearer ${opts.accessToken}`,
|
|
24
|
+
Accept: "text/event-stream",
|
|
25
|
+
"Content-Type": "application/json",
|
|
26
|
+
"Cache-Control": "no-cache",
|
|
27
|
+
},
|
|
28
|
+
body: JSON.stringify(initPayload),
|
|
29
|
+
});
|
|
30
|
+
if (!res.ok || !res.body) {
|
|
31
|
+
const body = await res.text().catch(() => "");
|
|
32
|
+
throw new Error(`MCP handshake failed (${res.status}): ${body}`);
|
|
33
|
+
}
|
|
34
|
+
// Read SSE lines until we see the first `data: ...` line (endpoint)
|
|
35
|
+
const reader = res.body.getReader();
|
|
36
|
+
const decoder = new TextDecoder();
|
|
37
|
+
let buf = "";
|
|
38
|
+
const deadline = Date.now() + 20_000;
|
|
39
|
+
while (Date.now() < deadline) {
|
|
40
|
+
const { value, done } = await reader.read();
|
|
41
|
+
if (done)
|
|
42
|
+
break;
|
|
43
|
+
buf += decoder.decode(value, { stream: true });
|
|
44
|
+
// SSE lines end with \n. We'll parse line-by-line.
|
|
45
|
+
let idx;
|
|
46
|
+
while ((idx = buf.indexOf("\n")) !== -1) {
|
|
47
|
+
const line = buf.slice(0, idx).trimEnd();
|
|
48
|
+
buf = buf.slice(idx + 1);
|
|
49
|
+
if (line.startsWith("data: ")) {
|
|
50
|
+
let endpoint = line.slice("data: ".length).trim();
|
|
51
|
+
if (!endpoint)
|
|
52
|
+
continue;
|
|
53
|
+
if (endpoint.startsWith("/"))
|
|
54
|
+
endpoint = `https://app.basicops.com${endpoint}`;
|
|
55
|
+
return endpoint;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
throw new Error("MCP handshake failed: no endpoint returned");
|
|
60
|
+
}
|
|
61
|
+
export async function callMcpTool(opts) {
|
|
62
|
+
const baseUrl = opts.mcp.baseUrl || DEFAULT_SSE_URL;
|
|
63
|
+
const accessToken = opts.mcp.accessToken;
|
|
64
|
+
const protocolVersion = opts.mcp.protocolVersion || DEFAULT_PROTOCOL_VERSION;
|
|
65
|
+
if (!accessToken) {
|
|
66
|
+
throw new Error("Missing BasicOps MCP access token (channels.basicops.accounts.<id>.mcp.accessToken)");
|
|
67
|
+
}
|
|
68
|
+
const endpoint = await handshake({ baseUrl, accessToken, protocolVersion });
|
|
69
|
+
const payload = {
|
|
70
|
+
jsonrpc: "2.0",
|
|
71
|
+
id: opts.requestId ?? 2,
|
|
72
|
+
method: "tools/call",
|
|
73
|
+
params: {
|
|
74
|
+
name: opts.tool,
|
|
75
|
+
arguments: opts.args ?? {},
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
const res = await fetch(endpoint, {
|
|
79
|
+
method: "POST",
|
|
80
|
+
headers: {
|
|
81
|
+
Authorization: `Bearer ${accessToken}`,
|
|
82
|
+
Accept: "application/json",
|
|
83
|
+
"Content-Type": "application/json",
|
|
84
|
+
},
|
|
85
|
+
body: JSON.stringify(payload),
|
|
86
|
+
});
|
|
87
|
+
const raw = await res.text().catch(() => "");
|
|
88
|
+
if (!res.ok) {
|
|
89
|
+
throw new Error(`MCP tool call failed (${res.status}): ${raw}`);
|
|
90
|
+
}
|
|
91
|
+
let json;
|
|
92
|
+
try {
|
|
93
|
+
json = raw ? JSON.parse(raw) : {};
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
throw new Error(`MCP tool call returned non-JSON: ${raw.slice(0, 400)}`);
|
|
97
|
+
}
|
|
98
|
+
// BasicOps MCP may return { result: { isError: true, content: [...] } }
|
|
99
|
+
const isError = Boolean(json?.result?.isError);
|
|
100
|
+
if (isError) {
|
|
101
|
+
const content = json?.result?.content;
|
|
102
|
+
const text = Array.isArray(content)
|
|
103
|
+
? content.map((c) => (c && typeof c === "object" ? c.text : "")).join(" ")
|
|
104
|
+
: "";
|
|
105
|
+
throw new Error(`MCP tool error: ${text || "(unknown)"}`);
|
|
106
|
+
}
|
|
107
|
+
return json;
|
|
108
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import { callMcpTool } from "./basicops-mcp.js";
|
|
2
|
+
function listAccountIds(cfg) {
|
|
3
|
+
return Object.keys(cfg?.channels?.basicops?.accounts ?? {});
|
|
4
|
+
}
|
|
5
|
+
function resolveAccount(cfg, accountId) {
|
|
6
|
+
const resolved = cfg?.channels?.basicops?.accounts?.[accountId ?? "default"];
|
|
7
|
+
const account = resolved ?? { accountId };
|
|
8
|
+
// Minimal configured heuristic
|
|
9
|
+
account.configured = Boolean(account?.enabled && (account?.mcp?.accessToken || account?.apiToken));
|
|
10
|
+
return account;
|
|
11
|
+
}
|
|
12
|
+
function normalizeTarget(raw) {
|
|
13
|
+
const s = String(raw || "").trim();
|
|
14
|
+
const m = s.match(/^(project|task|channel|chat|groupchat)\s*:\s*(.+)$/i);
|
|
15
|
+
if (m)
|
|
16
|
+
return { containerType: m[1].toLowerCase(), containerId: String(m[2]).trim() };
|
|
17
|
+
// If not typed, assume task id (common) — caller should prefer typed targets.
|
|
18
|
+
return { containerType: "task", containerId: s };
|
|
19
|
+
}
|
|
20
|
+
function buildMentionRegex(agentUsername) {
|
|
21
|
+
// Match @Username with a loose boundary so @Gus, @Gus, etc.
|
|
22
|
+
// We intentionally do not enforce word boundaries too strictly because some UIs
|
|
23
|
+
// include punctuation.
|
|
24
|
+
const escaped = agentUsername.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
25
|
+
return new RegExp(`(^|\\s)@${escaped}(\\b|$)`, "i");
|
|
26
|
+
}
|
|
27
|
+
function extractText(record) {
|
|
28
|
+
// Best-effort: BasicOps record shapes may vary per container.
|
|
29
|
+
return (record?.message ??
|
|
30
|
+
record?.text ??
|
|
31
|
+
record?.body ??
|
|
32
|
+
record?.content ??
|
|
33
|
+
record?.Message ??
|
|
34
|
+
record?.Text ??
|
|
35
|
+
"").toString();
|
|
36
|
+
}
|
|
37
|
+
function extractSender(record) {
|
|
38
|
+
const senderId = (record?.UserId ??
|
|
39
|
+
record?.userId ??
|
|
40
|
+
record?.SenderId ??
|
|
41
|
+
record?.senderId ??
|
|
42
|
+
record?.FromUserId ??
|
|
43
|
+
record?.fromUserId ??
|
|
44
|
+
"unknown").toString();
|
|
45
|
+
const senderLabel = (record?.UserName ??
|
|
46
|
+
record?.username ??
|
|
47
|
+
record?.SenderName ??
|
|
48
|
+
record?.senderName ??
|
|
49
|
+
record?.FromUserName ??
|
|
50
|
+
record?.fromUserName ??
|
|
51
|
+
undefined);
|
|
52
|
+
return { senderId, senderLabel };
|
|
53
|
+
}
|
|
54
|
+
function extractContainerFromEvent(eventName, record) {
|
|
55
|
+
const e = String(eventName || "").toLowerCase();
|
|
56
|
+
if (e.includes("task.")) {
|
|
57
|
+
const id = record?.TaskId ?? record?.taskId ?? record?.TaskID ?? record?.taskID;
|
|
58
|
+
return { containerType: "task", containerId: String(id ?? record?.Task ?? record?.task ?? "") };
|
|
59
|
+
}
|
|
60
|
+
if (e.includes("project.")) {
|
|
61
|
+
const id = record?.ProjectId ?? record?.projectId ?? record?.ProjectID ?? record?.projectID;
|
|
62
|
+
return { containerType: "project", containerId: String(id ?? record?.Project ?? record?.project ?? "") };
|
|
63
|
+
}
|
|
64
|
+
if (e.includes("groupchat.")) {
|
|
65
|
+
const id = record?.GroupchatId ?? record?.groupchatId ?? record?.GroupChatId ?? record?.groupChatId;
|
|
66
|
+
return { containerType: "groupchat", containerId: String(id ?? "") };
|
|
67
|
+
}
|
|
68
|
+
if (e.includes("channel.")) {
|
|
69
|
+
const id = record?.ChannelId ?? record?.channelId;
|
|
70
|
+
return { containerType: "channel", containerId: String(id ?? "") };
|
|
71
|
+
}
|
|
72
|
+
if (e.includes("chat.")) {
|
|
73
|
+
const id = record?.ChatId ?? record?.chatId;
|
|
74
|
+
return { containerType: "chat", containerId: String(id ?? "") };
|
|
75
|
+
}
|
|
76
|
+
// Fallback: try obvious keys
|
|
77
|
+
for (const k of ["taskId", "TaskId", "projectId", "ProjectId", "chatId", "ChatId", "groupchatId", "GroupchatId", "channelId", "ChannelId"]) {
|
|
78
|
+
if (record?.[k] != null) {
|
|
79
|
+
const v = String(record[k]);
|
|
80
|
+
const type = k.toLowerCase().includes("task")
|
|
81
|
+
? "task"
|
|
82
|
+
: k.toLowerCase().includes("project")
|
|
83
|
+
? "project"
|
|
84
|
+
: k.toLowerCase().includes("group")
|
|
85
|
+
? "groupchat"
|
|
86
|
+
: k.toLowerCase().includes("channel")
|
|
87
|
+
? "channel"
|
|
88
|
+
: "chat";
|
|
89
|
+
return { containerType: type, containerId: v };
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return { containerType: "task", containerId: "" };
|
|
93
|
+
}
|
|
94
|
+
async function sendViaMcp(opts) {
|
|
95
|
+
const { account, target, text } = opts;
|
|
96
|
+
const mcp = account?.mcp;
|
|
97
|
+
if (!mcp?.accessToken) {
|
|
98
|
+
return { ok: false, error: "Missing channels.basicops.accounts.<id>.mcp.accessToken" };
|
|
99
|
+
}
|
|
100
|
+
const { containerType, containerId } = normalizeTarget(target);
|
|
101
|
+
const toolByType = {
|
|
102
|
+
project: "create_message_in_project",
|
|
103
|
+
task: "create_message_in_task",
|
|
104
|
+
channel: "create_message_in_channel",
|
|
105
|
+
chat: "create_message_in_chat",
|
|
106
|
+
groupchat: "create_message_in_groupchat",
|
|
107
|
+
};
|
|
108
|
+
const tool = toolByType[containerType];
|
|
109
|
+
if (!tool) {
|
|
110
|
+
return { ok: false, error: `Unsupported BasicOps target container: ${containerType}` };
|
|
111
|
+
}
|
|
112
|
+
// NOTE: BasicOps tools use different arg names; we standardize to id + message.
|
|
113
|
+
// We learned at least one schema expectation: create_message_in_chat expects `message` not `text`.
|
|
114
|
+
const idKeyByType = {
|
|
115
|
+
project: "projectId",
|
|
116
|
+
task: "taskId",
|
|
117
|
+
channel: "channelId",
|
|
118
|
+
chat: "chatId",
|
|
119
|
+
groupchat: "groupchatId",
|
|
120
|
+
};
|
|
121
|
+
const args = {
|
|
122
|
+
[idKeyByType[containerType]]: containerId,
|
|
123
|
+
message: text,
|
|
124
|
+
};
|
|
125
|
+
await callMcpTool({ mcp, tool, args });
|
|
126
|
+
return { ok: true };
|
|
127
|
+
}
|
|
128
|
+
const basicopsChannel = {
|
|
129
|
+
id: "basicops",
|
|
130
|
+
meta: {
|
|
131
|
+
id: "basicops",
|
|
132
|
+
label: "BasicOps",
|
|
133
|
+
selectionLabel: "BasicOps Messaging",
|
|
134
|
+
docsPath: "/channels/basicops",
|
|
135
|
+
blurb: "BasicOps messaging connector",
|
|
136
|
+
aliases: ["bo", "basicops-messaging"],
|
|
137
|
+
},
|
|
138
|
+
capabilities: {
|
|
139
|
+
chatTypes: ["direct", "group"],
|
|
140
|
+
},
|
|
141
|
+
reload: { configPrefixes: ["channels.basicops"] },
|
|
142
|
+
config: {
|
|
143
|
+
listAccountIds,
|
|
144
|
+
resolveAccount,
|
|
145
|
+
// Helps status/doctor flows avoid materializing secrets
|
|
146
|
+
inspectAccount: (cfg, accountId) => {
|
|
147
|
+
const a = resolveAccount(cfg, accountId);
|
|
148
|
+
return {
|
|
149
|
+
accountId: a.accountId ?? accountId ?? "default",
|
|
150
|
+
enabled: Boolean(a.enabled),
|
|
151
|
+
configured: Boolean(a.configured),
|
|
152
|
+
hasMcpToken: Boolean(a?.mcp?.accessToken),
|
|
153
|
+
hasApiToken: Boolean(a?.apiToken),
|
|
154
|
+
};
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
messaging: {
|
|
158
|
+
normalizeTarget: (target) => {
|
|
159
|
+
const { containerType, containerId } = normalizeTarget(target);
|
|
160
|
+
return `${containerType}:${containerId}`;
|
|
161
|
+
},
|
|
162
|
+
targetResolver: {
|
|
163
|
+
looksLikeId: (input) => /^(project|task|channel|chat|groupchat)\s*:\s*\S+$/i.test(input.trim()),
|
|
164
|
+
hint: "project:<id> | task:<id> | channel:<id> | chat:<id> | groupchat:<id>",
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
outbound: {
|
|
168
|
+
deliveryMode: "direct",
|
|
169
|
+
sendText: async ({ account, target, to, text }) => {
|
|
170
|
+
const dest = target ?? to;
|
|
171
|
+
if (!dest)
|
|
172
|
+
return { ok: false, error: "Missing target/to" };
|
|
173
|
+
return sendViaMcp({ account, target: dest, text: text ?? "" });
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
};
|
|
177
|
+
export default function register(api) {
|
|
178
|
+
api.registerChannel({ plugin: basicopsChannel });
|
|
179
|
+
// Webhook route (plugin-managed auth)
|
|
180
|
+
// Recommended gateway path: /channels/basicops/webhook/<pathToken>
|
|
181
|
+
api.registerHttpRoute({
|
|
182
|
+
path: "/channels/basicops/webhook",
|
|
183
|
+
auth: "plugin",
|
|
184
|
+
match: "prefix",
|
|
185
|
+
handler: async (req, res) => {
|
|
186
|
+
try {
|
|
187
|
+
if (req.method !== "POST") {
|
|
188
|
+
res.statusCode = 200;
|
|
189
|
+
res.end("ok");
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
// Read raw body
|
|
193
|
+
const chunks = [];
|
|
194
|
+
await new Promise((resolve, reject) => {
|
|
195
|
+
req.on("data", (c) => chunks.push(c));
|
|
196
|
+
req.on("end", () => resolve());
|
|
197
|
+
req.on("error", (e) => reject(e));
|
|
198
|
+
});
|
|
199
|
+
const raw = Buffer.concat(chunks).toString("utf8");
|
|
200
|
+
const body = raw ? JSON.parse(raw) : {};
|
|
201
|
+
// Determine accountId; default only for now.
|
|
202
|
+
const cfg = api.runtime.config.loadConfig();
|
|
203
|
+
const accountId = "default";
|
|
204
|
+
const account = resolveAccount(cfg, accountId);
|
|
205
|
+
// Path token check (if configured)
|
|
206
|
+
const token = account?.webhooks?.pathToken;
|
|
207
|
+
if (token) {
|
|
208
|
+
const url = String(req.url || "");
|
|
209
|
+
// req.url includes the path from this route, e.g. /channels/basicops/webhook/<token>
|
|
210
|
+
const parts = url.split("?")[0].split("/").filter(Boolean);
|
|
211
|
+
const idx = parts.findIndex((p) => p === "webhook");
|
|
212
|
+
const got = idx >= 0 ? parts[idx + 1] : null;
|
|
213
|
+
if (got !== token) {
|
|
214
|
+
res.statusCode = 401;
|
|
215
|
+
res.end("unauthorized");
|
|
216
|
+
return true;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
const agentUsername = (account.agentUsername || "Gus").trim();
|
|
220
|
+
const mentionRe = buildMentionRegex(agentUsername);
|
|
221
|
+
const mentionOnly = account.respondOnlyWhenMentioned !== false;
|
|
222
|
+
const events = Array.isArray(body?.events) ? body.events : [];
|
|
223
|
+
// Ack quickly; OpenClaw pipeline work happens after parsing anyway.
|
|
224
|
+
res.statusCode = 200;
|
|
225
|
+
res.setHeader("content-type", "application/json");
|
|
226
|
+
res.end(JSON.stringify({ ok: true }));
|
|
227
|
+
// Process events after ACK
|
|
228
|
+
for (const ev of events) {
|
|
229
|
+
const eventName = String(ev?.event || ev?.name || "");
|
|
230
|
+
const records = Array.isArray(ev?.records) ? ev.records : [];
|
|
231
|
+
for (const record of records) {
|
|
232
|
+
const text = extractText(record);
|
|
233
|
+
if (!text)
|
|
234
|
+
continue;
|
|
235
|
+
if (mentionOnly && !mentionRe.test(text))
|
|
236
|
+
continue;
|
|
237
|
+
const { senderId, senderLabel } = extractSender(record);
|
|
238
|
+
const { containerType, containerId } = extractContainerFromEvent(eventName, record);
|
|
239
|
+
if (!containerId)
|
|
240
|
+
continue;
|
|
241
|
+
const chatId = `${containerType}:${containerId}`;
|
|
242
|
+
const chatType = containerType === "chat" ? "direct" : "group";
|
|
243
|
+
const runtime = api.runtime;
|
|
244
|
+
await runtime.channel.reply.handleInboundMessage?.({
|
|
245
|
+
channel: "basicops",
|
|
246
|
+
accountId,
|
|
247
|
+
senderId,
|
|
248
|
+
senderLabel,
|
|
249
|
+
chatType,
|
|
250
|
+
chatId,
|
|
251
|
+
text,
|
|
252
|
+
// Provide a reply callback that posts back into the same container.
|
|
253
|
+
reply: async (responseText) => {
|
|
254
|
+
await sendViaMcp({ account, target: chatId, text: responseText });
|
|
255
|
+
},
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
return true;
|
|
260
|
+
}
|
|
261
|
+
catch (err) {
|
|
262
|
+
api.logger?.error?.(`basicops webhook error: ${err?.message || String(err)}`);
|
|
263
|
+
try {
|
|
264
|
+
res.statusCode = 500;
|
|
265
|
+
res.end("error");
|
|
266
|
+
}
|
|
267
|
+
catch { }
|
|
268
|
+
return true;
|
|
269
|
+
}
|
|
270
|
+
},
|
|
271
|
+
});
|
|
272
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "basicops",
|
|
3
|
+
"name": "BasicOps",
|
|
4
|
+
"description": "BasicOps messaging channel connector (webhooks inbound + MCP outbound)",
|
|
5
|
+
"version": "0.1.0",
|
|
6
|
+
"entry": "dist/index.js",
|
|
7
|
+
"tags": ["channel", "basicops", "messaging"],
|
|
8
|
+
"docs": {
|
|
9
|
+
"channelDocsPath": "/channels/basicops"
|
|
10
|
+
}
|
|
11
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "openclaw-channel-basicops",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "BasicOps messaging channel connector for OpenClaw (webhooks inbound + MCP outbound)",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist/",
|
|
12
|
+
"openclaw.plugin.json",
|
|
13
|
+
"README.md",
|
|
14
|
+
"DESIGN.md",
|
|
15
|
+
"LICENSE"
|
|
16
|
+
],
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "tsc -p tsconfig.json",
|
|
19
|
+
"dev": "tsc -w -p tsconfig.json",
|
|
20
|
+
"prepack": "npm run build"
|
|
21
|
+
},
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"openclaw": ">=2026.3.0"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"typescript": "^5.9.2"
|
|
27
|
+
},
|
|
28
|
+
"license": "MIT"
|
|
29
|
+
}
|