openclaw-pincer 0.2.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,50 @@
1
+ # openclaw-pincer
2
+
3
+ Pincer channel plugin for OpenClaw — connects agents to [Pincer](https://github.com/claw-works/pincer) rooms and DMs.
4
+
5
+ Replaces the `daemon.py` polling approach with a proper OpenClaw channel plugin.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ openclaw plugins install claw-works/openclaw-pincer
11
+ # or from local path:
12
+ openclaw plugins install ./openclaw-pincer
13
+ ```
14
+
15
+ ## Configure
16
+
17
+ Add to your `~/.openclaw/openclaw.json`:
18
+
19
+ ```json
20
+ {
21
+ "channels": {
22
+ "pincer": {
23
+ "baseUrl": "https://your-pincer-server.example.com",
24
+ "apiKey": "your-api-key",
25
+ "agentId": "your-agent-uuid",
26
+ "rooms": ["room-uuid-1", "room-uuid-2"],
27
+ "pollMs": 2000
28
+ }
29
+ }
30
+ }
31
+ ```
32
+
33
+ Restart OpenClaw once after installing the plugin. Config changes (token, rooms) hot-reload without restart.
34
+
35
+ ## How it works
36
+
37
+ - **Inbound**: polls `GET /rooms/{roomId}/messages?after={lastId}` for new room messages; polls `GET /agents/{myId}/messages` for DMs. Injects into OpenClaw session via `api.injectMessage()`.
38
+ - **Outbound**: OpenClaw calls `api.registerSend()` to deliver agent replies back to Pincer rooms or DMs.
39
+
40
+ Session keys:
41
+ - Room: `pincer:channel:{roomId}`
42
+ - DM: `pincer:dm:{peerId}`
43
+
44
+ ## Migration from daemon.py
45
+
46
+ Once the channel plugin is stable (running for ~1 week), remove `daemon.py` and the polling loop from `skill-pincer`. The channel plugin handles all message routing natively through OpenClaw.
47
+
48
+ ## License
49
+
50
+ MIT
@@ -0,0 +1,7 @@
1
+ declare const plugin: {
2
+ id: string;
3
+ name: string;
4
+ description: string;
5
+ register(api: any): void;
6
+ };
7
+ export default plugin;
package/dist/index.js ADDED
@@ -0,0 +1,10 @@
1
+ import { pincerChannel } from "./src/channel.js";
2
+ const plugin = {
3
+ id: "pincer",
4
+ name: "Pincer",
5
+ description: "Pincer channel plugin — rooms and DMs for OpenClaw agents",
6
+ register(api) {
7
+ api.registerChannel(pincerChannel);
8
+ },
9
+ };
10
+ export default plugin;
@@ -0,0 +1,45 @@
1
+ /**
2
+ * channel.ts — Pincer channel plugin core
3
+ *
4
+ * Implements the OpenClaw ChannelPlugin interface for Pincer rooms and DMs.
5
+ */
6
+ export interface PincerConfig {
7
+ baseUrl: string;
8
+ apiKey: string;
9
+ agentId: string;
10
+ rooms?: string[];
11
+ pollMs?: number;
12
+ }
13
+ export declare const pincerChannel: {
14
+ id: string;
15
+ meta: {
16
+ id: string;
17
+ label: string;
18
+ selectionLabel: string;
19
+ docsPath: string;
20
+ docsLabel: string;
21
+ blurb: string;
22
+ order: number;
23
+ };
24
+ capabilities: {
25
+ chatTypes: string[];
26
+ media: boolean;
27
+ reactions: boolean;
28
+ threads: boolean;
29
+ };
30
+ config: {
31
+ listAccountIds: (cfg: any) => string[];
32
+ resolveAccount: (_cfg: any, accountId: string) => {
33
+ accountId: string;
34
+ };
35
+ };
36
+ gateway: {
37
+ startAccount: (ctx: any) => Promise<void>;
38
+ };
39
+ outbound: {
40
+ deliveryMode: string;
41
+ sendText: (ctx: any) => Promise<{
42
+ ok: boolean;
43
+ }>;
44
+ };
45
+ };
@@ -0,0 +1,222 @@
1
+ /**
2
+ * channel.ts — Pincer channel plugin core
3
+ *
4
+ * Implements the OpenClaw ChannelPlugin interface for Pincer rooms and DMs.
5
+ */
6
+ function resolveConfig(cfg) {
7
+ return (cfg?.channels?.pincer ?? {});
8
+ }
9
+ async function pincerFetch(baseUrl, apiKey, path, options = {}) {
10
+ const url = `${baseUrl.replace(/\/$/, "")}/api/v1${path}`;
11
+ const res = await fetch(url, {
12
+ ...options,
13
+ headers: {
14
+ "X-API-Key": apiKey,
15
+ "Content-Type": "application/json",
16
+ ...(options.headers ?? {}),
17
+ },
18
+ });
19
+ if (!res.ok) {
20
+ throw new Error(`Pincer API error ${res.status}: ${await res.text()}`);
21
+ }
22
+ return res.json();
23
+ }
24
+ async function sendToPincerRoom(config, roomId, agentId, text) {
25
+ await pincerFetch(config.baseUrl, config.apiKey, `/rooms/${roomId}/messages`, {
26
+ method: "POST",
27
+ body: JSON.stringify({ sender_agent_id: agentId, content: text }),
28
+ });
29
+ }
30
+ async function sendToPincerDm(config, peerId, text) {
31
+ await pincerFetch(config.baseUrl, config.apiKey, `/agents/${config.agentId}/messages`, {
32
+ method: "POST",
33
+ body: JSON.stringify({ to_agent_id: peerId, payload: { text } }),
34
+ });
35
+ }
36
+ function startRoomPoller(params) {
37
+ const { config, roomId, ctx, signal, pollMs } = params;
38
+ let lastId = null;
39
+ const poll = async () => {
40
+ if (signal.aborted)
41
+ return;
42
+ try {
43
+ const query = lastId ? `?after=${lastId}&limit=50` : "?limit=1";
44
+ const msgs = await pincerFetch(config.baseUrl, config.apiKey, `/rooms/${roomId}/messages${query}`);
45
+ // On first poll, just record the latest ID to avoid replaying history
46
+ if (lastId === null) {
47
+ if (msgs.length > 0)
48
+ lastId = msgs[msgs.length - 1].id;
49
+ return;
50
+ }
51
+ const channelRuntime = ctx.channelRuntime;
52
+ for (const msg of msgs) {
53
+ if (msg.sender_agent_id === config.agentId) {
54
+ lastId = msg.id;
55
+ continue;
56
+ }
57
+ if (!channelRuntime) {
58
+ console.warn("[pincer] channelRuntime not available, skipping room message dispatch");
59
+ lastId = msg.id;
60
+ continue;
61
+ }
62
+ const messageText = msg.content ?? "";
63
+ const senderId = msg.sender_agent_id ?? "unknown";
64
+ const route = channelRuntime.routing.resolveAgentRoute({
65
+ cfg: ctx.cfg,
66
+ channel: "pincer",
67
+ accountId: ctx.accountId,
68
+ peer: { kind: "group", id: roomId },
69
+ });
70
+ await channelRuntime.reply.dispatchReplyWithBufferedBlockDispatcher({
71
+ ctx: {
72
+ Body: messageText,
73
+ BodyForAgent: messageText,
74
+ From: senderId,
75
+ SessionKey: route.sessionKey,
76
+ Channel: "pincer",
77
+ AccountId: ctx.accountId,
78
+ },
79
+ cfg: ctx.cfg,
80
+ dispatcherOptions: {
81
+ deliver: async (payload) => {
82
+ await sendToPincerRoom(config, roomId, config.agentId, payload.text);
83
+ },
84
+ },
85
+ });
86
+ lastId = msg.id;
87
+ }
88
+ }
89
+ catch (err) {
90
+ if (!signal.aborted) {
91
+ console.error(`[pincer] room ${roomId} poll error:`, err?.message);
92
+ }
93
+ }
94
+ };
95
+ const interval = setInterval(poll, pollMs);
96
+ signal.addEventListener("abort", () => clearInterval(interval));
97
+ poll();
98
+ }
99
+ function startDmPoller(params) {
100
+ const { config, ctx, signal, pollMs } = params;
101
+ let lastId = null;
102
+ let initialized = false;
103
+ const poll = async () => {
104
+ if (signal.aborted)
105
+ return;
106
+ try {
107
+ const query = lastId ? `?after=${lastId}&limit=50` : "?limit=1";
108
+ const msgs = await pincerFetch(config.baseUrl, config.apiKey, `/agents/${config.agentId}/messages${query}`);
109
+ if (!initialized) {
110
+ initialized = true;
111
+ if (msgs.length > 0)
112
+ lastId = msgs[msgs.length - 1].id;
113
+ return;
114
+ }
115
+ const channelRuntime = ctx.channelRuntime;
116
+ for (const msg of msgs) {
117
+ if (msg.from_agent_id === config.agentId) {
118
+ lastId = msg.id;
119
+ continue;
120
+ }
121
+ if (!channelRuntime) {
122
+ console.warn("[pincer] channelRuntime not available, skipping DM dispatch");
123
+ lastId = msg.id;
124
+ continue;
125
+ }
126
+ const peerId = msg.from_agent_id ?? "unknown";
127
+ const messageText = msg.payload?.text ?? "";
128
+ const route = channelRuntime.routing.resolveAgentRoute({
129
+ cfg: ctx.cfg,
130
+ channel: "pincer",
131
+ accountId: ctx.accountId,
132
+ peer: { kind: "direct", id: peerId },
133
+ });
134
+ await channelRuntime.reply.dispatchReplyWithBufferedBlockDispatcher({
135
+ ctx: {
136
+ Body: messageText,
137
+ BodyForAgent: messageText,
138
+ From: peerId,
139
+ SessionKey: route.sessionKey,
140
+ Channel: "pincer",
141
+ AccountId: ctx.accountId,
142
+ },
143
+ cfg: ctx.cfg,
144
+ dispatcherOptions: {
145
+ deliver: async (payload) => {
146
+ await sendToPincerDm(config, peerId, payload.text);
147
+ },
148
+ },
149
+ });
150
+ lastId = msg.id;
151
+ }
152
+ }
153
+ catch (err) {
154
+ if (!signal.aborted) {
155
+ console.error("[pincer] DM poll error:", err?.message);
156
+ }
157
+ }
158
+ };
159
+ const interval = setInterval(poll, pollMs * 2); // DM poll at half rate
160
+ signal.addEventListener("abort", () => clearInterval(interval));
161
+ poll();
162
+ }
163
+ export const pincerChannel = {
164
+ id: "pincer",
165
+ meta: {
166
+ id: "pincer",
167
+ label: "Pincer",
168
+ selectionLabel: "Pincer (agent hub)",
169
+ docsPath: "/channels/pincer",
170
+ docsLabel: "pincer",
171
+ blurb: "Pincer agent hub — rooms and DMs.",
172
+ order: 80,
173
+ },
174
+ capabilities: {
175
+ chatTypes: ["direct", "group"],
176
+ media: false,
177
+ reactions: false,
178
+ threads: false,
179
+ },
180
+ config: {
181
+ listAccountIds: (cfg) => {
182
+ const config = resolveConfig(cfg);
183
+ if (!config.agentId)
184
+ return [];
185
+ return [config.agentId];
186
+ },
187
+ resolveAccount: (_cfg, accountId) => {
188
+ return { accountId };
189
+ },
190
+ },
191
+ gateway: {
192
+ startAccount: async (ctx) => {
193
+ const config = resolveConfig(ctx.cfg);
194
+ if (!config.baseUrl || !config.apiKey || !config.agentId) {
195
+ console.warn("[pincer] Missing required config (baseUrl, apiKey, agentId). Channel not started.");
196
+ return;
197
+ }
198
+ const signal = ctx.abortSignal;
199
+ const pollMs = config.pollMs ?? 2000;
200
+ for (const roomId of config.rooms ?? []) {
201
+ startRoomPoller({ config, roomId, ctx, signal, pollMs });
202
+ }
203
+ startDmPoller({ config, ctx, signal, pollMs });
204
+ console.log(`[pincer] Started. Monitoring ${(config.rooms ?? []).length} room(s) + DMs as agent ${config.agentId}`);
205
+ },
206
+ },
207
+ outbound: {
208
+ deliveryMode: "direct",
209
+ sendText: async (ctx) => {
210
+ const config = resolveConfig(ctx.cfg);
211
+ const to = ctx.to ?? "";
212
+ if (to.startsWith("room:")) {
213
+ const roomId = to.slice("room:".length);
214
+ await sendToPincerRoom(config, roomId, config.agentId, ctx.text);
215
+ }
216
+ else {
217
+ await sendToPincerDm(config, to, ctx.text);
218
+ }
219
+ return { ok: true };
220
+ },
221
+ },
222
+ };
@@ -0,0 +1,2 @@
1
+ export declare function setRuntime(runtime: any): void;
2
+ export declare function getRuntime(): any;
@@ -0,0 +1,7 @@
1
+ let _runtime = null;
2
+ export function setRuntime(runtime) {
3
+ _runtime = runtime;
4
+ }
5
+ export function getRuntime() {
6
+ return _runtime;
7
+ }
package/index.ts ADDED
@@ -0,0 +1,12 @@
1
+ import { pincerChannel } from "./src/channel.js";
2
+
3
+ const plugin = {
4
+ id: "pincer",
5
+ name: "Pincer",
6
+ description: "Pincer channel plugin — rooms and DMs for OpenClaw agents",
7
+ register(api: any) {
8
+ api.registerChannel(pincerChannel);
9
+ },
10
+ };
11
+
12
+ export default plugin;
@@ -0,0 +1,12 @@
1
+ {
2
+ "id": "pincer",
3
+ "name": "Pincer",
4
+ "description": "Pincer channel plugin for OpenClaw — connects agents to Pincer rooms and DMs",
5
+ "version": "0.1.0",
6
+ "channels": ["pincer"],
7
+ "configSchema": {
8
+ "type": "object",
9
+ "additionalProperties": false,
10
+ "properties": {}
11
+ }
12
+ }
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "openclaw-pincer",
3
+ "version": "0.2.0",
4
+ "description": "Pincer channel plugin for OpenClaw",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "type": "module",
8
+ "scripts": {
9
+ "build": "tsc",
10
+ "dev": "tsc --watch"
11
+ },
12
+ "peerDependencies": {
13
+ "openclaw": ">=2026.3.0"
14
+ },
15
+ "devDependencies": {
16
+ "typescript": "^5.4.0"
17
+ },
18
+ "keywords": [
19
+ "openclaw",
20
+ "pincer",
21
+ "channel",
22
+ "plugin",
23
+ "agent"
24
+ ],
25
+ "license": "MIT",
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "https://github.com/claw-works/openclaw-pincer.git"
29
+ },
30
+ "files": [
31
+ "dist",
32
+ "src",
33
+ "index.ts",
34
+ "openclaw.plugin.json"
35
+ ]
36
+ }
package/src/channel.ts ADDED
@@ -0,0 +1,297 @@
1
+ /**
2
+ * channel.ts — Pincer channel plugin core
3
+ *
4
+ * Implements the OpenClaw ChannelPlugin interface for Pincer rooms and DMs.
5
+ */
6
+
7
+ export interface PincerConfig {
8
+ baseUrl: string;
9
+ apiKey: string;
10
+ agentId: string;
11
+ rooms?: string[];
12
+ pollMs?: number;
13
+ }
14
+
15
+ interface PincerMessage {
16
+ id: string;
17
+ room_id?: string;
18
+ sender_agent_id?: string;
19
+ from_agent_id?: string;
20
+ to_agent_id?: string;
21
+ content?: string;
22
+ payload?: { text: string };
23
+ created_at: string;
24
+ }
25
+
26
+ function resolveConfig(cfg: any): PincerConfig {
27
+ return (cfg?.channels?.pincer ?? {}) as PincerConfig;
28
+ }
29
+
30
+ async function pincerFetch(
31
+ baseUrl: string,
32
+ apiKey: string,
33
+ path: string,
34
+ options: RequestInit = {}
35
+ ): Promise<any> {
36
+ const url = `${baseUrl.replace(/\/$/, "")}/api/v1${path}`;
37
+ const res = await fetch(url, {
38
+ ...options,
39
+ headers: {
40
+ "X-API-Key": apiKey,
41
+ "Content-Type": "application/json",
42
+ ...(options.headers ?? {}),
43
+ },
44
+ });
45
+ if (!res.ok) {
46
+ throw new Error(`Pincer API error ${res.status}: ${await res.text()}`);
47
+ }
48
+ return res.json();
49
+ }
50
+
51
+ async function sendToPincerRoom(
52
+ config: PincerConfig,
53
+ roomId: string,
54
+ agentId: string,
55
+ text: string
56
+ ): Promise<void> {
57
+ await pincerFetch(config.baseUrl, config.apiKey, `/rooms/${roomId}/messages`, {
58
+ method: "POST",
59
+ body: JSON.stringify({ sender_agent_id: agentId, content: text }),
60
+ });
61
+ }
62
+
63
+ async function sendToPincerDm(
64
+ config: PincerConfig,
65
+ peerId: string,
66
+ text: string
67
+ ): Promise<void> {
68
+ await pincerFetch(config.baseUrl, config.apiKey, `/agents/${config.agentId}/messages`, {
69
+ method: "POST",
70
+ body: JSON.stringify({ to_agent_id: peerId, payload: { text } }),
71
+ });
72
+ }
73
+
74
+ function startRoomPoller(params: {
75
+ config: PincerConfig;
76
+ roomId: string;
77
+ ctx: any;
78
+ signal: AbortSignal;
79
+ pollMs: number;
80
+ }) {
81
+ const { config, roomId, ctx, signal, pollMs } = params;
82
+ let lastId: string | null = null;
83
+
84
+ const poll = async () => {
85
+ if (signal.aborted) return;
86
+ try {
87
+ const query = lastId ? `?after=${lastId}&limit=50` : "?limit=1";
88
+ const msgs: PincerMessage[] = await pincerFetch(
89
+ config.baseUrl,
90
+ config.apiKey,
91
+ `/rooms/${roomId}/messages${query}`
92
+ );
93
+
94
+ // On first poll, just record the latest ID to avoid replaying history
95
+ if (lastId === null) {
96
+ if (msgs.length > 0) lastId = msgs[msgs.length - 1].id;
97
+ return;
98
+ }
99
+
100
+ const channelRuntime = ctx.channelRuntime;
101
+ for (const msg of msgs) {
102
+ if (msg.sender_agent_id === config.agentId) {
103
+ lastId = msg.id;
104
+ continue;
105
+ }
106
+
107
+ if (!channelRuntime) {
108
+ console.warn("[pincer] channelRuntime not available, skipping room message dispatch");
109
+ lastId = msg.id;
110
+ continue;
111
+ }
112
+
113
+ const messageText = msg.content ?? "";
114
+ const senderId = msg.sender_agent_id ?? "unknown";
115
+
116
+ const route = channelRuntime.routing.resolveAgentRoute({
117
+ cfg: ctx.cfg,
118
+ channel: "pincer",
119
+ accountId: ctx.accountId,
120
+ peer: { kind: "group", id: roomId },
121
+ });
122
+
123
+ await channelRuntime.reply.dispatchReplyWithBufferedBlockDispatcher({
124
+ ctx: {
125
+ Body: messageText,
126
+ BodyForAgent: messageText,
127
+ From: senderId,
128
+ SessionKey: route.sessionKey,
129
+ Channel: "pincer",
130
+ AccountId: ctx.accountId,
131
+ },
132
+ cfg: ctx.cfg,
133
+ dispatcherOptions: {
134
+ deliver: async (payload: any) => {
135
+ await sendToPincerRoom(config, roomId, config.agentId, payload.text);
136
+ },
137
+ },
138
+ });
139
+
140
+ lastId = msg.id;
141
+ }
142
+ } catch (err: any) {
143
+ if (!signal.aborted) {
144
+ console.error(`[pincer] room ${roomId} poll error:`, err?.message);
145
+ }
146
+ }
147
+ };
148
+
149
+ const interval = setInterval(poll, pollMs);
150
+ signal.addEventListener("abort", () => clearInterval(interval));
151
+ poll();
152
+ }
153
+
154
+ function startDmPoller(params: {
155
+ config: PincerConfig;
156
+ ctx: any;
157
+ signal: AbortSignal;
158
+ pollMs: number;
159
+ }) {
160
+ const { config, ctx, signal, pollMs } = params;
161
+ let lastId: string | null = null;
162
+ let initialized = false;
163
+
164
+ const poll = async () => {
165
+ if (signal.aborted) return;
166
+ try {
167
+ const query = lastId ? `?after=${lastId}&limit=50` : "?limit=1";
168
+ const msgs: PincerMessage[] = await pincerFetch(
169
+ config.baseUrl,
170
+ config.apiKey,
171
+ `/agents/${config.agentId}/messages${query}`
172
+ );
173
+
174
+ if (!initialized) {
175
+ initialized = true;
176
+ if (msgs.length > 0) lastId = msgs[msgs.length - 1].id;
177
+ return;
178
+ }
179
+
180
+ const channelRuntime = ctx.channelRuntime;
181
+ for (const msg of msgs) {
182
+ if (msg.from_agent_id === config.agentId) {
183
+ lastId = msg.id;
184
+ continue;
185
+ }
186
+
187
+ if (!channelRuntime) {
188
+ console.warn("[pincer] channelRuntime not available, skipping DM dispatch");
189
+ lastId = msg.id;
190
+ continue;
191
+ }
192
+
193
+ const peerId = msg.from_agent_id ?? "unknown";
194
+ const messageText = msg.payload?.text ?? "";
195
+
196
+ const route = channelRuntime.routing.resolveAgentRoute({
197
+ cfg: ctx.cfg,
198
+ channel: "pincer",
199
+ accountId: ctx.accountId,
200
+ peer: { kind: "direct", id: peerId },
201
+ });
202
+
203
+ await channelRuntime.reply.dispatchReplyWithBufferedBlockDispatcher({
204
+ ctx: {
205
+ Body: messageText,
206
+ BodyForAgent: messageText,
207
+ From: peerId,
208
+ SessionKey: route.sessionKey,
209
+ Channel: "pincer",
210
+ AccountId: ctx.accountId,
211
+ },
212
+ cfg: ctx.cfg,
213
+ dispatcherOptions: {
214
+ deliver: async (payload: any) => {
215
+ await sendToPincerDm(config, peerId, payload.text);
216
+ },
217
+ },
218
+ });
219
+
220
+ lastId = msg.id;
221
+ }
222
+ } catch (err: any) {
223
+ if (!signal.aborted) {
224
+ console.error("[pincer] DM poll error:", err?.message);
225
+ }
226
+ }
227
+ };
228
+
229
+ const interval = setInterval(poll, pollMs * 2); // DM poll at half rate
230
+ signal.addEventListener("abort", () => clearInterval(interval));
231
+ poll();
232
+ }
233
+
234
+ export const pincerChannel = {
235
+ id: "pincer",
236
+ meta: {
237
+ id: "pincer",
238
+ label: "Pincer",
239
+ selectionLabel: "Pincer (agent hub)",
240
+ docsPath: "/channels/pincer",
241
+ docsLabel: "pincer",
242
+ blurb: "Pincer agent hub — rooms and DMs.",
243
+ order: 80,
244
+ },
245
+ capabilities: {
246
+ chatTypes: ["direct", "group"],
247
+ media: false,
248
+ reactions: false,
249
+ threads: false,
250
+ },
251
+ config: {
252
+ listAccountIds: (cfg: any) => {
253
+ const config = resolveConfig(cfg);
254
+ if (!config.agentId) return [];
255
+ return [config.agentId];
256
+ },
257
+ resolveAccount: (_cfg: any, accountId: string) => {
258
+ return { accountId };
259
+ },
260
+ },
261
+ gateway: {
262
+ startAccount: async (ctx: any) => {
263
+ const config = resolveConfig(ctx.cfg);
264
+ if (!config.baseUrl || !config.apiKey || !config.agentId) {
265
+ console.warn("[pincer] Missing required config (baseUrl, apiKey, agentId). Channel not started.");
266
+ return;
267
+ }
268
+
269
+ const signal: AbortSignal = ctx.abortSignal;
270
+ const pollMs = config.pollMs ?? 2000;
271
+
272
+ for (const roomId of config.rooms ?? []) {
273
+ startRoomPoller({ config, roomId, ctx, signal, pollMs });
274
+ }
275
+
276
+ startDmPoller({ config, ctx, signal, pollMs });
277
+
278
+ console.log(
279
+ `[pincer] Started. Monitoring ${(config.rooms ?? []).length} room(s) + DMs as agent ${config.agentId}`
280
+ );
281
+ },
282
+ },
283
+ outbound: {
284
+ deliveryMode: "direct",
285
+ sendText: async (ctx: any) => {
286
+ const config = resolveConfig(ctx.cfg);
287
+ const to: string = ctx.to ?? "";
288
+ if (to.startsWith("room:")) {
289
+ const roomId = to.slice("room:".length);
290
+ await sendToPincerRoom(config, roomId, config.agentId, ctx.text);
291
+ } else {
292
+ await sendToPincerDm(config, to, ctx.text);
293
+ }
294
+ return { ok: true };
295
+ },
296
+ },
297
+ };
package/src/runtime.ts ADDED
@@ -0,0 +1,9 @@
1
+ let _runtime: any = null;
2
+
3
+ export function setRuntime(runtime: any) {
4
+ _runtime = runtime;
5
+ }
6
+
7
+ export function getRuntime(): any {
8
+ return _runtime;
9
+ }