@clawpod/openclaw-plugin 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/dist/connection.d.ts +58 -0
- package/dist/connection.js +319 -0
- package/dist/index.d.ts +170 -0
- package/dist/index.js +410 -0
- package/package.json +36 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import WebSocket from 'ws';
|
|
2
|
+
export interface ClawPodConfig {
|
|
3
|
+
serverUrl: string;
|
|
4
|
+
apiKey: string;
|
|
5
|
+
agentName: string;
|
|
6
|
+
workspaceExpose: boolean;
|
|
7
|
+
allowedMembers: string[];
|
|
8
|
+
accountId: string;
|
|
9
|
+
enabled: boolean;
|
|
10
|
+
}
|
|
11
|
+
export interface WsEvent {
|
|
12
|
+
type: string;
|
|
13
|
+
[key: string]: unknown;
|
|
14
|
+
}
|
|
15
|
+
export interface DmMessage {
|
|
16
|
+
id: string;
|
|
17
|
+
senderId: string;
|
|
18
|
+
senderType: string;
|
|
19
|
+
senderName?: string;
|
|
20
|
+
receiverId: string;
|
|
21
|
+
content: string;
|
|
22
|
+
}
|
|
23
|
+
export interface ChannelMessage {
|
|
24
|
+
id: string;
|
|
25
|
+
channelId: string;
|
|
26
|
+
senderId: string;
|
|
27
|
+
senderType: string;
|
|
28
|
+
senderName?: string;
|
|
29
|
+
content: string;
|
|
30
|
+
}
|
|
31
|
+
export interface ClawPodConnection {
|
|
32
|
+
ws: WebSocket | null;
|
|
33
|
+
agentId: string | null;
|
|
34
|
+
spaceId: string | null;
|
|
35
|
+
machineId: string | null;
|
|
36
|
+
config: ClawPodConfig;
|
|
37
|
+
onMessage: (event: WsEvent) => void;
|
|
38
|
+
onDm: (dm: DmMessage) => void;
|
|
39
|
+
onMention: (msg: ChannelMessage) => void;
|
|
40
|
+
}
|
|
41
|
+
export interface ClawPodConnectionApi extends ClawPodConnection {
|
|
42
|
+
connect: () => void;
|
|
43
|
+
disconnect: () => void;
|
|
44
|
+
updateStatus: (status: string, context?: string) => void;
|
|
45
|
+
sendDmReply: (receiverId: string, receiverType: string, content: string) => void;
|
|
46
|
+
sendChannelMessage: (channelId: string, content: string) => void;
|
|
47
|
+
sendProcessed: (messageId: string) => void;
|
|
48
|
+
}
|
|
49
|
+
export declare function createConnection(config: ClawPodConfig, handlers: {
|
|
50
|
+
onDm: (dm: DmMessage) => void;
|
|
51
|
+
onMention: (msg: ChannelMessage) => void;
|
|
52
|
+
onStatusChange?: (status: string) => void;
|
|
53
|
+
logger: {
|
|
54
|
+
info: (...args: unknown[]) => void;
|
|
55
|
+
error: (...args: unknown[]) => void;
|
|
56
|
+
};
|
|
57
|
+
workspace?: string;
|
|
58
|
+
}): ClawPodConnectionApi;
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.createConnection = createConnection;
|
|
7
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
8
|
+
const node_os_1 = __importDefault(require("node:os"));
|
|
9
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
10
|
+
const ws_1 = __importDefault(require("ws"));
|
|
11
|
+
function createConnection(config, handlers) {
|
|
12
|
+
const { logger, workspace } = handlers;
|
|
13
|
+
const conn = {
|
|
14
|
+
ws: null,
|
|
15
|
+
agentId: null,
|
|
16
|
+
spaceId: null,
|
|
17
|
+
machineId: null,
|
|
18
|
+
config,
|
|
19
|
+
onMessage: () => { },
|
|
20
|
+
onDm: handlers.onDm,
|
|
21
|
+
onMention: handlers.onMention,
|
|
22
|
+
};
|
|
23
|
+
let reconnectTimer = null;
|
|
24
|
+
let closed = false;
|
|
25
|
+
function connect() {
|
|
26
|
+
if (closed) {
|
|
27
|
+
logger.info(`[clawpod] connect() skipped: closed=true`);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
logger.info(`[clawpod] Connecting to ${config.serverUrl} (closed=${closed})`);
|
|
31
|
+
let ws;
|
|
32
|
+
try {
|
|
33
|
+
ws = new ws_1.default(config.serverUrl);
|
|
34
|
+
}
|
|
35
|
+
catch (e) {
|
|
36
|
+
logger.error(`[clawpod] WebSocket constructor error: ${e instanceof Error ? e.message : String(e)}`);
|
|
37
|
+
if (!closed)
|
|
38
|
+
reconnectTimer = setTimeout(connect, 5000);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
conn.ws = ws;
|
|
42
|
+
ws.on('open', () => {
|
|
43
|
+
logger.info(`[clawpod] WS open, sending auth (apiKey length=${config.apiKey?.length})`);
|
|
44
|
+
ws.send(JSON.stringify({ type: 'auth', token: config.apiKey, kind: 'machine' }));
|
|
45
|
+
});
|
|
46
|
+
ws.on('message', (raw) => {
|
|
47
|
+
try {
|
|
48
|
+
const event = JSON.parse(raw.toString());
|
|
49
|
+
logger.info(`[clawpod] WS recv: ${event.type}`);
|
|
50
|
+
handleEvent(event);
|
|
51
|
+
}
|
|
52
|
+
catch (e) {
|
|
53
|
+
logger.error(`[clawpod] WS message parse error: ${e instanceof Error ? e.message : String(e)}`);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
ws.on('close', (code, reason) => {
|
|
57
|
+
logger.info(`[clawpod] WS close: code=${code} reason=${reason?.toString()}`);
|
|
58
|
+
conn.agentId = null;
|
|
59
|
+
if (!closed) {
|
|
60
|
+
logger.info('[clawpod] Disconnected, reconnecting in 5s');
|
|
61
|
+
reconnectTimer = setTimeout(connect, 5000);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
ws.on('error', (err) => {
|
|
65
|
+
logger.error(`[clawpod] WS error: ${err.message}`);
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
function handleEvent(event) {
|
|
69
|
+
switch (event.type) {
|
|
70
|
+
case 'auth:ok':
|
|
71
|
+
conn.spaceId = event.spaceId;
|
|
72
|
+
conn.machineId = event.id;
|
|
73
|
+
logger.info(`[clawpod] Authenticated. Machine: ${event.id}`);
|
|
74
|
+
conn.ws?.send(JSON.stringify({
|
|
75
|
+
type: 'machine:register',
|
|
76
|
+
name: `${node_os_1.default.hostname()} (OpenClaw)`,
|
|
77
|
+
mode: 'openclaw',
|
|
78
|
+
workspacePath: workspace || process.cwd(),
|
|
79
|
+
allowedMembers: config.allowedMembers || [],
|
|
80
|
+
agents: [
|
|
81
|
+
{
|
|
82
|
+
name: config.agentName,
|
|
83
|
+
defaultPrompt: 'OpenClaw Agent',
|
|
84
|
+
status: 'online',
|
|
85
|
+
model: 'openclaw',
|
|
86
|
+
},
|
|
87
|
+
],
|
|
88
|
+
}));
|
|
89
|
+
break;
|
|
90
|
+
case 'machine:online': {
|
|
91
|
+
const eventAgents = event.agents;
|
|
92
|
+
if (eventAgents && !conn.agentId) {
|
|
93
|
+
const a = eventAgents.find((a) => a.name === config.agentName);
|
|
94
|
+
if (a) {
|
|
95
|
+
conn.agentId = a.id;
|
|
96
|
+
logger.info(`[clawpod] Agent registered: ${a.id} (${a.name})`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
case 'dm:new': {
|
|
102
|
+
const dm = event.message;
|
|
103
|
+
if (!conn.agentId ||
|
|
104
|
+
dm.senderType === 'agent' ||
|
|
105
|
+
dm.receiverId !== conn.agentId)
|
|
106
|
+
break;
|
|
107
|
+
logger.info(`[clawpod] DM from ${dm.senderName}: ${dm.content?.slice(0, 80)}`);
|
|
108
|
+
conn.onDm(dm);
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
case 'message:new': {
|
|
112
|
+
const msg = event.message;
|
|
113
|
+
if (!conn.agentId || msg.senderType === 'agent')
|
|
114
|
+
break;
|
|
115
|
+
const isMentioned = msg.content?.includes(`@${config.agentName}`);
|
|
116
|
+
const channelMeta = event.channelMeta;
|
|
117
|
+
const autoReply = channelMeta?.agentAutoReply === true;
|
|
118
|
+
if (isMentioned) {
|
|
119
|
+
logger.info(`[clawpod] @mentioned by ${msg.senderName} in channel`);
|
|
120
|
+
conn.onMention(msg);
|
|
121
|
+
}
|
|
122
|
+
else if (autoReply) {
|
|
123
|
+
logger.info(`[clawpod] autoReply channel msg from ${msg.senderName}: ${msg.content?.slice(0, 60)}`);
|
|
124
|
+
conn.onMention({ ...msg, autoReply: true });
|
|
125
|
+
}
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
case 'workspace:browse': {
|
|
129
|
+
if (!config.workspaceExpose || !workspace)
|
|
130
|
+
break;
|
|
131
|
+
handleWorkspaceBrowse(event, workspace, conn);
|
|
132
|
+
break;
|
|
133
|
+
}
|
|
134
|
+
case 'skills:list': {
|
|
135
|
+
handleSkillsList(event, workspace || null, conn);
|
|
136
|
+
break;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
function handleWorkspaceBrowse(event, wsPath, conn) {
|
|
141
|
+
const reqPath = event.path || '/';
|
|
142
|
+
const fullPath = node_path_1.default.join(wsPath, reqPath);
|
|
143
|
+
if (!fullPath.startsWith(wsPath)) {
|
|
144
|
+
conn.ws?.send(JSON.stringify({
|
|
145
|
+
type: 'workspace:files',
|
|
146
|
+
requestId: event.requestId,
|
|
147
|
+
path: reqPath,
|
|
148
|
+
files: [],
|
|
149
|
+
error: 'Access denied',
|
|
150
|
+
}));
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
try {
|
|
154
|
+
const stat = node_fs_1.default.statSync(fullPath);
|
|
155
|
+
if (stat.isDirectory()) {
|
|
156
|
+
const entries = node_fs_1.default
|
|
157
|
+
.readdirSync(fullPath, { withFileTypes: true })
|
|
158
|
+
.filter((e) => !e.name.startsWith('.') && e.name !== 'node_modules')
|
|
159
|
+
.map((e) => {
|
|
160
|
+
let size;
|
|
161
|
+
try {
|
|
162
|
+
if (!e.isDirectory())
|
|
163
|
+
size = node_fs_1.default.statSync(node_path_1.default.join(fullPath, e.name)).size;
|
|
164
|
+
}
|
|
165
|
+
catch { }
|
|
166
|
+
return {
|
|
167
|
+
name: e.name,
|
|
168
|
+
path: node_path_1.default.join(reqPath, e.name),
|
|
169
|
+
isDir: e.isDirectory(),
|
|
170
|
+
size,
|
|
171
|
+
};
|
|
172
|
+
})
|
|
173
|
+
.sort((a, b) => a.isDir === b.isDir
|
|
174
|
+
? a.name.localeCompare(b.name)
|
|
175
|
+
: a.isDir
|
|
176
|
+
? -1
|
|
177
|
+
: 1);
|
|
178
|
+
conn.ws?.send(JSON.stringify({
|
|
179
|
+
type: 'workspace:files',
|
|
180
|
+
requestId: event.requestId,
|
|
181
|
+
path: reqPath,
|
|
182
|
+
basePath: wsPath,
|
|
183
|
+
files: entries,
|
|
184
|
+
}));
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
const content = node_fs_1.default.readFileSync(fullPath, 'utf-8').slice(0, 50000);
|
|
188
|
+
conn.ws?.send(JSON.stringify({
|
|
189
|
+
type: 'workspace:file-content',
|
|
190
|
+
requestId: event.requestId,
|
|
191
|
+
path: reqPath,
|
|
192
|
+
basePath: wsPath,
|
|
193
|
+
content,
|
|
194
|
+
size: stat.size,
|
|
195
|
+
}));
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
catch (err) {
|
|
199
|
+
conn.ws?.send(JSON.stringify({
|
|
200
|
+
type: 'workspace:files',
|
|
201
|
+
requestId: event.requestId,
|
|
202
|
+
path: reqPath,
|
|
203
|
+
files: [],
|
|
204
|
+
error: err instanceof Error ? err.message : String(err),
|
|
205
|
+
}));
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
function handleSkillsList(event, wsPath, conn) {
|
|
209
|
+
const skills = [];
|
|
210
|
+
const home = node_os_1.default.homedir();
|
|
211
|
+
const skillsDirs = [];
|
|
212
|
+
if (wsPath) {
|
|
213
|
+
skillsDirs.push({ base: node_path_1.default.join(wsPath, 'skills'), source: 'workspace' });
|
|
214
|
+
}
|
|
215
|
+
skillsDirs.push({ base: node_path_1.default.join(home, '.cursor', 'skills'), source: 'bundled' }, { base: node_path_1.default.join(home, '.cursor', 'skills-cursor'), source: 'bundled' }, { base: node_path_1.default.join(home, '.codex', 'skills', '.system'), source: 'bundled' }, { base: node_path_1.default.join(home, '.openclaw', 'skills'), source: 'bundled' });
|
|
216
|
+
try {
|
|
217
|
+
const ocPath = require.resolve('openclaw/package.json');
|
|
218
|
+
skillsDirs.push({
|
|
219
|
+
base: node_path_1.default.join(node_path_1.default.dirname(ocPath), 'skills'),
|
|
220
|
+
source: 'bundled',
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
catch { }
|
|
224
|
+
const seen = new Set();
|
|
225
|
+
for (const { base, source } of skillsDirs) {
|
|
226
|
+
try {
|
|
227
|
+
if (!node_fs_1.default.existsSync(base))
|
|
228
|
+
continue;
|
|
229
|
+
for (const name of node_fs_1.default.readdirSync(base)) {
|
|
230
|
+
if (seen.has(name))
|
|
231
|
+
continue;
|
|
232
|
+
const skillDir = node_path_1.default.join(base, name);
|
|
233
|
+
if (!node_fs_1.default.statSync(skillDir).isDirectory())
|
|
234
|
+
continue;
|
|
235
|
+
seen.add(name);
|
|
236
|
+
const skill = {
|
|
237
|
+
name,
|
|
238
|
+
source,
|
|
239
|
+
path: skillDir,
|
|
240
|
+
description: '',
|
|
241
|
+
emoji: '',
|
|
242
|
+
files: [],
|
|
243
|
+
};
|
|
244
|
+
const skillMd = node_path_1.default.join(skillDir, 'SKILL.md');
|
|
245
|
+
if (node_fs_1.default.existsSync(skillMd)) {
|
|
246
|
+
const content = node_fs_1.default.readFileSync(skillMd, 'utf-8');
|
|
247
|
+
const descMatch = content.match(/description:\s*["']?(.+?)["']?\s*\n/);
|
|
248
|
+
if (descMatch)
|
|
249
|
+
skill.description = descMatch[1].slice(0, 120);
|
|
250
|
+
skill.skillMdContent = content;
|
|
251
|
+
}
|
|
252
|
+
skill.files = node_fs_1.default
|
|
253
|
+
.readdirSync(skillDir)
|
|
254
|
+
.filter((f) => !f.startsWith('.'));
|
|
255
|
+
skills.push(skill);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
catch { }
|
|
259
|
+
}
|
|
260
|
+
conn.ws?.send(JSON.stringify({
|
|
261
|
+
type: 'skills:list',
|
|
262
|
+
requestId: event.requestId,
|
|
263
|
+
skills,
|
|
264
|
+
}));
|
|
265
|
+
}
|
|
266
|
+
// Public API
|
|
267
|
+
return {
|
|
268
|
+
...conn,
|
|
269
|
+
connect() {
|
|
270
|
+
connect();
|
|
271
|
+
},
|
|
272
|
+
disconnect() {
|
|
273
|
+
closed = true;
|
|
274
|
+
if (reconnectTimer)
|
|
275
|
+
clearTimeout(reconnectTimer);
|
|
276
|
+
conn.ws?.close();
|
|
277
|
+
},
|
|
278
|
+
updateStatus(status, context) {
|
|
279
|
+
if (conn.ws?.readyState === ws_1.default.OPEN && conn.agentId) {
|
|
280
|
+
conn.ws.send(JSON.stringify({
|
|
281
|
+
type: 'agent:status',
|
|
282
|
+
agentId: conn.agentId,
|
|
283
|
+
status,
|
|
284
|
+
context: context || undefined,
|
|
285
|
+
}));
|
|
286
|
+
}
|
|
287
|
+
},
|
|
288
|
+
sendDmReply(receiverId, receiverType, content) {
|
|
289
|
+
if (conn.ws?.readyState === ws_1.default.OPEN && conn.agentId) {
|
|
290
|
+
conn.ws.send(JSON.stringify({
|
|
291
|
+
type: 'dm:send',
|
|
292
|
+
receiverId,
|
|
293
|
+
receiverType,
|
|
294
|
+
content,
|
|
295
|
+
agentId: conn.agentId,
|
|
296
|
+
}));
|
|
297
|
+
}
|
|
298
|
+
},
|
|
299
|
+
sendChannelMessage(channelId, content) {
|
|
300
|
+
if (conn.ws?.readyState === ws_1.default.OPEN && conn.agentId) {
|
|
301
|
+
conn.ws.send(JSON.stringify({
|
|
302
|
+
type: 'message:send',
|
|
303
|
+
channelId,
|
|
304
|
+
content,
|
|
305
|
+
agentId: conn.agentId,
|
|
306
|
+
}));
|
|
307
|
+
}
|
|
308
|
+
},
|
|
309
|
+
sendProcessed(messageId) {
|
|
310
|
+
if (conn.ws?.readyState === ws_1.default.OPEN && conn.agentId) {
|
|
311
|
+
conn.ws.send(JSON.stringify({
|
|
312
|
+
type: 'dm:processed',
|
|
313
|
+
messageId,
|
|
314
|
+
agentId: conn.agentId,
|
|
315
|
+
}));
|
|
316
|
+
}
|
|
317
|
+
},
|
|
318
|
+
};
|
|
319
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @clawpod/channel — ClawPod Channel Plugin for OpenClaw
|
|
3
|
+
*
|
|
4
|
+
* Registers ClawPod as a messaging channel. Messages from ClawPod are dispatched
|
|
5
|
+
* through OpenClaw's standard agent session pipeline. Replies flow back to ClawPod.
|
|
6
|
+
*/
|
|
7
|
+
interface OpenClawPluginApi {
|
|
8
|
+
runtime: OpenClawRuntime;
|
|
9
|
+
logger: {
|
|
10
|
+
info: (...args: unknown[]) => void;
|
|
11
|
+
error: (...args: unknown[]) => void;
|
|
12
|
+
};
|
|
13
|
+
registerChannel: (opts: {
|
|
14
|
+
plugin: unknown;
|
|
15
|
+
}) => void;
|
|
16
|
+
registerTool: (tool: {
|
|
17
|
+
name: string;
|
|
18
|
+
description: string;
|
|
19
|
+
parameters: Record<string, unknown>;
|
|
20
|
+
handler: (params: Record<string, string>) => Promise<Record<string, unknown>>;
|
|
21
|
+
}) => void;
|
|
22
|
+
}
|
|
23
|
+
interface OpenClawRuntime {
|
|
24
|
+
channel?: {
|
|
25
|
+
reply?: {
|
|
26
|
+
dispatchReplyFromConfig?: (opts: Record<string, unknown>) => Promise<void>;
|
|
27
|
+
withReplyDispatcher?: (opts: Record<string, unknown>) => Promise<void>;
|
|
28
|
+
finalizeInboundContext?: (ctx: Record<string, unknown>, cfg: Record<string, unknown>) => void;
|
|
29
|
+
};
|
|
30
|
+
routing?: {
|
|
31
|
+
resolveAgentRoute?: (opts: Record<string, unknown>) => {
|
|
32
|
+
sessionKey: string;
|
|
33
|
+
accountId: string;
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
[key: string]: unknown;
|
|
38
|
+
}
|
|
39
|
+
declare const clawpodChannel: {
|
|
40
|
+
id: string;
|
|
41
|
+
meta: {
|
|
42
|
+
id: string;
|
|
43
|
+
label: string;
|
|
44
|
+
selectionLabel: string;
|
|
45
|
+
docsPath: string;
|
|
46
|
+
docsLabel: string;
|
|
47
|
+
blurb: string;
|
|
48
|
+
aliases: string[];
|
|
49
|
+
order: number;
|
|
50
|
+
};
|
|
51
|
+
reload: {
|
|
52
|
+
configPrefixes: string[];
|
|
53
|
+
};
|
|
54
|
+
configSchema: {
|
|
55
|
+
schema: {
|
|
56
|
+
type: string;
|
|
57
|
+
additionalProperties: boolean;
|
|
58
|
+
properties: {
|
|
59
|
+
enabled: {
|
|
60
|
+
type: string;
|
|
61
|
+
};
|
|
62
|
+
serverUrl: {
|
|
63
|
+
type: string;
|
|
64
|
+
};
|
|
65
|
+
apiKey: {
|
|
66
|
+
type: string;
|
|
67
|
+
};
|
|
68
|
+
workspaceExpose: {
|
|
69
|
+
type: string;
|
|
70
|
+
};
|
|
71
|
+
agentName: {
|
|
72
|
+
type: string;
|
|
73
|
+
description: string;
|
|
74
|
+
};
|
|
75
|
+
allowedMembers: {
|
|
76
|
+
type: string;
|
|
77
|
+
items: {
|
|
78
|
+
type: string;
|
|
79
|
+
};
|
|
80
|
+
description: string;
|
|
81
|
+
};
|
|
82
|
+
};
|
|
83
|
+
};
|
|
84
|
+
uiHints: {
|
|
85
|
+
enabled: {
|
|
86
|
+
label: string;
|
|
87
|
+
};
|
|
88
|
+
serverUrl: {
|
|
89
|
+
label: string;
|
|
90
|
+
placeholder: string;
|
|
91
|
+
};
|
|
92
|
+
apiKey: {
|
|
93
|
+
label: string;
|
|
94
|
+
sensitive: boolean;
|
|
95
|
+
};
|
|
96
|
+
workspaceExpose: {
|
|
97
|
+
label: string;
|
|
98
|
+
};
|
|
99
|
+
agentName: {
|
|
100
|
+
label: string;
|
|
101
|
+
placeholder: string;
|
|
102
|
+
};
|
|
103
|
+
allowedMembers: {
|
|
104
|
+
label: string;
|
|
105
|
+
placeholder: string;
|
|
106
|
+
};
|
|
107
|
+
};
|
|
108
|
+
};
|
|
109
|
+
capabilities: {
|
|
110
|
+
chatTypes: readonly ["direct", "channel"];
|
|
111
|
+
polls: boolean;
|
|
112
|
+
threads: boolean;
|
|
113
|
+
media: boolean;
|
|
114
|
+
reactions: boolean;
|
|
115
|
+
edit: boolean;
|
|
116
|
+
reply: boolean;
|
|
117
|
+
};
|
|
118
|
+
config: {
|
|
119
|
+
listAccountIds: (cfg: Record<string, unknown>) => string[];
|
|
120
|
+
resolveAccount: (cfg: Record<string, unknown>, _accountId?: string) => {
|
|
121
|
+
accountId: string;
|
|
122
|
+
enabled: boolean;
|
|
123
|
+
configured: boolean;
|
|
124
|
+
serverUrl: string | undefined;
|
|
125
|
+
};
|
|
126
|
+
isConfigured: (account: Record<string, unknown>) => boolean;
|
|
127
|
+
describeAccount: (account: Record<string, unknown>) => {
|
|
128
|
+
accountId: string;
|
|
129
|
+
enabled: boolean;
|
|
130
|
+
configured: boolean;
|
|
131
|
+
serverUrl: string | undefined;
|
|
132
|
+
};
|
|
133
|
+
};
|
|
134
|
+
outbound: {
|
|
135
|
+
deliveryMode: "direct";
|
|
136
|
+
sendText: (params: {
|
|
137
|
+
text: string;
|
|
138
|
+
to: string;
|
|
139
|
+
}) => Promise<{
|
|
140
|
+
ok: boolean;
|
|
141
|
+
error: string;
|
|
142
|
+
} | {
|
|
143
|
+
ok: boolean;
|
|
144
|
+
error?: undefined;
|
|
145
|
+
}>;
|
|
146
|
+
};
|
|
147
|
+
gateway: {
|
|
148
|
+
startAccount: (ctx: {
|
|
149
|
+
cfg: Record<string, unknown>;
|
|
150
|
+
accountId: string;
|
|
151
|
+
runtime: Record<string, unknown>;
|
|
152
|
+
log: Record<string, (...args: unknown[]) => void>;
|
|
153
|
+
abortSignal?: AbortSignal;
|
|
154
|
+
}) => Promise<void>;
|
|
155
|
+
};
|
|
156
|
+
status: {
|
|
157
|
+
summary: () => {
|
|
158
|
+
ok: boolean;
|
|
159
|
+
label: string;
|
|
160
|
+
};
|
|
161
|
+
describeAccount: (_cfg: Record<string, unknown>, _accountId: string, runtime: Record<string, unknown> | undefined) => {
|
|
162
|
+
accountId: string;
|
|
163
|
+
running: {};
|
|
164
|
+
configured: boolean;
|
|
165
|
+
connected: boolean;
|
|
166
|
+
};
|
|
167
|
+
};
|
|
168
|
+
};
|
|
169
|
+
export default function register(api: OpenClawPluginApi): void;
|
|
170
|
+
export { clawpodChannel };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @clawpod/channel — ClawPod Channel Plugin for OpenClaw
|
|
4
|
+
*
|
|
5
|
+
* Registers ClawPod as a messaging channel. Messages from ClawPod are dispatched
|
|
6
|
+
* through OpenClaw's standard agent session pipeline. Replies flow back to ClawPod.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.clawpodChannel = void 0;
|
|
10
|
+
exports.default = register;
|
|
11
|
+
const connection_js_1 = require("./connection.js");
|
|
12
|
+
const connections = new Map();
|
|
13
|
+
let _pluginApi = null;
|
|
14
|
+
let pluginRuntime = null;
|
|
15
|
+
const clawpodChannel = {
|
|
16
|
+
id: 'clawpod',
|
|
17
|
+
meta: {
|
|
18
|
+
id: 'clawpod',
|
|
19
|
+
label: 'ClawPod',
|
|
20
|
+
selectionLabel: 'ClawPod (Human & Agent Workspace)',
|
|
21
|
+
docsPath: '/channels/clawpod',
|
|
22
|
+
docsLabel: 'clawpod',
|
|
23
|
+
blurb: 'Connect OpenClaw agents to ClawPod collaborative spaces.',
|
|
24
|
+
aliases: ['den'],
|
|
25
|
+
order: 80,
|
|
26
|
+
},
|
|
27
|
+
reload: { configPrefixes: ['channels.clawpod'] },
|
|
28
|
+
configSchema: {
|
|
29
|
+
schema: {
|
|
30
|
+
type: 'object',
|
|
31
|
+
additionalProperties: false,
|
|
32
|
+
properties: {
|
|
33
|
+
enabled: { type: 'boolean' },
|
|
34
|
+
serverUrl: { type: 'string' },
|
|
35
|
+
apiKey: { type: 'string' },
|
|
36
|
+
workspaceExpose: { type: 'boolean' },
|
|
37
|
+
agentName: {
|
|
38
|
+
type: 'string',
|
|
39
|
+
description: 'Agent display name in ClawPod',
|
|
40
|
+
},
|
|
41
|
+
allowedMembers: {
|
|
42
|
+
type: 'array',
|
|
43
|
+
items: { type: 'string' },
|
|
44
|
+
description: 'ClawPod member IDs allowed to view workspace & skills',
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
uiHints: {
|
|
49
|
+
enabled: { label: '启用 ClawPod' },
|
|
50
|
+
serverUrl: { label: '服务器地址', placeholder: 'ws://localhost:4000/ws' },
|
|
51
|
+
apiKey: { label: 'Machine API Key', sensitive: true },
|
|
52
|
+
workspaceExpose: { label: '暴露工作区文件' },
|
|
53
|
+
agentName: { label: 'Agent 显示名称', placeholder: '胡桃 🦊' },
|
|
54
|
+
allowedMembers: {
|
|
55
|
+
label: '允许查看工作区/Skills 的成员 ID',
|
|
56
|
+
placeholder: '["mem_xxx"]',
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
capabilities: {
|
|
61
|
+
chatTypes: ['direct', 'channel'],
|
|
62
|
+
polls: false,
|
|
63
|
+
threads: false,
|
|
64
|
+
media: false,
|
|
65
|
+
reactions: false,
|
|
66
|
+
edit: false,
|
|
67
|
+
reply: false,
|
|
68
|
+
},
|
|
69
|
+
config: {
|
|
70
|
+
listAccountIds: (cfg) => {
|
|
71
|
+
const channels = cfg?.channels;
|
|
72
|
+
return channels?.clawpod?.enabled ? ['default'] : [];
|
|
73
|
+
},
|
|
74
|
+
resolveAccount: (cfg, _accountId) => {
|
|
75
|
+
const channels = cfg?.channels;
|
|
76
|
+
const ch = channels?.clawpod ?? {};
|
|
77
|
+
return {
|
|
78
|
+
accountId: 'default',
|
|
79
|
+
enabled: ch.enabled ?? false,
|
|
80
|
+
configured: !!(ch.serverUrl && ch.apiKey),
|
|
81
|
+
serverUrl: ch.serverUrl,
|
|
82
|
+
...ch,
|
|
83
|
+
};
|
|
84
|
+
},
|
|
85
|
+
isConfigured: (account) => !!account?.configured,
|
|
86
|
+
describeAccount: (account) => ({
|
|
87
|
+
accountId: account.accountId || 'default',
|
|
88
|
+
enabled: account.enabled ?? false,
|
|
89
|
+
configured: account.configured ?? false,
|
|
90
|
+
serverUrl: account.serverUrl,
|
|
91
|
+
}),
|
|
92
|
+
},
|
|
93
|
+
outbound: {
|
|
94
|
+
deliveryMode: 'direct',
|
|
95
|
+
sendText: async (params) => {
|
|
96
|
+
const { text, to } = params;
|
|
97
|
+
const conn = connections.get('default');
|
|
98
|
+
if (!conn?.ws)
|
|
99
|
+
return { ok: false, error: 'ClawPod not connected' };
|
|
100
|
+
if (!to)
|
|
101
|
+
return { ok: false, error: 'No target' };
|
|
102
|
+
if (to.startsWith('dm:')) {
|
|
103
|
+
const parts = to.split(':');
|
|
104
|
+
conn.sendDmReply(parts[1], parts[2] || 'member', text);
|
|
105
|
+
}
|
|
106
|
+
else if (to.startsWith('channel:')) {
|
|
107
|
+
conn.sendChannelMessage(to.replace('channel:', ''), text);
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
conn.sendDmReply(to, 'member', text);
|
|
111
|
+
}
|
|
112
|
+
return { ok: true };
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
gateway: {
|
|
116
|
+
startAccount: async (ctx) => {
|
|
117
|
+
const { cfg, accountId, runtime: _runtime, log, abortSignal } = ctx;
|
|
118
|
+
const channels = cfg?.channels;
|
|
119
|
+
const ch = channels?.clawpod;
|
|
120
|
+
if (!ch?.serverUrl || !ch?.apiKey) {
|
|
121
|
+
log?.warn?.(`[clawpod] Missing serverUrl or apiKey`);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
const config = {
|
|
125
|
+
serverUrl: ch.serverUrl,
|
|
126
|
+
apiKey: ch.apiKey,
|
|
127
|
+
agentName: ch.agentName || 'OpenClaw Agent',
|
|
128
|
+
workspaceExpose: ch.workspaceExpose !== false,
|
|
129
|
+
allowedMembers: ch.allowedMembers || [],
|
|
130
|
+
accountId: 'default',
|
|
131
|
+
enabled: true,
|
|
132
|
+
};
|
|
133
|
+
const agentsCfg = cfg?.agents;
|
|
134
|
+
const workspace = agentsCfg?.defaults?.workspace;
|
|
135
|
+
const logFn = typeof log?.info === 'function'
|
|
136
|
+
? log.info.bind(log)
|
|
137
|
+
: typeof log === 'function'
|
|
138
|
+
? log
|
|
139
|
+
: console.log;
|
|
140
|
+
const errFn = typeof log?.error === 'function' ? log.error.bind(log) : console.error;
|
|
141
|
+
const warnFn = typeof log?.warn === 'function' ? log.warn.bind(log) : console.warn;
|
|
142
|
+
const logger = { info: logFn, error: errFn, warn: warnFn };
|
|
143
|
+
const conn = (0, connection_js_1.createConnection)(config, {
|
|
144
|
+
onDm: async (dm) => {
|
|
145
|
+
logger.info(`[clawpod:${accountId}] DM from ${dm.senderName}: ${dm.content?.slice(0, 60)}`);
|
|
146
|
+
await handleInboundMessage({
|
|
147
|
+
cfg,
|
|
148
|
+
logger,
|
|
149
|
+
conn,
|
|
150
|
+
accountId,
|
|
151
|
+
senderId: dm.senderId,
|
|
152
|
+
senderName: dm.senderName || dm.senderId,
|
|
153
|
+
content: dm.content,
|
|
154
|
+
messageId: dm.id,
|
|
155
|
+
isDm: true,
|
|
156
|
+
dmReceiverId: dm.receiverId,
|
|
157
|
+
});
|
|
158
|
+
},
|
|
159
|
+
onMention: async (msg) => {
|
|
160
|
+
logger.info(`[clawpod:${accountId}] @mention by ${msg.senderName}`);
|
|
161
|
+
const clean = msg.content?.replace(`@${config.agentName}`, '').trim();
|
|
162
|
+
await handleInboundMessage({
|
|
163
|
+
cfg,
|
|
164
|
+
logger,
|
|
165
|
+
conn,
|
|
166
|
+
accountId,
|
|
167
|
+
senderId: msg.senderId,
|
|
168
|
+
senderName: msg.senderName || msg.senderId,
|
|
169
|
+
content: clean || msg.content,
|
|
170
|
+
messageId: msg.id,
|
|
171
|
+
isDm: false,
|
|
172
|
+
channelId: msg.channelId,
|
|
173
|
+
});
|
|
174
|
+
},
|
|
175
|
+
logger,
|
|
176
|
+
workspace,
|
|
177
|
+
});
|
|
178
|
+
connections.set('default', conn);
|
|
179
|
+
// Connect and keep running until aborted
|
|
180
|
+
return new Promise((resolve) => {
|
|
181
|
+
conn.connect();
|
|
182
|
+
logger.info(`[clawpod] Started, waiting for abort signal...`);
|
|
183
|
+
if (abortSignal) {
|
|
184
|
+
abortSignal.addEventListener('abort', () => {
|
|
185
|
+
logger.info('[clawpod] Abort signal received, disconnecting');
|
|
186
|
+
conn.disconnect();
|
|
187
|
+
connections.delete('default');
|
|
188
|
+
resolve();
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
status: {
|
|
195
|
+
summary: () => {
|
|
196
|
+
const conn = connections.get('default');
|
|
197
|
+
const connected = conn?.ws?.readyState === 1;
|
|
198
|
+
return {
|
|
199
|
+
ok: connected,
|
|
200
|
+
label: connected ? 'Connected' : 'Not connected',
|
|
201
|
+
};
|
|
202
|
+
},
|
|
203
|
+
describeAccount: (_cfg, _accountId, runtime) => ({
|
|
204
|
+
accountId: 'default',
|
|
205
|
+
running: runtime?.running ?? connections.get('default')?.ws?.readyState === 1,
|
|
206
|
+
configured: true,
|
|
207
|
+
connected: connections.get('default')?.ws?.readyState === 1,
|
|
208
|
+
}),
|
|
209
|
+
},
|
|
210
|
+
};
|
|
211
|
+
exports.clawpodChannel = clawpodChannel;
|
|
212
|
+
async function handleInboundMessage(params) {
|
|
213
|
+
const { cfg, logger, conn, accountId, senderId, senderName, content, messageId, isDm, channelId, } = params;
|
|
214
|
+
// Context for status isolation: dm:<senderId> or channel:<channelId>
|
|
215
|
+
const statusContext = isDm ? `dm:${senderId}` : `channel:${channelId}`;
|
|
216
|
+
try {
|
|
217
|
+
conn.updateStatus('thinking', statusContext);
|
|
218
|
+
// Log available runtime keys for debugging
|
|
219
|
+
logger.info(`[clawpod] Runtime keys: ${Object.keys(pluginRuntime || {}).join(', ')}`);
|
|
220
|
+
if (pluginRuntime?.channel)
|
|
221
|
+
logger.info(`[clawpod] Runtime.channel keys: ${Object.keys(pluginRuntime.channel).join(', ')}`);
|
|
222
|
+
// Build a valid session ID (simple, no colons in problematic places)
|
|
223
|
+
const peerId = isDm ? senderId : channelId;
|
|
224
|
+
const sessionKey = `clawpod-${isDm ? 'dm' : 'ch'}-${peerId}`;
|
|
225
|
+
logger.info(`[clawpod] Session: ${sessionKey}`);
|
|
226
|
+
let route = { sessionKey, accountId };
|
|
227
|
+
if (pluginRuntime?.channel?.routing?.resolveAgentRoute) {
|
|
228
|
+
try {
|
|
229
|
+
route = pluginRuntime.channel.routing.resolveAgentRoute({
|
|
230
|
+
cfg,
|
|
231
|
+
channel: 'clawpod',
|
|
232
|
+
accountId,
|
|
233
|
+
peer: { kind: isDm ? 'dm' : 'group', id: peerId },
|
|
234
|
+
});
|
|
235
|
+
logger.info(`[clawpod] Resolved route: ${route.sessionKey}`);
|
|
236
|
+
}
|
|
237
|
+
catch (e) {
|
|
238
|
+
logger.info(`[clawpod] Route fallback: ${e instanceof Error ? e.message : String(e)}`);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
const ctx = {
|
|
242
|
+
SessionKey: route.sessionKey,
|
|
243
|
+
Channel: 'clawpod',
|
|
244
|
+
AccountId: accountId,
|
|
245
|
+
Body: content,
|
|
246
|
+
SenderName: senderName,
|
|
247
|
+
SenderId: senderId,
|
|
248
|
+
ChatType: isDm ? 'direct' : 'group',
|
|
249
|
+
CommandAuthorized: true,
|
|
250
|
+
};
|
|
251
|
+
function extractText(payload) {
|
|
252
|
+
if (typeof payload === 'string')
|
|
253
|
+
return payload;
|
|
254
|
+
if (payload && typeof payload === 'object') {
|
|
255
|
+
const obj = payload;
|
|
256
|
+
if (typeof obj.text === 'string')
|
|
257
|
+
return obj.text;
|
|
258
|
+
if (typeof obj.content === 'string')
|
|
259
|
+
return obj.content;
|
|
260
|
+
if (typeof obj.body === 'string')
|
|
261
|
+
return obj.body;
|
|
262
|
+
logger.info(`[clawpod] Unknown reply payload type: ${typeof payload}, keys: ${Object.keys(obj).join(',')}`);
|
|
263
|
+
}
|
|
264
|
+
return String(payload ?? '');
|
|
265
|
+
}
|
|
266
|
+
// Try the full dispatch pipeline
|
|
267
|
+
if (pluginRuntime?.channel?.reply?.dispatchReplyFromConfig) {
|
|
268
|
+
let replied = false;
|
|
269
|
+
const dispatcher = {
|
|
270
|
+
sendToolResult: () => false,
|
|
271
|
+
sendBlockReply: (raw) => {
|
|
272
|
+
const text = extractText(raw);
|
|
273
|
+
if (text.trim()) {
|
|
274
|
+
conn.updateStatus('working', statusContext);
|
|
275
|
+
logger.info(`[clawpod] Block reply (${text.length} chars): ${text.slice(0, 80)}`);
|
|
276
|
+
if (isDm)
|
|
277
|
+
conn.sendDmReply(senderId, 'member', text);
|
|
278
|
+
else if (channelId)
|
|
279
|
+
conn.sendChannelMessage(channelId, text);
|
|
280
|
+
replied = true;
|
|
281
|
+
}
|
|
282
|
+
return true;
|
|
283
|
+
},
|
|
284
|
+
sendFinalReply: (raw) => {
|
|
285
|
+
const text = extractText(raw);
|
|
286
|
+
logger.info(`[clawpod] Final reply (${text.length} chars, alreadyReplied=${replied}): ${text.slice(0, 80)}`);
|
|
287
|
+
if (text.trim() && !replied) {
|
|
288
|
+
conn.updateStatus('working', statusContext);
|
|
289
|
+
if (isDm)
|
|
290
|
+
conn.sendDmReply(senderId, 'member', text);
|
|
291
|
+
else if (channelId)
|
|
292
|
+
conn.sendChannelMessage(channelId, text);
|
|
293
|
+
}
|
|
294
|
+
conn.sendProcessed(messageId);
|
|
295
|
+
conn.updateStatus('online', statusContext);
|
|
296
|
+
return true;
|
|
297
|
+
},
|
|
298
|
+
waitForIdle: async () => { },
|
|
299
|
+
getQueuedCounts: () => ({ tool: 0, block: 0, final: 0 }),
|
|
300
|
+
markComplete: () => { },
|
|
301
|
+
};
|
|
302
|
+
const replyApi = pluginRuntime.channel.reply;
|
|
303
|
+
if (replyApi.finalizeInboundContext) {
|
|
304
|
+
replyApi.finalizeInboundContext(ctx, cfg);
|
|
305
|
+
}
|
|
306
|
+
await replyApi.withReplyDispatcher({
|
|
307
|
+
dispatcher,
|
|
308
|
+
onSettled: () => conn.updateStatus('online', statusContext),
|
|
309
|
+
run: () => replyApi.dispatchReplyFromConfig({
|
|
310
|
+
ctx,
|
|
311
|
+
cfg,
|
|
312
|
+
dispatcher,
|
|
313
|
+
replyOptions: {},
|
|
314
|
+
}),
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
else {
|
|
318
|
+
// Use openclaw agent CLI via detached spawn
|
|
319
|
+
logger.info('[clawpod] Dispatching via openclaw agent CLI');
|
|
320
|
+
const { spawn: spawnFn } = await import('node:child_process');
|
|
321
|
+
const reply = await new Promise((resolve) => {
|
|
322
|
+
const proc = spawnFn('openclaw', ['agent', '--message', content, '--session-id', route.sessionKey], {
|
|
323
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
324
|
+
timeout: 90000,
|
|
325
|
+
detached: false,
|
|
326
|
+
env: { ...process.env },
|
|
327
|
+
});
|
|
328
|
+
let stdout = '';
|
|
329
|
+
let stderr = '';
|
|
330
|
+
proc.stdout?.on('data', (d) => {
|
|
331
|
+
stdout += d.toString();
|
|
332
|
+
});
|
|
333
|
+
proc.stderr?.on('data', (d) => {
|
|
334
|
+
stderr += d.toString();
|
|
335
|
+
});
|
|
336
|
+
proc.on('close', (code) => {
|
|
337
|
+
if (code !== 0) {
|
|
338
|
+
logger.error(`[clawpod] Agent CLI exit ${code}: ${stderr.slice(0, 300)}`);
|
|
339
|
+
}
|
|
340
|
+
const clean = stdout
|
|
341
|
+
.split('\n')
|
|
342
|
+
.filter((l) => !l.startsWith('[plugins]') && !l.startsWith('[openclaw]'))
|
|
343
|
+
.join('\n')
|
|
344
|
+
.trim();
|
|
345
|
+
logger.info(`[clawpod] Agent CLI output (${clean.length} chars): ${clean.slice(0, 80)}`);
|
|
346
|
+
resolve(clean || null);
|
|
347
|
+
});
|
|
348
|
+
proc.on('error', (err) => {
|
|
349
|
+
logger.error(`[clawpod] Spawn error: ${err.message}`);
|
|
350
|
+
resolve(null);
|
|
351
|
+
});
|
|
352
|
+
});
|
|
353
|
+
if (reply) {
|
|
354
|
+
conn.updateStatus('working', statusContext);
|
|
355
|
+
if (isDm)
|
|
356
|
+
conn.sendDmReply(senderId, 'member', reply);
|
|
357
|
+
else if (channelId)
|
|
358
|
+
conn.sendChannelMessage(channelId, reply);
|
|
359
|
+
logger.info(`[clawpod] Replied: ${reply.slice(0, 60)}`);
|
|
360
|
+
}
|
|
361
|
+
conn.sendProcessed(messageId);
|
|
362
|
+
conn.updateStatus('online', statusContext);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
catch (err) {
|
|
366
|
+
logger.error(`[clawpod] Dispatch error: ${err instanceof Error ? err.message : String(err)}`);
|
|
367
|
+
conn.updateStatus('online', statusContext);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
// Plugin entry
|
|
371
|
+
function register(api) {
|
|
372
|
+
_pluginApi = api;
|
|
373
|
+
pluginRuntime = api.runtime;
|
|
374
|
+
api.logger.info(`[clawpod] api.runtime keys: ${Object.keys(api.runtime || {}).join(', ')}`);
|
|
375
|
+
api.registerChannel({ plugin: clawpodChannel });
|
|
376
|
+
// Register clawpod_send tool
|
|
377
|
+
api.registerTool({
|
|
378
|
+
name: 'clawpod_send',
|
|
379
|
+
description: 'Send a message to a ClawPod channel or DM',
|
|
380
|
+
parameters: {
|
|
381
|
+
type: 'object',
|
|
382
|
+
properties: {
|
|
383
|
+
target: {
|
|
384
|
+
type: 'string',
|
|
385
|
+
description: "Target: 'channel:<id>' or 'dm:<userId>:<type>'",
|
|
386
|
+
},
|
|
387
|
+
message: { type: 'string', description: 'Message content' },
|
|
388
|
+
accountId: {
|
|
389
|
+
type: 'string',
|
|
390
|
+
description: "ClawPod account ID (default: 'default')",
|
|
391
|
+
},
|
|
392
|
+
},
|
|
393
|
+
required: ['target', 'message'],
|
|
394
|
+
},
|
|
395
|
+
handler: async (params) => {
|
|
396
|
+
const conn = connections.get(params.accountId || 'default');
|
|
397
|
+
if (!conn?.ws)
|
|
398
|
+
return { error: 'ClawPod not connected' };
|
|
399
|
+
if (params.target.startsWith('channel:')) {
|
|
400
|
+
conn.sendChannelMessage(params.target.replace('channel:', ''), params.message);
|
|
401
|
+
}
|
|
402
|
+
else if (params.target.startsWith('dm:')) {
|
|
403
|
+
const [, id, type] = params.target.split(':');
|
|
404
|
+
conn.sendDmReply(id, type || 'member', params.message);
|
|
405
|
+
}
|
|
406
|
+
return { success: true };
|
|
407
|
+
},
|
|
408
|
+
});
|
|
409
|
+
api.logger.info('[clawpod] Channel plugin registered');
|
|
410
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@clawpod/openclaw-plugin",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "ClawPod channel plugin for OpenClaw",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist"
|
|
9
|
+
],
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsc",
|
|
12
|
+
"prepublishOnly": "npm run build"
|
|
13
|
+
},
|
|
14
|
+
"openclaw": {
|
|
15
|
+
"extensions": [
|
|
16
|
+
"./src/index.ts"
|
|
17
|
+
],
|
|
18
|
+
"channel": {
|
|
19
|
+
"id": "clawpod",
|
|
20
|
+
"label": "ClawPod",
|
|
21
|
+
"selectionLabel": "ClawPod (Human & Agent Workspace)",
|
|
22
|
+
"docsPath": "/channels/clawpod",
|
|
23
|
+
"blurb": "Connect OpenClaw agents to ClawPod collaborative spaces.",
|
|
24
|
+
"order": 80,
|
|
25
|
+
"aliases": [
|
|
26
|
+
"den"
|
|
27
|
+
]
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"ws": "^8.18.0"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@types/ws": "^8.5.0"
|
|
35
|
+
}
|
|
36
|
+
}
|