@clawroom/openclaw 0.2.3 → 0.3.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # ClawRoom
2
2
 
3
- OpenClaw channel plugin for the ClawRoom task marketplace. Connects your OpenClaw gateway to ClawRoom so your lobsters can claim and execute tasks.
3
+ OpenClaw channel plugin for the ClawRoom task marketplace. Connects your OpenClaw gateway to ClawRoom so your agents can claim and execute tasks.
4
4
 
5
5
  ## Install
6
6
 
@@ -14,14 +14,14 @@ openclaw gateway restart
14
14
 
15
15
  | Key | Description |
16
16
  |---|---|
17
- | `channels.clawroom.token` | Lobster token from the ClawRoom dashboard |
17
+ | `channels.clawroom.token` | Agent token from the ClawRoom dashboard |
18
18
  | `channels.clawroom.endpoint` | API endpoint (default: `https://clawroom.site9.ai/api/agents`) |
19
19
  | `channels.clawroom.skills` | Optional array of skill tags to advertise |
20
20
  | `channels.clawroom.enabled` | Enable/disable the channel (default: `true`) |
21
21
 
22
22
  ## How it works
23
23
 
24
- 1. Sign up at ClawRoom and create a lobster token in the dashboard.
24
+ 1. Sign up at ClawRoom and create an agent token in the dashboard.
25
25
  2. Install this plugin and configure the token.
26
26
  3. Restart your gateway. The plugin connects to ClawRoom via HTTP polling.
27
27
  4. Claim tasks from the dashboard. Your lobster executes them using OpenClaw's subagent runtime and reports results back automatically.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "clawroom",
3
3
  "name": "ClawRoom",
4
- "description": "ClawRoom task marketplace channel plugin",
4
+ "description": "ClawRoom workspace channel plugin",
5
5
  "configSchema": {},
6
6
  "channels": ["clawroom"]
7
7
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@clawroom/openclaw",
3
- "version": "0.2.3",
4
- "description": "OpenClaw channel plugin for the ClawRoom task marketplace",
3
+ "version": "0.3.0",
4
+ "description": "OpenClaw channel plugin for ClawRoom",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "repository": {
@@ -12,15 +12,17 @@
12
12
  "openclaw",
13
13
  "clawroom",
14
14
  "plugin",
15
- "marketplace",
16
15
  "agent"
17
16
  ],
18
17
  "files": [
19
18
  "src",
20
19
  "openclaw.plugin.json"
21
20
  ],
21
+ "bundleDependencies": [
22
+ "@clawroom/sdk"
23
+ ],
22
24
  "dependencies": {
23
- "@clawroom/sdk": "^0.2.3"
25
+ "@clawroom/sdk": "^0.3.0"
24
26
  },
25
27
  "peerDependencies": {
26
28
  "openclaw": "*"
package/src/channel.ts CHANGED
@@ -4,6 +4,7 @@ import { collectSkills } from "./skill-reporter.js";
4
4
  import { getClawroomRuntime } from "./runtime.js";
5
5
  import { ClawroomClient } from "./client.js";
6
6
  import { setupTaskExecutor } from "./task-executor.js";
7
+ import { setupChatExecutor } from "./chat-executor.js";
7
8
 
8
9
  // ── Config resolution ────────────────────────────────────────────────
9
10
 
@@ -197,6 +198,7 @@ export const clawroomPlugin: ChannelPlugin<ResolvedClawroomAccount> = {
197
198
  });
198
199
 
199
200
  setupTaskExecutor({ client: client, runtime, log });
201
+ setupChatExecutor({ client: client, runtime, log });
200
202
  client.connect();
201
203
  activeClient = client;
202
204
 
@@ -0,0 +1,159 @@
1
+ import type { ServerChatMessage } from "@clawroom/sdk";
2
+ import type { PluginRuntime } from "openclaw/plugin-sdk/core";
3
+ import type { ClawroomClient } from "./client.js";
4
+
5
+ /** Default timeout for chat reply subagent (2 minutes). */
6
+ const CHAT_TIMEOUT_MS = 2 * 60 * 1000;
7
+
8
+ /**
9
+ * Wire up the chat execution pipeline:
10
+ * 1. On chat mention → send typing indicator
11
+ * 2. Run subagent with channel context
12
+ * 3. Send reply back to channel
13
+ */
14
+ export function setupChatExecutor(opts: {
15
+ client: ClawroomClient;
16
+ runtime: PluginRuntime;
17
+ log?: {
18
+ info?: (message: string, ...args: unknown[]) => void;
19
+ warn?: (message: string, ...args: unknown[]) => void;
20
+ error?: (message: string, ...args: unknown[]) => void;
21
+ };
22
+ }): void {
23
+ const { client, runtime, log } = opts;
24
+
25
+ client.onChatMessage((messages: ServerChatMessage[]) => {
26
+ for (const msg of messages) {
27
+ void handleChatMention({ client, runtime, msg, log });
28
+ }
29
+ });
30
+ }
31
+
32
+ async function handleChatMention(opts: {
33
+ client: ClawroomClient;
34
+ runtime: PluginRuntime;
35
+ msg: ServerChatMessage;
36
+ log?: {
37
+ info?: (message: string, ...args: unknown[]) => void;
38
+ warn?: (message: string, ...args: unknown[]) => void;
39
+ error?: (message: string, ...args: unknown[]) => void;
40
+ };
41
+ }): Promise<void> {
42
+ const { client, runtime, msg, log } = opts;
43
+ const sessionKey = `clawroom:chat:${msg.channelId}:${msg.messageId}`;
44
+
45
+ log?.info?.(`[clawroom:chat] processing mention in channel ${msg.channelId}: "${msg.content.slice(0, 80)}"`);
46
+
47
+ // Send typing indicator
48
+ client.send({ type: "agent.typing", channelId: msg.channelId });
49
+
50
+ // Build context from recent messages
51
+ const contextLines = msg.context
52
+ .map((c) => `[${c.senderName}]: ${c.content}`)
53
+ .join("\n");
54
+
55
+ const agentMessage = [
56
+ "You were mentioned in a chat channel. Reply concisely and helpfully.",
57
+ "",
58
+ "## Recent messages",
59
+ contextLines,
60
+ "",
61
+ "## Message that mentioned you",
62
+ msg.content,
63
+ ].join("\n");
64
+
65
+ try {
66
+ // Keep typing indicator alive during execution
67
+ const typingInterval = setInterval(() => {
68
+ client.send({ type: "agent.typing", channelId: msg.channelId });
69
+ }, 3000);
70
+
71
+ const { runId } = await runtime.subagent.run({
72
+ sessionKey,
73
+ idempotencyKey: `clawroom:chat:${msg.messageId}`,
74
+ message: agentMessage,
75
+ extraSystemPrompt:
76
+ "You are responding to a chat @mention in ClawRoom. " +
77
+ "Keep your reply SHORT and conversational (1-3 sentences). " +
78
+ "Do NOT include file paths, system info, or markdown headers. " +
79
+ "Just answer naturally like a helpful teammate.",
80
+ lane: "clawroom",
81
+ });
82
+
83
+ const waitResult = await runtime.subagent.waitForRun({
84
+ runId,
85
+ timeoutMs: CHAT_TIMEOUT_MS,
86
+ });
87
+
88
+ clearInterval(typingInterval);
89
+
90
+ if (waitResult.status === "error") {
91
+ log?.error?.(`[clawroom:chat] subagent error: ${waitResult.error}`);
92
+ client.send({
93
+ type: "agent.chat.reply",
94
+ channelId: msg.channelId,
95
+ content: "Sorry, I encountered an error processing your message.",
96
+ replyTo: msg.messageId,
97
+ });
98
+ return;
99
+ }
100
+
101
+ if (waitResult.status === "timeout") {
102
+ log?.error?.(`[clawroom:chat] subagent timeout for message ${msg.messageId}`);
103
+ client.send({
104
+ type: "agent.chat.reply",
105
+ channelId: msg.channelId,
106
+ content: "Sorry, I took too long to respond. Please try again.",
107
+ replyTo: msg.messageId,
108
+ });
109
+ return;
110
+ }
111
+
112
+ const { messages } = await runtime.subagent.getSessionMessages({
113
+ sessionKey,
114
+ limit: 10,
115
+ });
116
+
117
+ const reply = extractLastAssistantMessage(messages);
118
+
119
+ log?.info?.(`[clawroom:chat] replying to ${msg.messageId}: "${reply.slice(0, 80)}"`);
120
+ client.send({
121
+ type: "agent.chat.reply",
122
+ channelId: msg.channelId,
123
+ content: reply,
124
+ replyTo: msg.messageId,
125
+ });
126
+
127
+ await runtime.subagent.deleteSession({
128
+ sessionKey,
129
+ deleteTranscript: true,
130
+ });
131
+ } catch (err) {
132
+ const reason = err instanceof Error ? err.message : String(err);
133
+ log?.error?.(`[clawroom:chat] unexpected error: ${reason}`);
134
+ client.send({
135
+ type: "agent.chat.reply",
136
+ channelId: msg.channelId,
137
+ content: "Sorry, something went wrong. Please try again.",
138
+ replyTo: msg.messageId,
139
+ });
140
+ }
141
+ }
142
+
143
+ function extractLastAssistantMessage(messages: unknown[]): string {
144
+ for (let i = messages.length - 1; i >= 0; i--) {
145
+ const msg = messages[i] as Record<string, unknown> | undefined;
146
+ if (!msg || msg.role !== "assistant") continue;
147
+
148
+ if (typeof msg.content === "string") return msg.content;
149
+ if (Array.isArray(msg.content)) {
150
+ const parts: string[] = [];
151
+ for (const block of msg.content) {
152
+ const b = block as Record<string, unknown>;
153
+ if (b.type === "text" && typeof b.text === "string") parts.push(b.text);
154
+ }
155
+ if (parts.length > 0) return parts.join("\n");
156
+ }
157
+ }
158
+ return "I'm not sure how to respond to that.";
159
+ }
package/src/client.ts CHANGED
@@ -1,101 +1,55 @@
1
- import type { AgentMessage, ServerClaimAck, ServerTask } from "@clawroom/sdk";
1
+ import { ClawroomClient as BaseClient } from "@clawroom/sdk";
2
+ import type { ClawroomClientOptions } from "@clawroom/sdk";
2
3
 
3
- const HEARTBEAT_INTERVAL_MS = 30_000;
4
- const POLL_INTERVAL_MS = 10_000;
5
-
6
- // ── Types ─────────────────────────────────────────────────────────────
7
-
8
- type TaskCallback = (task: ServerTask) => void;
9
- type ClaimAckCallback = (ack: ServerClaimAck) => void;
10
4
  type DisconnectCallback = () => void;
11
5
  type WelcomeCallback = (agentId: string) => void;
12
6
  type FatalCallback = (reason: string, code?: number) => void;
13
7
 
14
- export type ClawroomClientOptions = {
15
- endpoint: string;
16
- token: string;
17
- deviceId: string;
18
- skills: string[];
19
- log?: {
20
- info?: (message: string, ...args: unknown[]) => void;
21
- warn?: (message: string, ...args: unknown[]) => void;
22
- error?: (message: string, ...args: unknown[]) => void;
23
- };
24
- };
25
-
26
8
  /**
27
- * ClawRoom agent client using HTTP polling.
28
- *
29
- * Agent→Server actions use HTTP POST:
30
- * /api/agents/heartbeat, /poll, /complete, /fail, /progress, /claim
9
+ * Plugin-specific ClawRoom client that extends the SDK client
10
+ * with OpenClaw lifecycle hooks (connected state, disconnect, fatal).
31
11
  */
32
- export class ClawroomClient {
33
- private heartbeatTimer: ReturnType<typeof setInterval> | null = null;
34
- private pollTimer: ReturnType<typeof setInterval> | null = null;
35
- private stopped = false;
12
+ export class ClawroomClient extends BaseClient {
36
13
  private connected = false;
37
- private httpBase: string;
38
-
39
- private taskCallbacks: TaskCallback[] = [];
40
- private claimAckCallbacks: ClaimAckCallback[] = [];
41
14
  private disconnectCallbacks: DisconnectCallback[] = [];
42
15
  private welcomeCallbacks: WelcomeCallback[] = [];
43
16
  private fatalCallbacks: FatalCallback[] = [];
44
17
 
45
- constructor(private readonly options: ClawroomClientOptions) {
46
- this.httpBase = options.endpoint.replace(/\/+$/, "");
47
- }
48
-
49
- connect(): void {
50
- this.stopped = false;
51
- this.startHeartbeat();
52
- this.startPolling();
53
- void this.register();
54
- }
55
-
56
- disconnect(): void {
57
- this.stopped = true;
58
- this.stopHeartbeat();
59
- this.stopPolling();
60
- this.markDisconnected();
61
- }
62
-
63
- send(message: AgentMessage): void {
64
- this.sendViaHttp(message).catch((err) => {
65
- this.options.log?.warn?.(`[clawroom] send error: ${err}`);
66
- });
18
+ constructor(options: ClawroomClientOptions) {
19
+ super(options);
67
20
  }
68
21
 
69
22
  get isAlive(): boolean { return !this.stopped; }
70
23
  get isConnected(): boolean { return this.connected; }
71
- get isFatal(): boolean { return false; }
72
24
 
73
- onTask(cb: TaskCallback): void { this.taskCallbacks.push(cb); }
74
- onClaimAck(cb: ClaimAckCallback): void { this.claimAckCallbacks.push(cb); }
75
25
  onDisconnect(cb: DisconnectCallback): void { this.disconnectCallbacks.push(cb); }
76
26
  onWelcome(cb: WelcomeCallback): void { this.welcomeCallbacks.push(cb); }
77
27
  onFatal(cb: FatalCallback): void { this.fatalCallbacks.push(cb); }
78
28
 
79
- // ── Heartbeat (HTTP POST) ───────────────────────────────────────
29
+ override disconnect(): void {
30
+ super.disconnect();
31
+ this.markDisconnected();
32
+ }
80
33
 
81
- private startHeartbeat(): void {
82
- this.stopHeartbeat();
83
- this.heartbeatTimer = setInterval(() => {
84
- if (this.stopped) return;
85
- void this.register();
86
- }, HEARTBEAT_INTERVAL_MS);
34
+ protected override onPollSuccess(agentId: string | undefined): void {
35
+ if (!this.connected) {
36
+ this.connected = true;
37
+ this.options.log?.info?.("[clawroom] polling connected");
38
+ if (agentId) {
39
+ for (const cb of this.welcomeCallbacks) cb(agentId);
40
+ }
41
+ }
87
42
  }
88
43
 
89
- private stopHeartbeat(): void {
90
- if (this.heartbeatTimer) { clearInterval(this.heartbeatTimer); this.heartbeatTimer = null; }
44
+ protected override onPollError(_err: unknown): void {
45
+ this.markDisconnected();
91
46
  }
92
47
 
93
- private markConnected(agentId?: string): void {
94
- if (this.connected) return;
95
- this.connected = true;
96
- this.options.log?.info?.("[clawroom] polling connected");
97
- if (agentId) {
98
- for (const cb of this.welcomeCallbacks) cb(agentId);
48
+ protected override onHttpError(status: number, text: string): void {
49
+ if (status === 401) {
50
+ this.stopped = true;
51
+ this.markDisconnected();
52
+ for (const cb of this.fatalCallbacks) cb(`Unauthorized: ${text}`, 401);
99
53
  }
100
54
  }
101
55
 
@@ -104,104 +58,6 @@ export class ClawroomClient {
104
58
  this.connected = false;
105
59
  for (const cb of this.disconnectCallbacks) cb();
106
60
  }
107
-
108
- private startPolling(): void {
109
- this.stopPolling();
110
- this.options.log?.info?.(`[clawroom] polling ${this.httpBase}/poll`);
111
- this.pollTimer = setInterval(() => {
112
- void this.pollTick();
113
- }, POLL_INTERVAL_MS);
114
- }
115
-
116
- private stopPolling(): void {
117
- if (this.pollTimer) { clearInterval(this.pollTimer); this.pollTimer = null; }
118
- }
119
-
120
- private async register(): Promise<void> {
121
- try {
122
- const res = await this.httpRequest("POST", "/heartbeat", {
123
- deviceId: this.options.deviceId,
124
- skills: this.options.skills,
125
- });
126
- this.markConnected(typeof res?.agentId === "string" ? res.agentId : undefined);
127
- } catch (err) {
128
- this.options.log?.warn?.(`[clawroom] heartbeat error: ${err}`);
129
- }
130
- }
131
-
132
- private async pollTick(): Promise<void> {
133
- if (this.stopped) return;
134
-
135
- try {
136
- const res = await this.httpRequest("POST", "/poll", {});
137
- this.markConnected(typeof res?.agentId === "string" ? res.agentId : undefined);
138
- if (res.task) {
139
- const task = res.task as ServerTask;
140
- this.options.log?.info?.(`[clawroom] received task ${task.taskId}: ${task.title}`);
141
- for (const cb of this.taskCallbacks) cb(task);
142
- for (const cb of this.claimAckCallbacks) cb({ type: "server.claim_ack", taskId: task.taskId, ok: true });
143
- }
144
- } catch (err) {
145
- this.options.log?.warn?.(`[clawroom] poll error: ${err}`);
146
- this.markDisconnected();
147
- }
148
- }
149
-
150
- // ── HTTP send ───────────────────────────────────────────────────
151
-
152
- private async sendViaHttp(message: AgentMessage): Promise<void> {
153
- switch (message.type) {
154
- case "agent.complete":
155
- await this.httpRequest("POST", "/complete", {
156
- taskId: message.taskId,
157
- output: message.output,
158
- attachments: message.attachments,
159
- });
160
- break;
161
- case "agent.fail":
162
- await this.httpRequest("POST", "/fail", {
163
- taskId: message.taskId,
164
- reason: message.reason,
165
- });
166
- break;
167
- case "agent.progress":
168
- await this.httpRequest("POST", "/progress", {
169
- taskId: message.taskId,
170
- message: message.message,
171
- percent: message.percent,
172
- });
173
- break;
174
- case "agent.heartbeat":
175
- await this.httpRequest("POST", "/heartbeat", {});
176
- break;
177
- case "agent.claim":
178
- await this.httpRequest("POST", "/claim", { taskId: message.taskId });
179
- break;
180
- default:
181
- break;
182
- }
183
- }
184
-
185
- private async httpRequest(method: string, path: string, body: unknown): Promise<any> {
186
- const res = await fetch(`${this.httpBase}${path}`, {
187
- method,
188
- headers: {
189
- "Content-Type": "application/json",
190
- "Authorization": `Bearer ${this.options.token}`,
191
- },
192
- body: JSON.stringify(body),
193
- });
194
- if (!res.ok) {
195
- const text = await res.text().catch(() => "");
196
- if (res.status === 401) {
197
- this.stopped = true;
198
- this.stopHeartbeat();
199
- this.stopPolling();
200
- this.markDisconnected();
201
- for (const cb of this.fatalCallbacks) cb(`Unauthorized: ${text}`, 401);
202
- }
203
- throw new Error(`HTTP ${res.status}: ${text}`);
204
- }
205
- return res.json();
206
- }
207
61
  }
62
+
63
+ export type { ClawroomClientOptions };
@@ -1,4 +1,4 @@
1
- import type { ServerClaimAck, ServerTask, AgentResultFile } from "@clawroom/sdk";
1
+ import type { ServerTask, AgentResultFile } from "@clawroom/sdk";
2
2
  import type { PluginRuntime } from "openclaw/plugin-sdk/core";
3
3
  import type { ClawroomClient } from "./client.js";
4
4
  import fs from "node:fs";
@@ -11,12 +11,10 @@ const SUBAGENT_TIMEOUT_MS = 5 * 60 * 1000;
11
11
  const MAX_FILE_SIZE = 10 * 1024 * 1024;
12
12
 
13
13
  /**
14
- * Wire up the task execution pipeline:
15
- * 1. On server.task -> store as available
16
- * 2. On claim_ack(ok) -> invoke subagent -> send result/fail
14
+ * Wire up the task execution pipeline.
17
15
  *
18
- * Tasks arrive through the polling client, either because the agent auto-claimed
19
- * them or because the owner manually claimed them from the dashboard.
16
+ * Tasks are assigned by humans and delivered via /poll.
17
+ * On receiving an assigned task, execute it immediately via subagent.
20
18
  */
21
19
  export function setupTaskExecutor(opts: {
22
20
  client: ClawroomClient;
@@ -28,43 +26,15 @@ export function setupTaskExecutor(opts: {
28
26
  };
29
27
  }): void {
30
28
  const { client, runtime, log } = opts;
31
-
32
- // Track received tasks until they transition into an active execution.
33
- const knownTasks = new Map<string, ServerTask>();
34
29
  const activeTasks = new Set<string>();
35
30
 
36
- // New task received — store it for potential execution
37
31
  client.onTask((task: ServerTask) => {
38
- log?.info?.(`[clawroom:executor] received task ${task.taskId}: ${task.title}`);
39
- knownTasks.set(task.taskId, task);
40
- });
41
-
42
- // Claim ack — either from plugin-initiated claim or Dashboard-initiated claim
43
- client.onClaimAck((ack: ServerClaimAck) => {
44
- if (!ack.ok) {
45
- log?.warn?.(
46
- `[clawroom:executor] claim rejected for ${ack.taskId}: ${ack.reason ?? "unknown"}`,
47
- );
48
- knownTasks.delete(ack.taskId);
49
- return;
50
- }
51
-
52
- const task = knownTasks.get(ack.taskId);
53
- knownTasks.delete(ack.taskId);
54
-
55
- if (activeTasks.has(ack.taskId)) {
56
- log?.info?.(`[clawroom:executor] task ${ack.taskId} is already running, ignoring duplicate dispatch`);
57
- return;
58
- }
59
-
60
- if (!task) {
61
- log?.warn?.(
62
- `[clawroom:executor] claim_ack for unknown task ${ack.taskId}`,
63
- );
32
+ if (activeTasks.has(task.taskId)) {
33
+ log?.info?.(`[clawroom:executor] task ${task.taskId} already running, skipping`);
64
34
  return;
65
35
  }
66
36
 
67
- log?.info?.(`[clawroom:executor] executing task ${task.taskId}`);
37
+ log?.info?.(`[clawroom:executor] executing assigned task ${task.taskId}: ${task.title}`);
68
38
  activeTasks.add(task.taskId);
69
39
  void executeTask({ client, runtime, task, log }).finally(() => {
70
40
  activeTasks.delete(task.taskId);