openclaw-pincer 0.3.2 → 0.3.3

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.
@@ -31,6 +31,91 @@ async function httpPost(config, path, body) {
31
31
  if (!res.ok)
32
32
  throw new Error(`Pincer POST ${path} ${res.status}`);
33
33
  }
34
+ // Fetch project room_ids from /api/v1/projects
35
+ async function fetchProjectRooms(config) {
36
+ const url = `${config.baseUrl.replace(/\/$/, "")}/api/v1/projects`;
37
+ try {
38
+ const res = await fetch(url, {
39
+ headers: { "X-API-Key": config.token, "User-Agent": "openclaw-pincer/0.3" },
40
+ });
41
+ if (!res.ok)
42
+ return [];
43
+ const data = await res.json();
44
+ const projects = Array.isArray(data) ? data : [];
45
+ return projects.map((p) => p.room_id).filter(Boolean);
46
+ }
47
+ catch {
48
+ return [];
49
+ }
50
+ }
51
+ // Subscribe to a single room WebSocket and dispatch messages
52
+ function connectRoomWs(params) {
53
+ const { config, ctx, roomId, signal } = params;
54
+ let retryMs = 2000;
55
+ function connect() {
56
+ if (signal.aborted)
57
+ return;
58
+ const wsBase = config.baseUrl.replace(/\/$/, "").replace(/^http/, "ws");
59
+ const ws = new WebSocket(`${wsBase}/api/v1/rooms/${roomId}/ws?api_key=${config.token}`);
60
+ ws.on("open", () => {
61
+ retryMs = 2000;
62
+ console.log(`[openclaw-pincer] Room WS connected: ${roomId}`);
63
+ });
64
+ ws.on("message", (data) => {
65
+ try {
66
+ const msg = JSON.parse(data.toString());
67
+ if (msg.type !== "room.message")
68
+ return;
69
+ const payload = msg.data ?? msg.payload ?? {};
70
+ const sender = payload.sender_agent_id ?? "unknown";
71
+ const content = payload.content ?? "";
72
+ if (sender === config.agentId || !content)
73
+ return;
74
+ // Mention-only: respond only when @agentName or @all
75
+ const agentName = config.agentName ?? "";
76
+ const isMentioned = agentName && content.includes(`@${agentName}`);
77
+ const isBroadcast = content.includes("@all") || content.includes("@所有人");
78
+ if (!isMentioned && !isBroadcast)
79
+ return;
80
+ const runtime = ctx.channelRuntime;
81
+ if (!runtime)
82
+ return;
83
+ dispatchToAgent(config, ctx, runtime, sender, content, roomId);
84
+ }
85
+ catch { /* ignore */ }
86
+ });
87
+ ws.on("close", () => {
88
+ if (signal.aborted)
89
+ return;
90
+ setTimeout(connect, retryMs);
91
+ retryMs = Math.min(retryMs * 1.5, 30000);
92
+ });
93
+ ws.on("error", () => ws.close());
94
+ signal.addEventListener("abort", () => ws.close(), { once: true });
95
+ }
96
+ connect();
97
+ }
98
+ // Subscribe to all project rooms, refresh every PROJECT_REFRESH_INTERVAL ms
99
+ function startProjectRoomSubscriptions(params) {
100
+ const { config, ctx, signal } = params;
101
+ const PROJECT_REFRESH_MS = 60_000;
102
+ const subscribedRooms = new Set();
103
+ async function refresh() {
104
+ if (signal.aborted)
105
+ return;
106
+ const rooms = await fetchProjectRooms(config);
107
+ for (const roomId of rooms) {
108
+ if (!subscribedRooms.has(roomId)) {
109
+ subscribedRooms.add(roomId);
110
+ connectRoomWs({ config, ctx, roomId, signal });
111
+ console.log(`[openclaw-pincer] Subscribed to project room: ${roomId}`);
112
+ }
113
+ }
114
+ }
115
+ refresh();
116
+ const timer = setInterval(refresh, PROJECT_REFRESH_MS);
117
+ signal.addEventListener("abort", () => clearInterval(timer), { once: true });
118
+ }
34
119
  function connectWs(params) {
35
120
  const { config, ctx, signal } = params;
36
121
  let retryMs = 1000;
@@ -267,7 +352,10 @@ export const pincerChannel = {
267
352
  if (!config.agentName)
268
353
  config.agentName = "openclaw-agent";
269
354
  const signal = ctx.abortSignal;
355
+ // Connect to agent DM hub
270
356
  connectWs({ config, ctx, signal });
357
+ // Subscribe to all project rooms (and refresh every 60s for new projects)
358
+ startProjectRoomSubscriptions({ config, ctx, signal });
271
359
  console.log("[openclaw-pincer] Started, connecting via WebSocket");
272
360
  await new Promise((resolve) => {
273
361
  if (signal.aborted)
@@ -2,7 +2,7 @@
2
2
  "id": "openclaw-pincer",
3
3
  "name": "Pincer",
4
4
  "description": "Pincer channel plugin for OpenClaw — connects agents to Pincer via WebSocket",
5
- "version": "0.3.1",
5
+ "version": "0.3.3",
6
6
  "channels": ["openclaw-pincer"],
7
7
  "configSchema": {
8
8
  "type": "object",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openclaw-pincer",
3
- "version": "0.3.2",
3
+ "version": "0.3.3",
4
4
  "description": "Pincer channel plugin for OpenClaw",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
package/src/channel.ts CHANGED
@@ -43,6 +43,101 @@ async function httpPost(config: PincerConfig, path: string, body: any): Promise<
43
43
  if (!res.ok) throw new Error(`Pincer POST ${path} ${res.status}`);
44
44
  }
45
45
 
46
+ // Fetch project room_ids from /api/v1/projects
47
+ async function fetchProjectRooms(config: PincerConfig): Promise<string[]> {
48
+ const url = `${config.baseUrl.replace(/\/$/, "")}/api/v1/projects`;
49
+ try {
50
+ const res = await fetch(url, {
51
+ headers: { "X-API-Key": config.token, "User-Agent": "openclaw-pincer/0.3" },
52
+ });
53
+ if (!res.ok) return [];
54
+ const data = await res.json();
55
+ const projects = Array.isArray(data) ? data : [];
56
+ return projects.map((p: any) => p.room_id).filter(Boolean);
57
+ } catch {
58
+ return [];
59
+ }
60
+ }
61
+
62
+ // Subscribe to a single room WebSocket and dispatch messages
63
+ function connectRoomWs(params: {
64
+ config: PincerConfig;
65
+ ctx: any;
66
+ roomId: string;
67
+ signal: AbortSignal;
68
+ }): void {
69
+ const { config, ctx, roomId, signal } = params;
70
+ let retryMs = 2000;
71
+
72
+ function connect() {
73
+ if (signal.aborted) return;
74
+ const wsBase = config.baseUrl.replace(/\/$/, "").replace(/^http/, "ws");
75
+ const ws = new WebSocket(`${wsBase}/api/v1/rooms/${roomId}/ws?api_key=${config.token}`);
76
+
77
+ ws.on("open", () => {
78
+ retryMs = 2000;
79
+ console.log(`[openclaw-pincer] Room WS connected: ${roomId}`);
80
+ });
81
+
82
+ ws.on("message", (data: WebSocket.Data) => {
83
+ try {
84
+ const msg = JSON.parse(data.toString());
85
+ if (msg.type !== "room.message") return;
86
+ const payload = msg.data ?? msg.payload ?? {};
87
+ const sender = payload.sender_agent_id ?? "unknown";
88
+ const content = payload.content ?? "";
89
+ if (sender === config.agentId || !content) return;
90
+ // Mention-only: respond only when @agentName or @all
91
+ const agentName = config.agentName ?? "";
92
+ const isMentioned = agentName && content.includes(`@${agentName}`);
93
+ const isBroadcast = content.includes("@all") || content.includes("@所有人");
94
+ if (!isMentioned && !isBroadcast) return;
95
+ const runtime = ctx.channelRuntime;
96
+ if (!runtime) return;
97
+ dispatchToAgent(config, ctx, runtime, sender, content, roomId);
98
+ } catch { /* ignore */ }
99
+ });
100
+
101
+ ws.on("close", () => {
102
+ if (signal.aborted) return;
103
+ setTimeout(connect, retryMs);
104
+ retryMs = Math.min(retryMs * 1.5, 30000);
105
+ });
106
+
107
+ ws.on("error", () => ws.close());
108
+ signal.addEventListener("abort", () => ws.close(), { once: true });
109
+ }
110
+
111
+ connect();
112
+ }
113
+
114
+ // Subscribe to all project rooms, refresh every PROJECT_REFRESH_INTERVAL ms
115
+ function startProjectRoomSubscriptions(params: {
116
+ config: PincerConfig;
117
+ ctx: any;
118
+ signal: AbortSignal;
119
+ }): void {
120
+ const { config, ctx, signal } = params;
121
+ const PROJECT_REFRESH_MS = 60_000;
122
+ const subscribedRooms = new Set<string>();
123
+
124
+ async function refresh() {
125
+ if (signal.aborted) return;
126
+ const rooms = await fetchProjectRooms(config);
127
+ for (const roomId of rooms) {
128
+ if (!subscribedRooms.has(roomId)) {
129
+ subscribedRooms.add(roomId);
130
+ connectRoomWs({ config, ctx, roomId, signal });
131
+ console.log(`[openclaw-pincer] Subscribed to project room: ${roomId}`);
132
+ }
133
+ }
134
+ }
135
+
136
+ refresh();
137
+ const timer = setInterval(refresh, PROJECT_REFRESH_MS);
138
+ signal.addEventListener("abort", () => clearInterval(timer), { once: true });
139
+ }
140
+
46
141
  function connectWs(params: {
47
142
  config: PincerConfig;
48
143
  ctx: any;
@@ -295,8 +390,13 @@ export const pincerChannel = {
295
390
  if (!config.agentName) config.agentName = "openclaw-agent";
296
391
 
297
392
  const signal: AbortSignal = ctx.abortSignal;
393
+
394
+ // Connect to agent DM hub
298
395
  connectWs({ config, ctx, signal });
299
396
 
397
+ // Subscribe to all project rooms (and refresh every 60s for new projects)
398
+ startProjectRoomSubscriptions({ config, ctx, signal });
399
+
300
400
  console.log("[openclaw-pincer] Started, connecting via WebSocket");
301
401
 
302
402
  await new Promise<void>((resolve) => {