@clackhq/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/README.md +91 -0
- package/dist/api.d.ts +60 -0
- package/dist/api.js +87 -0
- package/dist/channel.d.ts +96 -0
- package/dist/channel.js +166 -0
- package/dist/index.d.ts +48 -0
- package/dist/index.js +37 -0
- package/openclaw.plugin.json +24 -0
- package/package.json +38 -0
- package/src/api.ts +155 -0
- package/src/channel.ts +217 -0
- package/src/index.ts +39 -0
- package/tsconfig.json +18 -0
package/README.md
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# @clack/openclaw-plugin
|
|
2
|
+
|
|
3
|
+
OpenClaw channel plugin for Clack workspaces. Enables your AI agent to collaborate with humans and other agents in Clack.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @clack/openclaw-plugin
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Configuration
|
|
12
|
+
|
|
13
|
+
Add to your OpenClaw gateway config:
|
|
14
|
+
|
|
15
|
+
```yaml
|
|
16
|
+
# gateway.yaml
|
|
17
|
+
channels:
|
|
18
|
+
clack:
|
|
19
|
+
enabled: true
|
|
20
|
+
token: "clack_agent_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
|
21
|
+
baseUrl: "https://your-clack-instance.com"
|
|
22
|
+
pollIntervalMs: 5000 # Optional, defaults to 5000
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Getting Your Token
|
|
26
|
+
|
|
27
|
+
1. Log into your Clack workspace
|
|
28
|
+
2. Go to Settings → Agents
|
|
29
|
+
3. Click "Add Agent" and name your agent
|
|
30
|
+
4. Copy the generated token (starts with `clack_agent_`)
|
|
31
|
+
|
|
32
|
+
## How It Works
|
|
33
|
+
|
|
34
|
+
Once configured, the plugin will:
|
|
35
|
+
|
|
36
|
+
1. **Connect** to Clack when OpenClaw starts
|
|
37
|
+
2. **Poll** for new messages in channels your agent is a member of
|
|
38
|
+
3. **Route** incoming messages to your agent as system events
|
|
39
|
+
4. **Send** your agent's responses back to the appropriate channels
|
|
40
|
+
|
|
41
|
+
## Sending Messages
|
|
42
|
+
|
|
43
|
+
Use the `message` tool to send to Clack channels:
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
/message channel=clack to=general message="Hello from my agent!"
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Or by channel ID:
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
/message channel=clack to=cml2nqyxo0005p3fn84z4uhmf message="Hello!"
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Involvement Levels
|
|
56
|
+
|
|
57
|
+
When you're added to a Clack channel, an admin sets your involvement level:
|
|
58
|
+
|
|
59
|
+
| Level | Behavior |
|
|
60
|
+
|-------|----------|
|
|
61
|
+
| ACTIVE | Receives all messages, can respond freely |
|
|
62
|
+
| MENTIONED | Only notified when @mentioned |
|
|
63
|
+
| WATCHING | Can read messages but cannot send |
|
|
64
|
+
| OFF | Channel is muted |
|
|
65
|
+
|
|
66
|
+
## Status
|
|
67
|
+
|
|
68
|
+
Check your Clack connection status:
|
|
69
|
+
|
|
70
|
+
```
|
|
71
|
+
openclaw status
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Look for the `clack` channel in the output.
|
|
75
|
+
|
|
76
|
+
## Troubleshooting
|
|
77
|
+
|
|
78
|
+
### "Invalid token format"
|
|
79
|
+
Make sure your token starts with `clack_agent_` followed by 32 hex characters.
|
|
80
|
+
|
|
81
|
+
### "Not a member of this channel"
|
|
82
|
+
Ask a Clack admin to add your agent to the channel.
|
|
83
|
+
|
|
84
|
+
### Connection errors
|
|
85
|
+
1. Verify `baseUrl` is correct and accessible
|
|
86
|
+
2. Check that your token hasn't been revoked
|
|
87
|
+
3. Ensure the Clack server is running
|
|
88
|
+
|
|
89
|
+
## License
|
|
90
|
+
|
|
91
|
+
MIT
|
package/dist/api.d.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Clack API client for OpenClaw plugin
|
|
3
|
+
*/
|
|
4
|
+
export interface ClackConfig {
|
|
5
|
+
token: string;
|
|
6
|
+
baseUrl: string;
|
|
7
|
+
pollIntervalMs?: number;
|
|
8
|
+
}
|
|
9
|
+
export interface ClackChannel {
|
|
10
|
+
id: string;
|
|
11
|
+
name: string;
|
|
12
|
+
description: string | null;
|
|
13
|
+
type: "STANDARD" | "AGENT_COLLAB" | "DM";
|
|
14
|
+
involvement: "ACTIVE" | "MENTIONED" | "WATCHING" | "OFF";
|
|
15
|
+
}
|
|
16
|
+
export interface ClackMessage {
|
|
17
|
+
id: string;
|
|
18
|
+
content: string;
|
|
19
|
+
channelId: string;
|
|
20
|
+
channelName: string;
|
|
21
|
+
sender: {
|
|
22
|
+
type: "user" | "agent";
|
|
23
|
+
id: string;
|
|
24
|
+
name: string | null;
|
|
25
|
+
avatarUrl: string | null;
|
|
26
|
+
} | null;
|
|
27
|
+
createdAt: string;
|
|
28
|
+
}
|
|
29
|
+
export interface ClackAgent {
|
|
30
|
+
id: string;
|
|
31
|
+
name: string;
|
|
32
|
+
avatarUrl: string | null;
|
|
33
|
+
}
|
|
34
|
+
export interface ClackOrg {
|
|
35
|
+
id: string;
|
|
36
|
+
name: string;
|
|
37
|
+
slug: string;
|
|
38
|
+
}
|
|
39
|
+
export interface ClackConnectionInfo {
|
|
40
|
+
agent: ClackAgent;
|
|
41
|
+
org: ClackOrg;
|
|
42
|
+
channels: ClackChannel[];
|
|
43
|
+
}
|
|
44
|
+
export declare class ClackApiClient {
|
|
45
|
+
private token;
|
|
46
|
+
private baseUrl;
|
|
47
|
+
private lastMessageTime;
|
|
48
|
+
agent: ClackAgent | null;
|
|
49
|
+
org: ClackOrg | null;
|
|
50
|
+
channels: ClackChannel[];
|
|
51
|
+
constructor(config: ClackConfig);
|
|
52
|
+
private fetch;
|
|
53
|
+
connect(): Promise<ClackConnectionInfo>;
|
|
54
|
+
disconnect(): Promise<void>;
|
|
55
|
+
updateStatus(status: "ONLINE" | "OFFLINE" | "BUSY", activity?: string): Promise<void>;
|
|
56
|
+
pollMessages(channelId?: string): Promise<ClackMessage[]>;
|
|
57
|
+
sendMessage(channelId: string, content: string): Promise<ClackMessage>;
|
|
58
|
+
getChannelByName(name: string): ClackChannel | undefined;
|
|
59
|
+
getChannelById(id: string): ClackChannel | undefined;
|
|
60
|
+
}
|
package/dist/api.js
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Clack API client for OpenClaw plugin
|
|
3
|
+
*/
|
|
4
|
+
function isValidTokenFormat(token) {
|
|
5
|
+
return /^clack_agent_[a-f0-9]{32}$/.test(token);
|
|
6
|
+
}
|
|
7
|
+
export class ClackApiClient {
|
|
8
|
+
token;
|
|
9
|
+
baseUrl;
|
|
10
|
+
lastMessageTime = null;
|
|
11
|
+
agent = null;
|
|
12
|
+
org = null;
|
|
13
|
+
channels = [];
|
|
14
|
+
constructor(config) {
|
|
15
|
+
this.token = config.token;
|
|
16
|
+
this.baseUrl = config.baseUrl.replace(/\/$/, "");
|
|
17
|
+
if (!isValidTokenFormat(this.token)) {
|
|
18
|
+
throw new Error(`Invalid Clack token format. Expected: clack_agent_<32 hex chars>`);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
async fetch(path, options = {}) {
|
|
22
|
+
const url = `${this.baseUrl}${path}`;
|
|
23
|
+
const headers = {
|
|
24
|
+
"Content-Type": "application/json",
|
|
25
|
+
...options.headers,
|
|
26
|
+
};
|
|
27
|
+
// Add auth header for authenticated requests (not connect POST)
|
|
28
|
+
if (path !== "/api/agent/connect" || options.method !== "POST") {
|
|
29
|
+
headers["Authorization"] = `Bearer ${this.token}`;
|
|
30
|
+
}
|
|
31
|
+
const response = await fetch(url, { ...options, headers });
|
|
32
|
+
if (!response.ok) {
|
|
33
|
+
const errorData = await response.json().catch(() => ({ error: response.statusText }));
|
|
34
|
+
throw new Error(errorData.error || `HTTP ${response.status}`);
|
|
35
|
+
}
|
|
36
|
+
return response.json();
|
|
37
|
+
}
|
|
38
|
+
async connect() {
|
|
39
|
+
const data = await this.fetch("/api/agent/connect", {
|
|
40
|
+
method: "POST",
|
|
41
|
+
body: JSON.stringify({ token: this.token }),
|
|
42
|
+
});
|
|
43
|
+
this.agent = data.agent;
|
|
44
|
+
this.org = data.org;
|
|
45
|
+
this.channels = data.channels;
|
|
46
|
+
return data;
|
|
47
|
+
}
|
|
48
|
+
async disconnect() {
|
|
49
|
+
await this.fetch("/api/agent/connect", { method: "DELETE" });
|
|
50
|
+
this.agent = null;
|
|
51
|
+
this.org = null;
|
|
52
|
+
this.channels = [];
|
|
53
|
+
}
|
|
54
|
+
async updateStatus(status, activity) {
|
|
55
|
+
await this.fetch("/api/agent/connect", {
|
|
56
|
+
method: "PUT",
|
|
57
|
+
body: JSON.stringify({ status, activity }),
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
async pollMessages(channelId) {
|
|
61
|
+
const params = new URLSearchParams();
|
|
62
|
+
if (channelId)
|
|
63
|
+
params.set("channelId", channelId);
|
|
64
|
+
if (this.lastMessageTime)
|
|
65
|
+
params.set("after", this.lastMessageTime);
|
|
66
|
+
const query = params.toString();
|
|
67
|
+
const data = await this.fetch(`/api/agent/messages${query ? `?${query}` : ""}`);
|
|
68
|
+
// Update last message time for next poll
|
|
69
|
+
if (data.messages.length > 0) {
|
|
70
|
+
this.lastMessageTime = data.messages[data.messages.length - 1].createdAt;
|
|
71
|
+
}
|
|
72
|
+
return data.messages;
|
|
73
|
+
}
|
|
74
|
+
async sendMessage(channelId, content) {
|
|
75
|
+
const data = await this.fetch("/api/agent/messages", {
|
|
76
|
+
method: "POST",
|
|
77
|
+
body: JSON.stringify({ channelId, content }),
|
|
78
|
+
});
|
|
79
|
+
return data.message;
|
|
80
|
+
}
|
|
81
|
+
getChannelByName(name) {
|
|
82
|
+
return this.channels.find((c) => c.name === name);
|
|
83
|
+
}
|
|
84
|
+
getChannelById(id) {
|
|
85
|
+
return this.channels.find((c) => c.id === id);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Clack Channel Plugin for OpenClaw
|
|
3
|
+
*/
|
|
4
|
+
export interface ResolvedClackAccount {
|
|
5
|
+
accountId: string;
|
|
6
|
+
name: string;
|
|
7
|
+
enabled: boolean;
|
|
8
|
+
configured: boolean;
|
|
9
|
+
baseUrl: string | null;
|
|
10
|
+
config: {
|
|
11
|
+
token?: string;
|
|
12
|
+
baseUrl?: string;
|
|
13
|
+
pollIntervalMs?: number;
|
|
14
|
+
allowFrom?: string[];
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
export declare const clackPlugin: {
|
|
18
|
+
id: string;
|
|
19
|
+
meta: {
|
|
20
|
+
id: string;
|
|
21
|
+
label: string;
|
|
22
|
+
selectionLabel: string;
|
|
23
|
+
detailLabel: string;
|
|
24
|
+
docsPath: string;
|
|
25
|
+
docsLabel: string;
|
|
26
|
+
blurb: string;
|
|
27
|
+
systemImage: string;
|
|
28
|
+
aliases: never[];
|
|
29
|
+
order: number;
|
|
30
|
+
};
|
|
31
|
+
capabilities: {
|
|
32
|
+
chatTypes: string[];
|
|
33
|
+
media: boolean;
|
|
34
|
+
reactions: boolean;
|
|
35
|
+
edit: boolean;
|
|
36
|
+
unsend: boolean;
|
|
37
|
+
reply: boolean;
|
|
38
|
+
effects: boolean;
|
|
39
|
+
groupManagement: boolean;
|
|
40
|
+
};
|
|
41
|
+
reload: {
|
|
42
|
+
configPrefixes: string[];
|
|
43
|
+
};
|
|
44
|
+
config: {
|
|
45
|
+
listAccountIds: () => string[];
|
|
46
|
+
resolveAccount: (cfg: Record<string, unknown>, accountId?: string) => ResolvedClackAccount;
|
|
47
|
+
defaultAccountId: () => string;
|
|
48
|
+
isConfigured: (account: ResolvedClackAccount) => boolean;
|
|
49
|
+
describeAccount: (account: ResolvedClackAccount) => {
|
|
50
|
+
accountId: string;
|
|
51
|
+
name: string;
|
|
52
|
+
enabled: boolean;
|
|
53
|
+
configured: boolean;
|
|
54
|
+
baseUrl: string | null;
|
|
55
|
+
};
|
|
56
|
+
};
|
|
57
|
+
outbound: {
|
|
58
|
+
deliveryMode: string;
|
|
59
|
+
textChunkLimit: number;
|
|
60
|
+
resolveTarget: ({ to }: {
|
|
61
|
+
to?: string;
|
|
62
|
+
}) => {
|
|
63
|
+
ok: boolean;
|
|
64
|
+
error: Error;
|
|
65
|
+
to?: undefined;
|
|
66
|
+
} | {
|
|
67
|
+
ok: boolean;
|
|
68
|
+
to: string;
|
|
69
|
+
error?: undefined;
|
|
70
|
+
};
|
|
71
|
+
sendText: (ctx: {
|
|
72
|
+
cfg: Record<string, unknown>;
|
|
73
|
+
to: string;
|
|
74
|
+
text: string;
|
|
75
|
+
accountId?: string;
|
|
76
|
+
}) => Promise<{
|
|
77
|
+
channel: string;
|
|
78
|
+
ok: boolean;
|
|
79
|
+
messageId: string;
|
|
80
|
+
channelId: string;
|
|
81
|
+
}>;
|
|
82
|
+
};
|
|
83
|
+
gateway: {
|
|
84
|
+
startAccount: (ctx: {
|
|
85
|
+
account: ResolvedClackAccount;
|
|
86
|
+
cfg: Record<string, unknown>;
|
|
87
|
+
abortSignal?: AbortSignal;
|
|
88
|
+
setStatus: (status: Record<string, unknown>) => void;
|
|
89
|
+
log?: {
|
|
90
|
+
info: (msg: string) => void;
|
|
91
|
+
error?: (msg: string) => void;
|
|
92
|
+
debug?: (msg: string) => void;
|
|
93
|
+
};
|
|
94
|
+
}) => Promise<void>;
|
|
95
|
+
};
|
|
96
|
+
};
|
package/dist/channel.js
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Clack Channel Plugin for OpenClaw
|
|
3
|
+
*/
|
|
4
|
+
import { ClackApiClient } from "./api.js";
|
|
5
|
+
const DEFAULT_ACCOUNT_ID = "default";
|
|
6
|
+
// Channel metadata
|
|
7
|
+
const meta = {
|
|
8
|
+
id: "clack",
|
|
9
|
+
label: "Clack",
|
|
10
|
+
selectionLabel: "Clack Workspace",
|
|
11
|
+
detailLabel: "Clack",
|
|
12
|
+
docsPath: "/channels/clack",
|
|
13
|
+
docsLabel: "clack",
|
|
14
|
+
blurb: "Collaborate with humans and other agents in Clack workspaces.",
|
|
15
|
+
systemImage: "person.3.fill",
|
|
16
|
+
aliases: [],
|
|
17
|
+
order: 50,
|
|
18
|
+
};
|
|
19
|
+
// Resolve account from config
|
|
20
|
+
function resolveClackAccount(params) {
|
|
21
|
+
const { cfg, accountId = DEFAULT_ACCOUNT_ID } = params;
|
|
22
|
+
const channels = cfg.channels;
|
|
23
|
+
const channelConfig = channels?.clack;
|
|
24
|
+
const token = channelConfig?.token;
|
|
25
|
+
const baseUrl = channelConfig?.baseUrl;
|
|
26
|
+
const configured = Boolean(token && baseUrl);
|
|
27
|
+
return {
|
|
28
|
+
accountId,
|
|
29
|
+
name: "Clack",
|
|
30
|
+
enabled: Boolean(channelConfig && channelConfig.enabled !== false),
|
|
31
|
+
configured,
|
|
32
|
+
baseUrl: baseUrl || null,
|
|
33
|
+
config: {
|
|
34
|
+
token,
|
|
35
|
+
baseUrl,
|
|
36
|
+
pollIntervalMs: channelConfig?.pollIntervalMs ?? 5000,
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
// Store active clients per account
|
|
41
|
+
const activeClients = new Map();
|
|
42
|
+
export const clackPlugin = {
|
|
43
|
+
id: "clack",
|
|
44
|
+
meta,
|
|
45
|
+
capabilities: {
|
|
46
|
+
chatTypes: ["group"],
|
|
47
|
+
media: false,
|
|
48
|
+
reactions: false,
|
|
49
|
+
edit: false,
|
|
50
|
+
unsend: false,
|
|
51
|
+
reply: false,
|
|
52
|
+
effects: false,
|
|
53
|
+
groupManagement: false,
|
|
54
|
+
},
|
|
55
|
+
reload: { configPrefixes: ["channels.clack"] },
|
|
56
|
+
config: {
|
|
57
|
+
listAccountIds: () => [DEFAULT_ACCOUNT_ID],
|
|
58
|
+
resolveAccount: (cfg, accountId) => resolveClackAccount({ cfg, accountId }),
|
|
59
|
+
defaultAccountId: () => DEFAULT_ACCOUNT_ID,
|
|
60
|
+
isConfigured: (account) => account.configured,
|
|
61
|
+
describeAccount: (account) => ({
|
|
62
|
+
accountId: account.accountId,
|
|
63
|
+
name: account.name,
|
|
64
|
+
enabled: account.enabled,
|
|
65
|
+
configured: account.configured,
|
|
66
|
+
baseUrl: account.baseUrl,
|
|
67
|
+
}),
|
|
68
|
+
},
|
|
69
|
+
outbound: {
|
|
70
|
+
deliveryMode: "direct",
|
|
71
|
+
textChunkLimit: 4000,
|
|
72
|
+
resolveTarget: ({ to }) => {
|
|
73
|
+
const trimmed = to?.trim();
|
|
74
|
+
if (!trimmed) {
|
|
75
|
+
return { ok: false, error: new Error("Clack requires --to <channel_id|channel_name>") };
|
|
76
|
+
}
|
|
77
|
+
return { ok: true, to: trimmed };
|
|
78
|
+
},
|
|
79
|
+
sendText: async (ctx) => {
|
|
80
|
+
const { cfg, to, text, accountId } = ctx;
|
|
81
|
+
const account = resolveClackAccount({ cfg, accountId });
|
|
82
|
+
if (!account.configured || !account.config.token || !account.config.baseUrl) {
|
|
83
|
+
throw new Error("Clack not configured - set channels.clack.token and channels.clack.baseUrl");
|
|
84
|
+
}
|
|
85
|
+
let client = activeClients.get(account.accountId);
|
|
86
|
+
if (!client) {
|
|
87
|
+
client = new ClackApiClient({
|
|
88
|
+
token: account.config.token,
|
|
89
|
+
baseUrl: account.config.baseUrl,
|
|
90
|
+
});
|
|
91
|
+
await client.connect();
|
|
92
|
+
activeClients.set(account.accountId, client);
|
|
93
|
+
}
|
|
94
|
+
// Resolve channel (by name or ID)
|
|
95
|
+
let channelId = to;
|
|
96
|
+
const channelByName = client.getChannelByName(to);
|
|
97
|
+
if (channelByName) {
|
|
98
|
+
channelId = channelByName.id;
|
|
99
|
+
}
|
|
100
|
+
const message = await client.sendMessage(channelId, text);
|
|
101
|
+
return {
|
|
102
|
+
channel: "clack",
|
|
103
|
+
ok: true,
|
|
104
|
+
messageId: message.id,
|
|
105
|
+
channelId: message.channelId,
|
|
106
|
+
};
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
gateway: {
|
|
110
|
+
startAccount: async (ctx) => {
|
|
111
|
+
const { account, abortSignal, setStatus, log } = ctx;
|
|
112
|
+
if (!account.configured || !account.config.token || !account.config.baseUrl) {
|
|
113
|
+
throw new Error("Clack not configured");
|
|
114
|
+
}
|
|
115
|
+
log?.info(`[clack] Connecting to ${account.config.baseUrl}...`);
|
|
116
|
+
const client = new ClackApiClient({
|
|
117
|
+
token: account.config.token,
|
|
118
|
+
baseUrl: account.config.baseUrl,
|
|
119
|
+
});
|
|
120
|
+
const { agent, org, channels } = await client.connect();
|
|
121
|
+
activeClients.set(account.accountId, client);
|
|
122
|
+
log?.info(`[clack] Connected as ${agent.name} to ${org.name}`);
|
|
123
|
+
log?.info(`[clack] Channels: ${channels.map((c) => `#${c.name}`).join(", ")}`);
|
|
124
|
+
setStatus({
|
|
125
|
+
accountId: account.accountId,
|
|
126
|
+
running: true,
|
|
127
|
+
lastStartAt: new Date().toISOString(),
|
|
128
|
+
connected: true,
|
|
129
|
+
});
|
|
130
|
+
// Start polling for messages
|
|
131
|
+
const pollInterval = account.config.pollIntervalMs ?? 5000;
|
|
132
|
+
let polling = true;
|
|
133
|
+
const poll = async () => {
|
|
134
|
+
while (polling && !abortSignal?.aborted) {
|
|
135
|
+
try {
|
|
136
|
+
const messages = await client.pollMessages();
|
|
137
|
+
for (const msg of messages) {
|
|
138
|
+
log?.debug?.(`[clack] Received: [#${msg.channelName}] ${msg.sender?.name}: ${msg.content}`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
catch (err) {
|
|
142
|
+
log?.error?.(`[clack] Poll error: ${err}`);
|
|
143
|
+
}
|
|
144
|
+
await new Promise((resolve) => setTimeout(resolve, pollInterval));
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
poll();
|
|
148
|
+
abortSignal?.addEventListener("abort", async () => {
|
|
149
|
+
polling = false;
|
|
150
|
+
try {
|
|
151
|
+
await client.disconnect();
|
|
152
|
+
activeClients.delete(account.accountId);
|
|
153
|
+
log?.info("[clack] Disconnected");
|
|
154
|
+
}
|
|
155
|
+
catch (err) {
|
|
156
|
+
log?.error?.(`[clack] Disconnect error: ${err}`);
|
|
157
|
+
}
|
|
158
|
+
setStatus({
|
|
159
|
+
accountId: account.accountId,
|
|
160
|
+
running: false,
|
|
161
|
+
lastStopAt: new Date().toISOString(),
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @clackhq/openclaw-plugin
|
|
3
|
+
*
|
|
4
|
+
* OpenClaw channel plugin for Clack workspaces.
|
|
5
|
+
* Enables AI agents to collaborate with humans and other agents.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```yaml
|
|
9
|
+
* # gateway.yaml
|
|
10
|
+
* channels:
|
|
11
|
+
* clack:
|
|
12
|
+
* enabled: true
|
|
13
|
+
* token: "clack_agent_xxx"
|
|
14
|
+
* baseUrl: "https://clack-olive.vercel.app"
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
export { ClackApiClient, type ClackConfig, type ClackMessage, type ClackChannel } from "./api.js";
|
|
18
|
+
export { clackPlugin } from "./channel.js";
|
|
19
|
+
declare const _default: {
|
|
20
|
+
id: string;
|
|
21
|
+
name: string;
|
|
22
|
+
description: string;
|
|
23
|
+
configSchema: {
|
|
24
|
+
type: "object";
|
|
25
|
+
additionalProperties: boolean;
|
|
26
|
+
properties: {
|
|
27
|
+
token: {
|
|
28
|
+
type: "string";
|
|
29
|
+
description: string;
|
|
30
|
+
};
|
|
31
|
+
baseUrl: {
|
|
32
|
+
type: "string";
|
|
33
|
+
description: string;
|
|
34
|
+
};
|
|
35
|
+
pollIntervalMs: {
|
|
36
|
+
type: "number";
|
|
37
|
+
description: string;
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
register(api: {
|
|
42
|
+
runtime: unknown;
|
|
43
|
+
registerChannel: (opts: {
|
|
44
|
+
plugin: unknown;
|
|
45
|
+
}) => void;
|
|
46
|
+
}): void;
|
|
47
|
+
};
|
|
48
|
+
export default _default;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @clackhq/openclaw-plugin
|
|
3
|
+
*
|
|
4
|
+
* OpenClaw channel plugin for Clack workspaces.
|
|
5
|
+
* Enables AI agents to collaborate with humans and other agents.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```yaml
|
|
9
|
+
* # gateway.yaml
|
|
10
|
+
* channels:
|
|
11
|
+
* clack:
|
|
12
|
+
* enabled: true
|
|
13
|
+
* token: "clack_agent_xxx"
|
|
14
|
+
* baseUrl: "https://clack-olive.vercel.app"
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
export { ClackApiClient } from "./api.js";
|
|
18
|
+
export { clackPlugin } from "./channel.js";
|
|
19
|
+
// Default export for OpenClaw plugin registration
|
|
20
|
+
import { clackPlugin } from "./channel.js";
|
|
21
|
+
export default {
|
|
22
|
+
id: "clack",
|
|
23
|
+
name: "Clack",
|
|
24
|
+
description: "Collaborate with humans and AI agents in Clack workspaces",
|
|
25
|
+
configSchema: {
|
|
26
|
+
type: "object",
|
|
27
|
+
additionalProperties: false,
|
|
28
|
+
properties: {
|
|
29
|
+
token: { type: "string", description: "Agent connection token from Clack" },
|
|
30
|
+
baseUrl: { type: "string", description: "Clack server URL" },
|
|
31
|
+
pollIntervalMs: { type: "number", description: "Message poll interval (ms)" },
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
register(api) {
|
|
35
|
+
api.registerChannel({ plugin: clackPlugin });
|
|
36
|
+
},
|
|
37
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "clack",
|
|
3
|
+
"channels": ["clack"],
|
|
4
|
+
"configSchema": {
|
|
5
|
+
"type": "object",
|
|
6
|
+
"additionalProperties": false,
|
|
7
|
+
"properties": {
|
|
8
|
+
"token": {
|
|
9
|
+
"type": "string",
|
|
10
|
+
"description": "Agent connection token from Clack"
|
|
11
|
+
},
|
|
12
|
+
"baseUrl": {
|
|
13
|
+
"type": "string",
|
|
14
|
+
"description": "Clack server URL"
|
|
15
|
+
},
|
|
16
|
+
"pollIntervalMs": {
|
|
17
|
+
"type": "number",
|
|
18
|
+
"default": 5000,
|
|
19
|
+
"description": "Message poll interval in milliseconds"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"required": ["token", "baseUrl"]
|
|
23
|
+
}
|
|
24
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@clackhq/openclaw-plugin",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "OpenClaw channel plugin for Clack workspaces - collaborate with humans and AI agents",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"scripts": {
|
|
9
|
+
"build": "tsc",
|
|
10
|
+
"dev": "tsc --watch",
|
|
11
|
+
"prepublishOnly": "npm run build"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"openclaw",
|
|
15
|
+
"clack",
|
|
16
|
+
"ai-agents",
|
|
17
|
+
"collaboration",
|
|
18
|
+
"slack-alternative"
|
|
19
|
+
],
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "https://github.com/Clack-HQ/clack.git",
|
|
23
|
+
"directory": "packages/openclaw-plugin"
|
|
24
|
+
},
|
|
25
|
+
"homepage": "https://clackhq.com",
|
|
26
|
+
"bugs": {
|
|
27
|
+
"url": "https://github.com/Clack-HQ/clack/issues"
|
|
28
|
+
},
|
|
29
|
+
"license": "MIT",
|
|
30
|
+
"author": "Clack <hello@clackhq.com>",
|
|
31
|
+
"peerDependencies": {
|
|
32
|
+
"openclaw": ">=2025.0.0"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"typescript": "^5.3.0",
|
|
36
|
+
"@types/node": "^20.0.0"
|
|
37
|
+
}
|
|
38
|
+
}
|
package/src/api.ts
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Clack API client for OpenClaw plugin
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export interface ClackConfig {
|
|
6
|
+
token: string;
|
|
7
|
+
baseUrl: string;
|
|
8
|
+
pollIntervalMs?: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface ClackChannel {
|
|
12
|
+
id: string;
|
|
13
|
+
name: string;
|
|
14
|
+
description: string | null;
|
|
15
|
+
type: "STANDARD" | "AGENT_COLLAB" | "DM";
|
|
16
|
+
involvement: "ACTIVE" | "MENTIONED" | "WATCHING" | "OFF";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface ClackMessage {
|
|
20
|
+
id: string;
|
|
21
|
+
content: string;
|
|
22
|
+
channelId: string;
|
|
23
|
+
channelName: string;
|
|
24
|
+
sender: {
|
|
25
|
+
type: "user" | "agent";
|
|
26
|
+
id: string;
|
|
27
|
+
name: string | null;
|
|
28
|
+
avatarUrl: string | null;
|
|
29
|
+
} | null;
|
|
30
|
+
createdAt: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface ClackAgent {
|
|
34
|
+
id: string;
|
|
35
|
+
name: string;
|
|
36
|
+
avatarUrl: string | null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface ClackOrg {
|
|
40
|
+
id: string;
|
|
41
|
+
name: string;
|
|
42
|
+
slug: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface ClackConnectionInfo {
|
|
46
|
+
agent: ClackAgent;
|
|
47
|
+
org: ClackOrg;
|
|
48
|
+
channels: ClackChannel[];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function isValidTokenFormat(token: string): boolean {
|
|
52
|
+
return /^clack_agent_[a-f0-9]{32}$/.test(token);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export class ClackApiClient {
|
|
56
|
+
private token: string;
|
|
57
|
+
private baseUrl: string;
|
|
58
|
+
private lastMessageTime: string | null = null;
|
|
59
|
+
|
|
60
|
+
public agent: ClackAgent | null = null;
|
|
61
|
+
public org: ClackOrg | null = null;
|
|
62
|
+
public channels: ClackChannel[] = [];
|
|
63
|
+
|
|
64
|
+
constructor(config: ClackConfig) {
|
|
65
|
+
this.token = config.token;
|
|
66
|
+
this.baseUrl = config.baseUrl.replace(/\/$/, "");
|
|
67
|
+
|
|
68
|
+
if (!isValidTokenFormat(this.token)) {
|
|
69
|
+
throw new Error(`Invalid Clack token format. Expected: clack_agent_<32 hex chars>`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
private async fetch<T>(path: string, options: RequestInit = {}): Promise<T> {
|
|
74
|
+
const url = `${this.baseUrl}${path}`;
|
|
75
|
+
const headers: Record<string, string> = {
|
|
76
|
+
"Content-Type": "application/json",
|
|
77
|
+
...(options.headers as Record<string, string>),
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
// Add auth header for authenticated requests (not connect POST)
|
|
81
|
+
if (path !== "/api/agent/connect" || options.method !== "POST") {
|
|
82
|
+
headers["Authorization"] = `Bearer ${this.token}`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const response = await fetch(url, { ...options, headers });
|
|
86
|
+
|
|
87
|
+
if (!response.ok) {
|
|
88
|
+
const errorData = await response.json().catch(() => ({ error: response.statusText })) as { error?: string };
|
|
89
|
+
throw new Error(errorData.error || `HTTP ${response.status}`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return response.json() as Promise<T>;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async connect(): Promise<ClackConnectionInfo> {
|
|
96
|
+
const data = await this.fetch<ClackConnectionInfo>("/api/agent/connect", {
|
|
97
|
+
method: "POST",
|
|
98
|
+
body: JSON.stringify({ token: this.token }),
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
this.agent = data.agent;
|
|
102
|
+
this.org = data.org;
|
|
103
|
+
this.channels = data.channels;
|
|
104
|
+
|
|
105
|
+
return data;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async disconnect(): Promise<void> {
|
|
109
|
+
await this.fetch<{ success: boolean }>("/api/agent/connect", { method: "DELETE" });
|
|
110
|
+
this.agent = null;
|
|
111
|
+
this.org = null;
|
|
112
|
+
this.channels = [];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async updateStatus(status: "ONLINE" | "OFFLINE" | "BUSY", activity?: string): Promise<void> {
|
|
116
|
+
await this.fetch<{ success: boolean }>("/api/agent/connect", {
|
|
117
|
+
method: "PUT",
|
|
118
|
+
body: JSON.stringify({ status, activity }),
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async pollMessages(channelId?: string): Promise<ClackMessage[]> {
|
|
123
|
+
const params = new URLSearchParams();
|
|
124
|
+
if (channelId) params.set("channelId", channelId);
|
|
125
|
+
if (this.lastMessageTime) params.set("after", this.lastMessageTime);
|
|
126
|
+
|
|
127
|
+
const query = params.toString();
|
|
128
|
+
const data = await this.fetch<{ messages: ClackMessage[] }>(
|
|
129
|
+
`/api/agent/messages${query ? `?${query}` : ""}`
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
// Update last message time for next poll
|
|
133
|
+
if (data.messages.length > 0) {
|
|
134
|
+
this.lastMessageTime = data.messages[data.messages.length - 1].createdAt;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return data.messages;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async sendMessage(channelId: string, content: string): Promise<ClackMessage> {
|
|
141
|
+
const data = await this.fetch<{ message: ClackMessage }>("/api/agent/messages", {
|
|
142
|
+
method: "POST",
|
|
143
|
+
body: JSON.stringify({ channelId, content }),
|
|
144
|
+
});
|
|
145
|
+
return data.message;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
getChannelByName(name: string): ClackChannel | undefined {
|
|
149
|
+
return this.channels.find((c) => c.name === name);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
getChannelById(id: string): ClackChannel | undefined {
|
|
153
|
+
return this.channels.find((c) => c.id === id);
|
|
154
|
+
}
|
|
155
|
+
}
|
package/src/channel.ts
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Clack Channel Plugin for OpenClaw
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { ClackApiClient, type ClackConfig } from "./api.js";
|
|
6
|
+
|
|
7
|
+
const DEFAULT_ACCOUNT_ID = "default";
|
|
8
|
+
|
|
9
|
+
// Resolved account configuration
|
|
10
|
+
export interface ResolvedClackAccount {
|
|
11
|
+
accountId: string;
|
|
12
|
+
name: string;
|
|
13
|
+
enabled: boolean;
|
|
14
|
+
configured: boolean;
|
|
15
|
+
baseUrl: string | null;
|
|
16
|
+
config: {
|
|
17
|
+
token?: string;
|
|
18
|
+
baseUrl?: string;
|
|
19
|
+
pollIntervalMs?: number;
|
|
20
|
+
allowFrom?: string[];
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Channel metadata
|
|
25
|
+
const meta = {
|
|
26
|
+
id: "clack",
|
|
27
|
+
label: "Clack",
|
|
28
|
+
selectionLabel: "Clack Workspace",
|
|
29
|
+
detailLabel: "Clack",
|
|
30
|
+
docsPath: "/channels/clack",
|
|
31
|
+
docsLabel: "clack",
|
|
32
|
+
blurb: "Collaborate with humans and other agents in Clack workspaces.",
|
|
33
|
+
systemImage: "person.3.fill",
|
|
34
|
+
aliases: [],
|
|
35
|
+
order: 50,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// Resolve account from config
|
|
39
|
+
function resolveClackAccount(params: {
|
|
40
|
+
cfg: Record<string, unknown>;
|
|
41
|
+
accountId?: string;
|
|
42
|
+
}): ResolvedClackAccount {
|
|
43
|
+
const { cfg, accountId = DEFAULT_ACCOUNT_ID } = params;
|
|
44
|
+
const channels = cfg.channels as Record<string, unknown> | undefined;
|
|
45
|
+
const channelConfig = channels?.clack as ClackConfig | undefined;
|
|
46
|
+
|
|
47
|
+
const token = channelConfig?.token;
|
|
48
|
+
const baseUrl = channelConfig?.baseUrl;
|
|
49
|
+
const configured = Boolean(token && baseUrl);
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
accountId,
|
|
53
|
+
name: "Clack",
|
|
54
|
+
enabled: Boolean(channelConfig && (channelConfig as { enabled?: boolean }).enabled !== false),
|
|
55
|
+
configured,
|
|
56
|
+
baseUrl: baseUrl || null,
|
|
57
|
+
config: {
|
|
58
|
+
token,
|
|
59
|
+
baseUrl,
|
|
60
|
+
pollIntervalMs: channelConfig?.pollIntervalMs ?? 5000,
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Store active clients per account
|
|
66
|
+
const activeClients = new Map<string, ClackApiClient>();
|
|
67
|
+
|
|
68
|
+
export const clackPlugin = {
|
|
69
|
+
id: "clack",
|
|
70
|
+
meta,
|
|
71
|
+
capabilities: {
|
|
72
|
+
chatTypes: ["group"],
|
|
73
|
+
media: false,
|
|
74
|
+
reactions: false,
|
|
75
|
+
edit: false,
|
|
76
|
+
unsend: false,
|
|
77
|
+
reply: false,
|
|
78
|
+
effects: false,
|
|
79
|
+
groupManagement: false,
|
|
80
|
+
},
|
|
81
|
+
reload: { configPrefixes: ["channels.clack"] },
|
|
82
|
+
config: {
|
|
83
|
+
listAccountIds: () => [DEFAULT_ACCOUNT_ID],
|
|
84
|
+
resolveAccount: (cfg: Record<string, unknown>, accountId?: string) =>
|
|
85
|
+
resolveClackAccount({ cfg, accountId }),
|
|
86
|
+
defaultAccountId: () => DEFAULT_ACCOUNT_ID,
|
|
87
|
+
isConfigured: (account: ResolvedClackAccount) => account.configured,
|
|
88
|
+
describeAccount: (account: ResolvedClackAccount) => ({
|
|
89
|
+
accountId: account.accountId,
|
|
90
|
+
name: account.name,
|
|
91
|
+
enabled: account.enabled,
|
|
92
|
+
configured: account.configured,
|
|
93
|
+
baseUrl: account.baseUrl,
|
|
94
|
+
}),
|
|
95
|
+
},
|
|
96
|
+
outbound: {
|
|
97
|
+
deliveryMode: "direct",
|
|
98
|
+
textChunkLimit: 4000,
|
|
99
|
+
resolveTarget: ({ to }: { to?: string }) => {
|
|
100
|
+
const trimmed = to?.trim();
|
|
101
|
+
if (!trimmed) {
|
|
102
|
+
return { ok: false, error: new Error("Clack requires --to <channel_id|channel_name>") };
|
|
103
|
+
}
|
|
104
|
+
return { ok: true, to: trimmed };
|
|
105
|
+
},
|
|
106
|
+
sendText: async (ctx: {
|
|
107
|
+
cfg: Record<string, unknown>;
|
|
108
|
+
to: string;
|
|
109
|
+
text: string;
|
|
110
|
+
accountId?: string
|
|
111
|
+
}) => {
|
|
112
|
+
const { cfg, to, text, accountId } = ctx;
|
|
113
|
+
const account = resolveClackAccount({ cfg, accountId });
|
|
114
|
+
|
|
115
|
+
if (!account.configured || !account.config.token || !account.config.baseUrl) {
|
|
116
|
+
throw new Error("Clack not configured - set channels.clack.token and channels.clack.baseUrl");
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
let client = activeClients.get(account.accountId);
|
|
120
|
+
if (!client) {
|
|
121
|
+
client = new ClackApiClient({
|
|
122
|
+
token: account.config.token,
|
|
123
|
+
baseUrl: account.config.baseUrl,
|
|
124
|
+
});
|
|
125
|
+
await client.connect();
|
|
126
|
+
activeClients.set(account.accountId, client);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Resolve channel (by name or ID)
|
|
130
|
+
let channelId = to;
|
|
131
|
+
const channelByName = client.getChannelByName(to);
|
|
132
|
+
if (channelByName) {
|
|
133
|
+
channelId = channelByName.id;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const message = await client.sendMessage(channelId, text);
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
channel: "clack",
|
|
140
|
+
ok: true,
|
|
141
|
+
messageId: message.id,
|
|
142
|
+
channelId: message.channelId,
|
|
143
|
+
};
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
gateway: {
|
|
147
|
+
startAccount: async (ctx: {
|
|
148
|
+
account: ResolvedClackAccount;
|
|
149
|
+
cfg: Record<string, unknown>;
|
|
150
|
+
abortSignal?: AbortSignal;
|
|
151
|
+
setStatus: (status: Record<string, unknown>) => void;
|
|
152
|
+
log?: { info: (msg: string) => void; error?: (msg: string) => void; debug?: (msg: string) => void };
|
|
153
|
+
}) => {
|
|
154
|
+
const { account, abortSignal, setStatus, log } = ctx;
|
|
155
|
+
|
|
156
|
+
if (!account.configured || !account.config.token || !account.config.baseUrl) {
|
|
157
|
+
throw new Error("Clack not configured");
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
log?.info(`[clack] Connecting to ${account.config.baseUrl}...`);
|
|
161
|
+
|
|
162
|
+
const client = new ClackApiClient({
|
|
163
|
+
token: account.config.token,
|
|
164
|
+
baseUrl: account.config.baseUrl,
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
const { agent, org, channels } = await client.connect();
|
|
168
|
+
activeClients.set(account.accountId, client);
|
|
169
|
+
|
|
170
|
+
log?.info(`[clack] Connected as ${agent.name} to ${org.name}`);
|
|
171
|
+
log?.info(`[clack] Channels: ${channels.map((c) => `#${c.name}`).join(", ")}`);
|
|
172
|
+
|
|
173
|
+
setStatus({
|
|
174
|
+
accountId: account.accountId,
|
|
175
|
+
running: true,
|
|
176
|
+
lastStartAt: new Date().toISOString(),
|
|
177
|
+
connected: true,
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
// Start polling for messages
|
|
181
|
+
const pollInterval = account.config.pollIntervalMs ?? 5000;
|
|
182
|
+
let polling = true;
|
|
183
|
+
|
|
184
|
+
const poll = async () => {
|
|
185
|
+
while (polling && !abortSignal?.aborted) {
|
|
186
|
+
try {
|
|
187
|
+
const messages = await client.pollMessages();
|
|
188
|
+
for (const msg of messages) {
|
|
189
|
+
log?.debug?.(`[clack] Received: [#${msg.channelName}] ${msg.sender?.name}: ${msg.content}`);
|
|
190
|
+
}
|
|
191
|
+
} catch (err) {
|
|
192
|
+
log?.error?.(`[clack] Poll error: ${err}`);
|
|
193
|
+
}
|
|
194
|
+
await new Promise((resolve) => setTimeout(resolve, pollInterval));
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
poll();
|
|
199
|
+
|
|
200
|
+
abortSignal?.addEventListener("abort", async () => {
|
|
201
|
+
polling = false;
|
|
202
|
+
try {
|
|
203
|
+
await client.disconnect();
|
|
204
|
+
activeClients.delete(account.accountId);
|
|
205
|
+
log?.info("[clack] Disconnected");
|
|
206
|
+
} catch (err) {
|
|
207
|
+
log?.error?.(`[clack] Disconnect error: ${err}`);
|
|
208
|
+
}
|
|
209
|
+
setStatus({
|
|
210
|
+
accountId: account.accountId,
|
|
211
|
+
running: false,
|
|
212
|
+
lastStopAt: new Date().toISOString(),
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
};
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @clackhq/openclaw-plugin
|
|
3
|
+
*
|
|
4
|
+
* OpenClaw channel plugin for Clack workspaces.
|
|
5
|
+
* Enables AI agents to collaborate with humans and other agents.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```yaml
|
|
9
|
+
* # gateway.yaml
|
|
10
|
+
* channels:
|
|
11
|
+
* clack:
|
|
12
|
+
* enabled: true
|
|
13
|
+
* token: "clack_agent_xxx"
|
|
14
|
+
* baseUrl: "https://clack-olive.vercel.app"
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
export { ClackApiClient, type ClackConfig, type ClackMessage, type ClackChannel } from "./api.js";
|
|
19
|
+
export { clackPlugin } from "./channel.js";
|
|
20
|
+
|
|
21
|
+
// Default export for OpenClaw plugin registration
|
|
22
|
+
import { clackPlugin } from "./channel.js";
|
|
23
|
+
export default {
|
|
24
|
+
id: "clack",
|
|
25
|
+
name: "Clack",
|
|
26
|
+
description: "Collaborate with humans and AI agents in Clack workspaces",
|
|
27
|
+
configSchema: {
|
|
28
|
+
type: "object" as const,
|
|
29
|
+
additionalProperties: false,
|
|
30
|
+
properties: {
|
|
31
|
+
token: { type: "string" as const, description: "Agent connection token from Clack" },
|
|
32
|
+
baseUrl: { type: "string" as const, description: "Clack server URL" },
|
|
33
|
+
pollIntervalMs: { type: "number" as const, description: "Message poll interval (ms)" },
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
register(api: { runtime: unknown; registerChannel: (opts: { plugin: unknown }) => void }) {
|
|
37
|
+
api.registerChannel({ plugin: clackPlugin });
|
|
38
|
+
},
|
|
39
|
+
};
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"lib": ["ES2022"],
|
|
7
|
+
"declaration": true,
|
|
8
|
+
"strict": true,
|
|
9
|
+
"noImplicitAny": true,
|
|
10
|
+
"strictNullChecks": true,
|
|
11
|
+
"esModuleInterop": true,
|
|
12
|
+
"outDir": "./dist",
|
|
13
|
+
"rootDir": "./src",
|
|
14
|
+
"skipLibCheck": true
|
|
15
|
+
},
|
|
16
|
+
"include": ["src/**/*"],
|
|
17
|
+
"exclude": ["node_modules", "dist"]
|
|
18
|
+
}
|