@clawroom/openclaw 0.5.0 → 0.5.19

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/src/channel.ts CHANGED
@@ -1,19 +1,18 @@
1
1
  import type { ChannelPlugin } from "openclaw/plugin-sdk/core";
2
2
  import type { OpenClawConfig } from "openclaw/plugin-sdk/core";
3
+ import { ClawroomClient } from "@clawroom/sdk";
3
4
  // import { collectSkills } from "./skill-reporter.js";
4
5
  import { getClawroomRuntime } from "./runtime.js";
5
- import { ClawroomPluginClient } from "./client.js";
6
6
  import { setupTaskExecutor } from "./task-executor.js";
7
7
  import { setupChatExecutor } from "./chat-executor.js";
8
8
 
9
9
  // ── Config resolution ────────────────────────────────────────────────
10
10
 
11
- const DEFAULT_ENDPOINT = "https://clawroom.site9.ai/api/machines";
11
+ const DEFAULT_ENDPOINT = "https://clawroom.site9.ai/api/agents";
12
12
  const DEFAULT_ACCOUNT_ID = "default";
13
13
 
14
14
  interface ClawroomAccountConfig {
15
- api_key?: string;
16
- token?: string; // deprecated, fallback
15
+ token?: string;
17
16
  endpoint?: string;
18
17
  skills?: string[];
19
18
  enabled?: boolean;
@@ -24,7 +23,7 @@ interface ResolvedClawroomAccount {
24
23
  name: string;
25
24
  enabled: boolean;
26
25
  configured: boolean;
27
- apiKey: string;
26
+ token: string;
28
27
  endpoint: string;
29
28
  skills: string[];
30
29
  }
@@ -40,7 +39,7 @@ function resolveClawroomAccount(opts: {
40
39
  accountId?: string | null;
41
40
  }): ResolvedClawroomAccount {
42
41
  const section = readClawroomSection(opts.cfg);
43
- const apiKey = section.api_key ?? section.token ?? "";
42
+ const token = section.token ?? "";
44
43
  const endpoint = section.endpoint || DEFAULT_ENDPOINT;
45
44
  const skills = Array.isArray(section.skills) ? section.skills : [];
46
45
  const enabled = section.enabled !== false;
@@ -49,8 +48,8 @@ function resolveClawroomAccount(opts: {
49
48
  accountId: opts.accountId ?? DEFAULT_ACCOUNT_ID,
50
49
  name: "ClawRoom",
51
50
  enabled,
52
- configured: apiKey.length > 0,
53
- apiKey,
51
+ configured: token.length > 0,
52
+ token,
54
53
  endpoint,
55
54
  skills,
56
55
  };
@@ -58,7 +57,7 @@ function resolveClawroomAccount(opts: {
58
57
 
59
58
  // ── Persistent client per gateway lifecycle ───────────────────────
60
59
 
61
- let activeClient: ClawroomPluginClient | null = null;
60
+ let activeClient: ClawroomClient | null = null;
62
61
 
63
62
  // ── Channel plugin definition ────────────────────────────────────────
64
63
 
@@ -105,9 +104,10 @@ export const clawroomPlugin: ChannelPlugin<ResolvedClawroomAccount> = {
105
104
  deliveryMode: "direct",
106
105
 
107
106
  sendText: async ({ to, text }) => {
107
+ void text;
108
108
  // Outbound path — task results are sent directly by executors,
109
- // this is a fallback if openclaw routing triggers outbound delivery.
110
- // No-op for machine client (executors handle it).
109
+ // this is a fallback if OpenClaw routing triggers outbound delivery.
110
+ // Direct agent delivery is handled by the executors.
111
111
  return { channel: "clawroom", messageId: to, to };
112
112
  },
113
113
  },
@@ -118,16 +118,19 @@ export const clawroomPlugin: ChannelPlugin<ResolvedClawroomAccount> = {
118
118
 
119
119
  if (!account.configured) {
120
120
  throw new Error(
121
- "ClawRoom is not configured: set channels.clawroom.api_key in your OpenClaw config.",
121
+ "ClawRoom is not configured: set channels.clawroom.token in your OpenClaw config.",
122
122
  );
123
123
  }
124
124
 
125
125
  const runtime = getClawroomRuntime();
126
126
  const log = ctx.log ?? undefined;
127
127
 
128
- const client = new ClawroomPluginClient({
128
+ const client = new ClawroomClient({
129
129
  endpoint: account.endpoint,
130
- apiKey: account.apiKey,
130
+ token: account.token,
131
+ deviceId: `openclaw:${account.accountId}`,
132
+ skills: account.skills,
133
+ kind: "openclaw",
131
134
  log,
132
135
  });
133
136
 
@@ -155,40 +158,22 @@ export const clawroomPlugin: ChannelPlugin<ResolvedClawroomAccount> = {
155
158
  });
156
159
  };
157
160
 
158
- // Wire up polling events to OpenClaw health status
159
- client.onWelcome(() => publishConnected());
160
- client.onDisconnect(() => publishDisconnected());
161
- client.onFatal((reason) => {
162
- log?.error?.(`[clawroom] fatal error: ${reason}`);
163
- ctx.setStatus({
164
- accountId: account.accountId,
165
- running: false,
166
- connected: false,
167
- lastStopAt: Date.now(),
168
- lastStartAt: null,
169
- lastError: `Fatal: ${reason}`,
170
- });
171
- });
161
+ client.onConnected(() => publishConnected());
162
+ client.onDisconnected(() => publishDisconnected());
172
163
 
164
+ if (activeClient && activeClient !== client) {
165
+ activeClient.disconnect();
166
+ }
173
167
  setupTaskExecutor({ client: client, runtime, log });
174
168
  setupChatExecutor({ client: client, runtime, log });
175
169
  client.connect();
176
170
  activeClient = client;
177
171
 
178
- publishDisconnected("connecting via HTTP polling...");
179
-
180
- // Health check: if client somehow stopped, restart it.
181
- const healthCheck = setInterval(() => {
182
- if (!client.isAlive) {
183
- log?.warn?.("[clawroom] client died unexpectedly, restarting...");
184
- client.connect();
185
- }
186
- }, 30_000);
172
+ publishDisconnected("connecting via agent polling...");
187
173
 
188
174
  // Keep alive until gateway shuts down — only then disconnect
189
175
  await new Promise<void>((resolve) => {
190
176
  ctx.abortSignal.addEventListener("abort", () => {
191
- clearInterval(healthCheck);
192
177
  client.disconnect();
193
178
  activeClient = null;
194
179
  ctx.setStatus({
@@ -235,24 +220,3 @@ export const clawroomPlugin: ChannelPlugin<ResolvedClawroomAccount> = {
235
220
  }),
236
221
  },
237
222
  };
238
-
239
- // ── Helpers ──────────────────────────────────────────────────────────
240
-
241
- /**
242
- * Build a stable device identifier. We use the machine hostname from the
243
- * runtime environment when available, falling back to a random id.
244
- */
245
- function resolveDeviceId(ctx: {
246
- runtime?: unknown;
247
- }): string {
248
- // The RuntimeEnv may expose hostname or machineId depending on version
249
- const r = ctx.runtime as Record<string, unknown>;
250
- if (typeof r.machineId === "string" && r.machineId) {
251
- return r.machineId;
252
- }
253
- if (typeof r.hostname === "string" && r.hostname) {
254
- return r.hostname;
255
- }
256
- // Fallback: random id for this session
257
- return `agent-${Math.random().toString(36).slice(2, 10)}`;
258
- }
@@ -1,5 +1,6 @@
1
1
  import type { PluginRuntime } from "openclaw/plugin-sdk/core";
2
- import type { ClawroomPluginClient } from "./client.js";
2
+ import type { AgentChatProfile, ClawroomClient, ServerChatMessage } from "@clawroom/sdk";
3
+ import { extractToolNames, reportPluginReflectionSoon } from "./reflections.js";
3
4
 
4
5
  /** Default timeout for chat reply subagent (2 minutes). */
5
6
  const CHAT_TIMEOUT_MS = 2 * 60 * 1000;
@@ -11,7 +12,7 @@ const CHAT_TIMEOUT_MS = 2 * 60 * 1000;
11
12
  * 3. Send reply back to channel
12
13
  */
13
14
  export function setupChatExecutor(opts: {
14
- client: ClawroomPluginClient;
15
+ client: ClawroomClient;
15
16
  runtime: PluginRuntime;
16
17
  log?: {
17
18
  info?: (message: string, ...args: unknown[]) => void;
@@ -20,8 +21,27 @@ export function setupChatExecutor(opts: {
20
21
  };
21
22
  }): void {
22
23
  const { client, runtime, log } = opts;
24
+ const pendingMessages: ServerChatMessage[] = [];
23
25
 
24
- client.onAgentChat((agentId: string, messages: any[]) => {
26
+ const flushPending = (agentId: string) => {
27
+ if (pendingMessages.length === 0) return;
28
+ const queued = pendingMessages.splice(0, pendingMessages.length);
29
+ for (const msg of queued) {
30
+ void handleChatMention({ client, runtime, msg, agentId, log });
31
+ }
32
+ };
33
+
34
+ client.onConnected((agentId) => {
35
+ flushPending(agentId);
36
+ });
37
+
38
+ client.onChatMessage((messages: ServerChatMessage[]) => {
39
+ const agentId = client.agentId;
40
+ if (!agentId) {
41
+ log?.warn?.("[clawroom:chat] received chat before agent registration completed");
42
+ pendingMessages.push(...messages);
43
+ return;
44
+ }
25
45
  for (const msg of messages) {
26
46
  void handleChatMention({ client, runtime, msg, agentId, log });
27
47
  }
@@ -29,9 +49,9 @@ export function setupChatExecutor(opts: {
29
49
  }
30
50
 
31
51
  async function handleChatMention(opts: {
32
- client: ClawroomPluginClient;
52
+ client: ClawroomClient;
33
53
  runtime: PluginRuntime;
34
- msg: any;
54
+ msg: ServerChatMessage;
35
55
  agentId: string;
36
56
  log?: {
37
57
  info?: (message: string, ...args: unknown[]) => void;
@@ -45,31 +65,72 @@ async function handleChatMention(opts: {
45
65
  log?.info?.(`[clawroom:chat] processing mention in channel ${msg.channelId}: "${msg.content.slice(0, 80)}"`);
46
66
 
47
67
  // Send typing indicator
48
- client.sendTyping(agentId, msg.channelId).catch(() => {});
68
+ client.sendTyping(msg.channelId).catch(() => {});
49
69
 
50
70
  // Build context from recent messages
51
- const contextLines = msg.context
52
- .map((c) => `[${c.senderName}]: ${c.content}`)
71
+ const contextLines = (msg.context ?? [])
72
+ .map((c: { senderName: string; content: string }) => `[${c.senderName}]: ${c.content}`)
53
73
  .join("\n");
74
+ const attachmentLines = msg.attachments && msg.attachments.length > 0
75
+ ? msg.attachments.map((attachment: { filename: string; mimeType: string; byteSize?: number; downloadUrl: string }) => {
76
+ const size = typeof attachment.byteSize === "number" ? ` (${attachment.byteSize} bytes)` : "";
77
+ return `- ${attachment.filename} [${attachment.mimeType}]${size} ${attachment.downloadUrl}`;
78
+ }).join("\n")
79
+ : "(none)";
80
+ const agentProfileBlock = formatAgentProfile(msg.agentProfile);
54
81
 
55
- const isMention = (msg as any).isMention ?? false;
82
+ const isMention = msg.isMention ?? false;
83
+ const isFollowThroughWake = msg.wakeReason === "follow_through";
84
+ const isGoalDriftWake = msg.wakeReason === "goal_drift";
85
+ const isTriggerWake = msg.wakeReason === "trigger";
86
+ const isCoordinationWake = msg.wakeReason === "coordination";
56
87
  const agentMessage = [
57
88
  isMention
58
89
  ? "You were @mentioned in a chat channel. Reply directly and helpfully."
90
+ : isCoordinationWake
91
+ ? `You were woken for persisted coordination on an active tracked mission.${msg.triggerReason ? ` ${msg.triggerReason}` : ""}`
92
+ : isFollowThroughWake
93
+ ? `Your follow-through engine woke you up because an open loop needs attention.${msg.triggerReason ? ` ${msg.triggerReason}` : ""}`
94
+ : isGoalDriftWake
95
+ ? `Potential goal drift was detected in this channel.${msg.triggerReason ? ` ${msg.triggerReason}` : ""}`
96
+ : isTriggerWake
97
+ ? `A scheduled or proactive trigger woke you up.${msg.triggerReason ? ` ${msg.triggerReason}` : ""}`
59
98
  : "A new message appeared in a channel you're in. You are a participant in this channel — reply naturally as a teammate would. If the message is a greeting or directed at the group, respond. Only stay silent if the message is clearly a private conversation between other people that has nothing to do with you.",
60
99
  "",
100
+ "## Your saved profile",
101
+ agentProfileBlock,
102
+ "",
61
103
  "## Recent messages",
62
104
  contextLines,
63
105
  "",
64
- isMention ? "## Message that mentioned you" : "## Latest message",
106
+ "## Attachments",
107
+ attachmentLines,
108
+ "",
109
+ isMention ? "## Message that mentioned you" : isCoordinationWake ? "## Message that woke you" : "## Latest message",
65
110
  msg.content,
66
111
  ].join("\n");
67
112
 
113
+ let typingInterval: ReturnType<typeof setInterval> | null = null;
114
+ let shouldDeleteSession = false;
115
+
68
116
  try {
117
+ reportPluginReflectionSoon(client, {
118
+ scope: "chat",
119
+ status: "started",
120
+ summary: `Started reply for message ${msg.messageId}.`,
121
+ channelId: msg.channelId,
122
+ messageId: msg.messageId,
123
+ detail: {
124
+ wakeReason: msg.wakeReason ?? null,
125
+ isMention,
126
+ },
127
+ }, log);
128
+
69
129
  // Keep typing indicator alive during execution
70
- const typingInterval = setInterval(() => {
71
- client.sendTyping(agentId, msg.channelId).catch(() => {});
130
+ typingInterval = setInterval(() => {
131
+ client.sendTyping(msg.channelId).catch(() => {});
72
132
  }, 3000);
133
+ typingInterval.unref();
73
134
 
74
135
  const { runId } = await runtime.subagent.run({
75
136
  sessionKey,
@@ -77,33 +138,74 @@ async function handleChatMention(opts: {
77
138
  message: agentMessage,
78
139
  extraSystemPrompt: isMention
79
140
  ? "You are responding to a direct @mention in ClawRoom. " +
141
+ "Always follow your configured Role, System Prompt, and Memory for this reply. " +
80
142
  "You MUST reply with a helpful response. " +
81
143
  "Keep your reply SHORT and conversational (1-3 sentences). " +
82
144
  "Do NOT include file paths, system info, or markdown headers."
145
+ : isCoordinationWake
146
+ ? "You are coordinating a tracked multi-agent mission in ClawRoom. " +
147
+ "Always follow your configured Role, System Prompt, and Memory for this turn. " +
148
+ "Clarify the root task until the next execution step is concrete. Stay read-only while clarifying or planning. " +
149
+ "Create or follow up only on bounded child work when needed, synthesize returned results yourself, and drive convergence instead of open-ended chatter. " +
150
+ "If coordination drifts, decide directly or surface one concrete blocker. " +
151
+ "Do not respond with SKIP."
152
+ : isFollowThroughWake
153
+ ? "Your follow-through engine woke you up in ClawRoom because an open loop needs progress. " +
154
+ "Always follow your configured Role, System Prompt, and Memory for this reply. " +
155
+ "Move the work forward, close the loop, or ask the minimum clarifying question needed. " +
156
+ "Do not ignore the wake-up. Keep replies SHORT (1-3 sentences)."
157
+ : isGoalDriftWake
158
+ ? "Potential goal drift was detected in ClawRoom. " +
159
+ "Always follow your configured Role, System Prompt, and Memory for this reply. " +
160
+ "Surface the misalignment clearly and ask for re-alignment if needed. " +
161
+ "Do not respond with SKIP. Keep replies SHORT (1-3 sentences)."
162
+ : isTriggerWake
163
+ ? "A scheduled or proactive trigger woke you up in ClawRoom. " +
164
+ "Always follow your configured Role, System Prompt, and Memory for this reply. " +
165
+ "Do the triggered work directly, then report concise progress or outcome. " +
166
+ "Do not respond with SKIP unless the trigger is obviously stale or invalid."
83
167
  : "You are a member of a chat channel in ClawRoom. " +
168
+ "Always follow your configured Role, System Prompt, and Memory for this reply. " +
84
169
  "Respond naturally as a teammate. If someone greets the channel, greet back. " +
85
170
  "If someone asks a question, help if you can. " +
86
171
  "Only respond with exactly SKIP if the message is clearly a private exchange between others that doesn't involve you at all. " +
87
172
  "Keep replies SHORT (1-3 sentences).",
88
173
  lane: "clawroom",
89
174
  });
175
+ shouldDeleteSession = true;
90
176
 
91
177
  const waitResult = await runtime.subagent.waitForRun({
92
178
  runId,
93
179
  timeoutMs: CHAT_TIMEOUT_MS,
94
180
  });
95
181
 
96
- clearInterval(typingInterval);
97
-
98
182
  if (waitResult.status === "error") {
99
183
  log?.error?.(`[clawroom:chat] subagent error: ${waitResult.error}`);
100
- await client.sendChatReply(agentId, msg.channelId, "Sorry, I encountered an error processing your message.");
184
+ reportPluginReflectionSoon(client, {
185
+ scope: "chat",
186
+ status: "error",
187
+ summary: `Reply failed for message ${msg.messageId}.`,
188
+ channelId: msg.channelId,
189
+ messageId: msg.messageId,
190
+ responseExcerpt: waitResult.error ?? null,
191
+ detail: {
192
+ error: waitResult.error ?? "Unknown error",
193
+ },
194
+ }, log);
195
+ await client.sendChatReply(msg.channelId, "Sorry, I encountered an error processing your message.");
101
196
  return;
102
197
  }
103
198
 
104
199
  if (waitResult.status === "timeout") {
105
200
  log?.error?.(`[clawroom:chat] subagent timeout for message ${msg.messageId}`);
106
- await client.sendChatReply(agentId, msg.channelId, "Sorry, I took too long to respond. Please try again.");
201
+ reportPluginReflectionSoon(client, {
202
+ scope: "chat",
203
+ status: "timeout",
204
+ summary: `Reply timed out for message ${msg.messageId}.`,
205
+ channelId: msg.channelId,
206
+ messageId: msg.messageId,
207
+ }, log);
208
+ await client.sendChatReply(msg.channelId, "Sorry, I took too long to respond. Please try again.");
107
209
  return;
108
210
  }
109
211
 
@@ -112,26 +214,62 @@ async function handleChatMention(opts: {
112
214
  limit: 10,
113
215
  });
114
216
 
115
- const reply = extractLastAssistantMessage(messages);
217
+ const reply = normalizeChatReplyContent(extractLastAssistantMessage(messages));
116
218
 
117
219
  // If agent chose to skip (awareness message, not relevant), don't reply
118
- if (reply.trim() === "SKIP" || reply.trim() === "skip") {
220
+ const replyNorm = reply.trim().toLowerCase().replace(/[_\-\s]/g, "");
221
+ if (replyNorm === "skip" || replyNorm === "noreply" || replyNorm === "pass") {
119
222
  log?.info?.(`[clawroom:chat] skipping ${msg.messageId} (not relevant)`);
120
- await runtime.subagent.deleteSession({ sessionKey, deleteTranscript: true });
223
+ reportPluginReflectionSoon(client, {
224
+ scope: "chat",
225
+ status: "skipped",
226
+ summary: `Skipped reply for message ${msg.messageId}.`,
227
+ channelId: msg.channelId,
228
+ messageId: msg.messageId,
229
+ toolsUsed: extractToolNames(messages),
230
+ responseExcerpt: reply.slice(0, 400),
231
+ }, log);
121
232
  return;
122
233
  }
123
234
 
124
235
  log?.info?.(`[clawroom:chat] replying to ${msg.messageId}: "${reply.slice(0, 80)}"`);
125
- await client.sendChatReply(agentId, msg.channelId, reply);
126
-
127
- await runtime.subagent.deleteSession({
128
- sessionKey,
129
- deleteTranscript: true,
130
- });
236
+ await client.sendChatReply(msg.channelId, reply);
237
+ reportPluginReflectionSoon(client, {
238
+ scope: "chat",
239
+ status: "completed",
240
+ summary: `Replied in channel ${msg.channelId}.`,
241
+ channelId: msg.channelId,
242
+ messageId: msg.messageId,
243
+ toolsUsed: extractToolNames(messages),
244
+ responseExcerpt: reply.slice(0, 400),
245
+ detail: {
246
+ wakeReason: msg.wakeReason ?? null,
247
+ isMention,
248
+ },
249
+ }, log);
131
250
  } catch (err) {
132
251
  const reason = err instanceof Error ? err.message : String(err);
133
252
  log?.error?.(`[clawroom:chat] unexpected error: ${reason}`);
134
- await client.sendChatReply(agentId, msg.channelId, "Sorry, something went wrong. Please try again.");
253
+ reportPluginReflectionSoon(client, {
254
+ scope: "chat",
255
+ status: "error",
256
+ summary: `Unexpected chat executor failure for message ${msg.messageId}.`,
257
+ channelId: msg.channelId,
258
+ messageId: msg.messageId,
259
+ responseExcerpt: reason.slice(0, 400),
260
+ detail: {
261
+ error: reason,
262
+ },
263
+ }, log);
264
+ await client.sendChatReply(msg.channelId, "Sorry, something went wrong. Please try again.");
265
+ } finally {
266
+ if (typingInterval) clearInterval(typingInterval);
267
+ if (shouldDeleteSession) {
268
+ await runtime.subagent.deleteSession({
269
+ sessionKey,
270
+ deleteTranscript: true,
271
+ }).catch(() => {});
272
+ }
135
273
  }
136
274
  }
137
275
 
@@ -152,3 +290,24 @@ function extractLastAssistantMessage(messages: unknown[]): string {
152
290
  }
153
291
  return "I'm not sure how to respond to that.";
154
292
  }
293
+
294
+ function normalizeChatReplyContent(content: string): string {
295
+ return content
296
+ .replace(/^(?:[ \t]*\r?\n)+/, "")
297
+ .trimEnd();
298
+ }
299
+
300
+ function formatAgentProfile(profile: AgentChatProfile): string {
301
+ return [
302
+ `Role: ${profile.role}`,
303
+ "",
304
+ "System Prompt:",
305
+ profile.systemPrompt,
306
+ "",
307
+ "Memory:",
308
+ profile.memory,
309
+ "",
310
+ "Continuity Packet:",
311
+ profile.continuityPacket,
312
+ ].join("\n");
313
+ }
@@ -0,0 +1,60 @@
1
+ import type { ClawroomClient } from "@clawroom/sdk";
2
+
3
+ export type ReflectionScope = "chat" | "task";
4
+ export type ReflectionStatus = "started" | "completed" | "skipped" | "timeout" | "error";
5
+ type ReflectionParams = {
6
+ scope: ReflectionScope;
7
+ status: ReflectionStatus;
8
+ summary: string;
9
+ channelId?: string | null;
10
+ taskId?: string | null;
11
+ messageId?: string | null;
12
+ toolsUsed?: string[];
13
+ responseExcerpt?: string | null;
14
+ detail?: Record<string, unknown>;
15
+ };
16
+
17
+ export async function reportPluginReflection(
18
+ client: ClawroomClient,
19
+ params: ReflectionParams,
20
+ ): Promise<void> {
21
+ await client.sendReflection({
22
+ scope: params.scope,
23
+ status: params.status,
24
+ summary: params.summary,
25
+ channelId: params.channelId ?? null,
26
+ taskId: params.taskId ?? null,
27
+ messageId: params.messageId ?? null,
28
+ toolsUsed: params.toolsUsed ?? [],
29
+ responseExcerpt: params.responseExcerpt ?? null,
30
+ detail: {
31
+ source: "plugin",
32
+ ...(params.detail ?? {}),
33
+ },
34
+ });
35
+ }
36
+
37
+ export function reportPluginReflectionSoon(
38
+ client: ClawroomClient,
39
+ params: ReflectionParams,
40
+ log?: { warn?: (message: string, ...args: unknown[]) => void },
41
+ ): void {
42
+ void reportPluginReflection(client, params).catch((error) => {
43
+ log?.warn?.(`[clawroom:${params.scope}] reflection report failed`, error);
44
+ });
45
+ }
46
+
47
+ export function extractToolNames(messages: unknown[]): string[] {
48
+ const names = new Set<string>();
49
+ for (const message of messages) {
50
+ const msg = message as Record<string, unknown> | undefined;
51
+ if (!msg || !Array.isArray(msg.content)) continue;
52
+ for (const block of msg.content) {
53
+ const item = block as Record<string, unknown>;
54
+ if (item.type !== "tool_use" && item.type !== "tool_call") continue;
55
+ const name = item.name ?? item.function;
56
+ if (typeof name === "string" && name) names.add(name);
57
+ }
58
+ }
59
+ return Array.from(names);
60
+ }
package/src/runtime.ts CHANGED
@@ -3,4 +3,5 @@ import { createPluginRuntimeStore } from "openclaw/plugin-sdk";
3
3
 
4
4
  const { setRuntime: setClawroomRuntime, getRuntime: getClawroomRuntime } =
5
5
  createPluginRuntimeStore<PluginRuntime>("ClawRoom runtime not initialized");
6
+
6
7
  export { getClawroomRuntime, setClawroomRuntime };