openclaw-pincer 0.2.5 → 0.2.7

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.
@@ -7,8 +7,11 @@ export interface PincerConfig {
7
7
  baseUrl: string;
8
8
  apiKey: string;
9
9
  agentId: string;
10
+ agentName?: string;
10
11
  rooms?: string[];
11
12
  pollMs?: number;
13
+ requireMention?: boolean;
14
+ historyLimit?: number;
12
15
  }
13
16
  export declare const pincerChannel: {
14
17
  id: string;
@@ -27,6 +30,12 @@ export declare const pincerChannel: {
27
30
  reactions: boolean;
28
31
  threads: boolean;
29
32
  };
33
+ agentPrompt: {
34
+ messageToolHints: () => string[];
35
+ };
36
+ groups: {
37
+ resolveRequireMention: (params: any) => boolean;
38
+ };
30
39
  config: {
31
40
  listAccountIds: (cfg: any) => string[];
32
41
  resolveAccount: (_cfg: any, accountId: string) => {
@@ -28,13 +28,41 @@ async function sendToPincerRoom(config, roomId, agentId, text) {
28
28
  });
29
29
  }
30
30
  async function sendToPincerDm(config, peerId, text) {
31
- await pincerFetch(config.baseUrl, config.apiKey, `/agents/${config.agentId}/messages`, {
31
+ await pincerFetch(config.baseUrl, config.apiKey, "/messages/send", {
32
32
  method: "POST",
33
- body: JSON.stringify({ to_agent_id: peerId, payload: { text } }),
33
+ body: JSON.stringify({
34
+ from_agent_id: config.agentId,
35
+ to_agent_id: peerId,
36
+ payload: { text },
37
+ }),
34
38
  });
35
39
  }
40
+ /** Check if a room message mentions this agent (by agentId or agentName). */
41
+ function isMentioned(text, config) {
42
+ if (text.includes(config.agentId))
43
+ return true;
44
+ if (config.agentName && text.includes(config.agentName))
45
+ return true;
46
+ return false;
47
+ }
48
+ /** Fetch recent room messages to use as conversation history. */
49
+ async function fetchRoomHistory(config, roomId, limit) {
50
+ try {
51
+ const msgs = await pincerFetch(config.baseUrl, config.apiKey, `/rooms/${roomId}/messages?limit=${limit}`);
52
+ return msgs.map((m) => ({
53
+ sender: m.sender_agent_id ?? "unknown",
54
+ body: m.content ?? "",
55
+ timestamp: m.created_at ? new Date(m.created_at).getTime() : undefined,
56
+ }));
57
+ }
58
+ catch {
59
+ return [];
60
+ }
61
+ }
36
62
  function startRoomPoller(params) {
37
63
  const { config, roomId, ctx, signal, pollMs } = params;
64
+ const requireMention = config.requireMention !== false; // default true
65
+ const historyLimit = config.historyLimit ?? 10;
38
66
  let lastId = null;
39
67
  const poll = async () => {
40
68
  if (signal.aborted)
@@ -50,6 +78,7 @@ function startRoomPoller(params) {
50
78
  }
51
79
  const channelRuntime = ctx.channelRuntime;
52
80
  for (const msg of msgs) {
81
+ // Skip own messages
53
82
  if (msg.sender_agent_id === config.agentId) {
54
83
  lastId = msg.id;
55
84
  continue;
@@ -60,7 +89,14 @@ function startRoomPoller(params) {
60
89
  continue;
61
90
  }
62
91
  const messageText = msg.content ?? "";
92
+ // Require mention in group rooms (default: on)
93
+ if (requireMention && !isMentioned(messageText, config)) {
94
+ lastId = msg.id;
95
+ continue;
96
+ }
63
97
  const senderId = msg.sender_agent_id ?? "unknown";
98
+ // Fetch recent history for context
99
+ const history = await fetchRoomHistory(config, roomId, historyLimit);
64
100
  const route = channelRuntime.routing.resolveAgentRoute({
65
101
  cfg: ctx.cfg,
66
102
  channel: "pincer",
@@ -75,6 +111,7 @@ function startRoomPoller(params) {
75
111
  SessionKey: route.sessionKey,
76
112
  Channel: "pincer",
77
113
  AccountId: ctx.accountId,
114
+ InboundHistory: history,
78
115
  },
79
116
  cfg: ctx.cfg,
80
117
  dispatcherOptions: {
@@ -82,6 +119,15 @@ function startRoomPoller(params) {
82
119
  await sendToPincerRoom(config, roomId, config.agentId, payload.text);
83
120
  },
84
121
  },
122
+ replyOptions: {
123
+ extraSystemPrompt: [
124
+ "You are in a Pincer agent room (group chat). Rules:",
125
+ "- Only respond when directly mentioned or asked a question.",
126
+ "- Keep responses concise and on-topic.",
127
+ "- Do NOT engage in idle chit-chat or filler responses.",
128
+ "- Do NOT respond to every message — quality over quantity.",
129
+ ].join("\n"),
130
+ },
85
131
  });
86
132
  lastId = msg.id;
87
133
  }
@@ -177,6 +223,18 @@ export const pincerChannel = {
177
223
  reactions: false,
178
224
  threads: false,
179
225
  },
226
+ agentPrompt: {
227
+ messageToolHints: () => [
228
+ "- In Pincer rooms, only respond when @mentioned or directly asked. No idle chit-chat.",
229
+ "- In Pincer DMs, respond normally.",
230
+ ],
231
+ },
232
+ groups: {
233
+ resolveRequireMention: (params) => {
234
+ const config = resolveConfig(params.cfg);
235
+ return config.requireMention !== false; // default true
236
+ },
237
+ },
180
238
  config: {
181
239
  listAccountIds: (cfg) => {
182
240
  const config = resolveConfig(cfg);
@@ -196,14 +254,12 @@ export const pincerChannel = {
196
254
  return;
197
255
  }
198
256
  const signal = ctx.abortSignal;
199
- const pollMs = config.pollMs ?? 2000;
257
+ const pollMs = config.pollMs ?? 15000;
200
258
  for (const roomId of config.rooms ?? []) {
201
259
  startRoomPoller({ config, roomId, ctx, signal, pollMs });
202
260
  }
203
261
  startDmPoller({ config, ctx, signal, pollMs });
204
- console.log(`[pincer] Started. Monitoring ${(config.rooms ?? []).length} room(s) + DMs as agent ${config.agentId}`);
205
- // Keep startAccount alive until the signal fires — OpenClaw treats immediate
206
- // return as a crash and schedules auto-restart.
262
+ console.log(`[pincer] Started. requireMention=${config.requireMention !== false}. Monitoring ${(config.rooms ?? []).length} room(s) + DMs as agent ${config.agentId}`);
207
263
  await new Promise((resolve) => {
208
264
  if (signal.aborted) {
209
265
  resolve();
@@ -2,7 +2,7 @@
2
2
  "id": "pincer",
3
3
  "name": "Pincer",
4
4
  "description": "Pincer channel plugin for OpenClaw — connects agents to Pincer rooms and DMs",
5
- "version": "0.2.1",
5
+ "version": "0.2.7",
6
6
  "channels": ["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.2.5",
3
+ "version": "0.2.7",
4
4
  "description": "Pincer channel plugin for OpenClaw",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -38,4 +38,4 @@
38
38
  "index.ts",
39
39
  "openclaw.plugin.json"
40
40
  ]
41
- }
41
+ }
package/src/channel.ts CHANGED
@@ -8,8 +8,11 @@ export interface PincerConfig {
8
8
  baseUrl: string;
9
9
  apiKey: string;
10
10
  agentId: string;
11
+ agentName?: string; // display name for mention detection (e.g. "蔻儿")
11
12
  rooms?: string[];
12
13
  pollMs?: number;
14
+ requireMention?: boolean; // default: true — only respond in rooms when @mentioned
15
+ historyLimit?: number; // how many messages to include as context (default: 10)
13
16
  }
14
17
 
15
18
  interface PincerMessage {
@@ -65,12 +68,45 @@ async function sendToPincerDm(
65
68
  peerId: string,
66
69
  text: string
67
70
  ): Promise<void> {
68
- await pincerFetch(config.baseUrl, config.apiKey, `/agents/${config.agentId}/messages`, {
71
+ await pincerFetch(config.baseUrl, config.apiKey, "/messages/send", {
69
72
  method: "POST",
70
- body: JSON.stringify({ to_agent_id: peerId, payload: { text } }),
73
+ body: JSON.stringify({
74
+ from_agent_id: config.agentId,
75
+ to_agent_id: peerId,
76
+ payload: { text },
77
+ }),
71
78
  });
72
79
  }
73
80
 
81
+ /** Check if a room message mentions this agent (by agentId or agentName). */
82
+ function isMentioned(text: string, config: PincerConfig): boolean {
83
+ if (text.includes(config.agentId)) return true;
84
+ if (config.agentName && text.includes(config.agentName)) return true;
85
+ return false;
86
+ }
87
+
88
+ /** Fetch recent room messages to use as conversation history. */
89
+ async function fetchRoomHistory(
90
+ config: PincerConfig,
91
+ roomId: string,
92
+ limit: number
93
+ ): Promise<Array<{ sender: string; body: string; timestamp?: number }>> {
94
+ try {
95
+ const msgs: PincerMessage[] = await pincerFetch(
96
+ config.baseUrl,
97
+ config.apiKey,
98
+ `/rooms/${roomId}/messages?limit=${limit}`
99
+ );
100
+ return msgs.map((m) => ({
101
+ sender: m.sender_agent_id ?? "unknown",
102
+ body: m.content ?? "",
103
+ timestamp: m.created_at ? new Date(m.created_at).getTime() : undefined,
104
+ }));
105
+ } catch {
106
+ return [];
107
+ }
108
+ }
109
+
74
110
  function startRoomPoller(params: {
75
111
  config: PincerConfig;
76
112
  roomId: string;
@@ -79,6 +115,8 @@ function startRoomPoller(params: {
79
115
  pollMs: number;
80
116
  }) {
81
117
  const { config, roomId, ctx, signal, pollMs } = params;
118
+ const requireMention = config.requireMention !== false; // default true
119
+ const historyLimit = config.historyLimit ?? 10;
82
120
  let lastId: string | null = null;
83
121
 
84
122
  const poll = async () => {
@@ -99,6 +137,7 @@ function startRoomPoller(params: {
99
137
 
100
138
  const channelRuntime = ctx.channelRuntime;
101
139
  for (const msg of msgs) {
140
+ // Skip own messages
102
141
  if (msg.sender_agent_id === config.agentId) {
103
142
  lastId = msg.id;
104
143
  continue;
@@ -111,8 +150,18 @@ function startRoomPoller(params: {
111
150
  }
112
151
 
113
152
  const messageText = msg.content ?? "";
153
+
154
+ // Require mention in group rooms (default: on)
155
+ if (requireMention && !isMentioned(messageText, config)) {
156
+ lastId = msg.id;
157
+ continue;
158
+ }
159
+
114
160
  const senderId = msg.sender_agent_id ?? "unknown";
115
161
 
162
+ // Fetch recent history for context
163
+ const history = await fetchRoomHistory(config, roomId, historyLimit);
164
+
116
165
  const route = channelRuntime.routing.resolveAgentRoute({
117
166
  cfg: ctx.cfg,
118
167
  channel: "pincer",
@@ -128,6 +177,7 @@ function startRoomPoller(params: {
128
177
  SessionKey: route.sessionKey,
129
178
  Channel: "pincer",
130
179
  AccountId: ctx.accountId,
180
+ InboundHistory: history,
131
181
  },
132
182
  cfg: ctx.cfg,
133
183
  dispatcherOptions: {
@@ -135,6 +185,15 @@ function startRoomPoller(params: {
135
185
  await sendToPincerRoom(config, roomId, config.agentId, payload.text);
136
186
  },
137
187
  },
188
+ replyOptions: {
189
+ extraSystemPrompt: [
190
+ "You are in a Pincer agent room (group chat). Rules:",
191
+ "- Only respond when directly mentioned or asked a question.",
192
+ "- Keep responses concise and on-topic.",
193
+ "- Do NOT engage in idle chit-chat or filler responses.",
194
+ "- Do NOT respond to every message — quality over quantity.",
195
+ ].join("\n"),
196
+ } as any,
138
197
  });
139
198
 
140
199
  lastId = msg.id;
@@ -248,6 +307,18 @@ export const pincerChannel = {
248
307
  reactions: false,
249
308
  threads: false,
250
309
  },
310
+ agentPrompt: {
311
+ messageToolHints: () => [
312
+ "- In Pincer rooms, only respond when @mentioned or directly asked. No idle chit-chat.",
313
+ "- In Pincer DMs, respond normally.",
314
+ ],
315
+ },
316
+ groups: {
317
+ resolveRequireMention: (params: any) => {
318
+ const config = resolveConfig(params.cfg);
319
+ return config.requireMention !== false; // default true
320
+ },
321
+ },
251
322
  config: {
252
323
  listAccountIds: (cfg: any) => {
253
324
  const config = resolveConfig(cfg);
@@ -267,7 +338,7 @@ export const pincerChannel = {
267
338
  }
268
339
 
269
340
  const signal: AbortSignal = ctx.abortSignal;
270
- const pollMs = config.pollMs ?? 2000;
341
+ const pollMs = config.pollMs ?? 15000;
271
342
 
272
343
  for (const roomId of config.rooms ?? []) {
273
344
  startRoomPoller({ config, roomId, ctx, signal, pollMs });
@@ -276,11 +347,9 @@ export const pincerChannel = {
276
347
  startDmPoller({ config, ctx, signal, pollMs });
277
348
 
278
349
  console.log(
279
- `[pincer] Started. Monitoring ${(config.rooms ?? []).length} room(s) + DMs as agent ${config.agentId}`
350
+ `[pincer] Started. requireMention=${config.requireMention !== false}. Monitoring ${(config.rooms ?? []).length} room(s) + DMs as agent ${config.agentId}`
280
351
  );
281
352
 
282
- // Keep startAccount alive until the signal fires — OpenClaw treats immediate
283
- // return as a crash and schedules auto-restart.
284
353
  await new Promise<void>((resolve) => {
285
354
  if (signal.aborted) { resolve(); return; }
286
355
  signal.addEventListener("abort", () => resolve(), { once: true });