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