@gholl-studio/pier-connector 0.2.51 → 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/src/robot.ts ADDED
@@ -0,0 +1,300 @@
1
+ import { PierClient, protocol } from '@gholl-studio/pier-sdk';
2
+ const { createRequestPayload, createResultPayload, createErrorPayload } = protocol;
3
+ import { parseJob, safeRespond, truncate } from './job-handler.js';
4
+ import { jetstream, jetstreamManager, AckPolicy, DeliverPolicy, type JetStreamClient, type JetStreamManager } from '@nats-io/jetstream';
5
+ import type { PierAccountConfig, PierPluginApi, InboundMessage } from './types.js';
6
+
7
+ type NatsConnection = any; // Fallback for NatsConnection typing if not directly exported from jetstream
8
+
9
+
10
+ export class PierRobot {
11
+ public config: PierAccountConfig;
12
+ public accountId: string;
13
+ public client: PierClient;
14
+ public nc: NatsConnection | null = null;
15
+ public js: JetStreamClient | null = null;
16
+ public jsm: JetStreamManager | null = null;
17
+ public activeNodeJobs: Map<string, any> = new Map();
18
+ public jobSubscriptions: Map<string, any> = new Map();
19
+ public isBusy: boolean = false;
20
+ public connectionStatus: 'disconnected' | 'connecting' | 'connected' | 'error' = 'disconnected';
21
+ public stats = { received: 0, completed: 0, failed: 0 };
22
+
23
+ private api: PierPluginApi;
24
+ private logger: any;
25
+ private heartbeatTimer: NodeJS.Timeout | null = null;
26
+ private onInbound: (inbound: InboundMessage, jobId: string) => Promise<void>;
27
+
28
+ constructor(
29
+ config: PierAccountConfig,
30
+ api: PierPluginApi,
31
+ onInbound: (inbound: InboundMessage, jobId: string) => Promise<void>
32
+ ) {
33
+ this.config = config;
34
+ this.accountId = config.accountId;
35
+ this.api = api;
36
+ this.logger = api.logger;
37
+ this.onInbound = onInbound;
38
+ this.client = new PierClient({
39
+ apiUrl: config.pierApiUrl,
40
+ natsUrl: config.natsUrl,
41
+ logger: this.logger
42
+ });
43
+ }
44
+
45
+ async heartbeat() {
46
+ if (!this.config.nodeId || !this.config.secretKey) return null;
47
+ try {
48
+ const data = await this.client.heartbeat(
49
+ this.config.nodeId,
50
+ this.config.secretKey,
51
+ this.config.capabilities
52
+ );
53
+ return data.nats_config || null;
54
+ } catch (err: any) {
55
+ this.logger.warn(`[pier-connector][${this.accountId}] Heartbeat failed: ${err.message}`);
56
+ return null;
57
+ }
58
+ }
59
+
60
+ async claimJob(jobId: string) {
61
+ return await this.client.claimJob(jobId, this.config.nodeId, this.config.secretKey);
62
+ }
63
+
64
+ async subscribeToJobMessages(jobId: string) {
65
+ if (!this.js || !this.jsm) return;
66
+
67
+ const chatSubject = `chat.${jobId}`;
68
+ const streamName = 'PIER_JOBS';
69
+ const nodeSlug = this.config.nodeId.replace(/[^a-zA-Z0-9]/g, '_');
70
+ const jobSlug = jobId.replace(/[^a-zA-Z0-9]/g, '_');
71
+ const durableName = `pier_chat_${nodeSlug}_${jobSlug}`;
72
+
73
+ try {
74
+ let consumer;
75
+ try {
76
+ consumer = await this.js.consumers.get(streamName, durableName);
77
+ } catch {
78
+ await this.jsm.consumers.add(streamName, {
79
+ durable_name: durableName,
80
+ filter_subject: chatSubject,
81
+ deliver_policy: DeliverPolicy.New,
82
+ ack_policy: AckPolicy.Explicit,
83
+ ack_wait: 1000 * 60 * 60,
84
+ });
85
+ consumer = await this.js.consumers.get(streamName, durableName);
86
+ }
87
+
88
+ const iter = await consumer.consume();
89
+ this.jobSubscriptions.set(jobId, iter);
90
+
91
+ (async () => {
92
+ this.logger.info(`[pier-connector][${this.accountId}] \u{1F442} Monitoring ${chatSubject}`);
93
+ const processedMessages = new Set();
94
+ for await (const msg of iter) {
95
+ try {
96
+ const rawMsg = new TextDecoder().decode(msg.data);
97
+ let msgPayload = JSON.parse(rawMsg);
98
+
99
+ if (msgPayload.id && processedMessages.has(msgPayload.id)) { msg.ack(); continue; }
100
+ if (msgPayload.sender_id === this.config.nodeId) { msg.ack(); continue; }
101
+ if (msgPayload.type === 'receipt') { msg.ack(); continue; }
102
+ if (msgPayload.type === 'system_action' || msgPayload.msg_type === 'system_action') { msg.ack(); continue; }
103
+
104
+ try {
105
+ await this.js!.publish(chatSubject, new TextEncoder().encode(JSON.stringify({
106
+ type: 'receipt', msg_id: msgPayload.id, reader_id: this.config.nodeId
107
+ })));
108
+ } catch (err) {}
109
+
110
+ const content = msgPayload.content;
111
+ if (!content) { msg.ack(); continue; }
112
+
113
+ this.logger.info(`[pier-connector][${this.accountId}] \u{1F4AC} Chat: [${jobId}] ${msgPayload.sender_id}: "${truncate(content, 40)}"`);
114
+ if (msgPayload.id) processedMessages.add(msgPayload.id);
115
+
116
+ const jobMeta = this.activeNodeJobs.get(jobId);
117
+ if (!jobMeta || !jobMeta.isTargeted) {
118
+ msg.ack();
119
+ continue;
120
+ }
121
+
122
+ msg.ack();
123
+ await this.onInbound({
124
+ accountId: this.accountId,
125
+ senderId: `pier:${msgPayload.sender_id}`,
126
+ body: content,
127
+ jobId: jobId
128
+ }, jobId);
129
+ } catch (err: any) { this.logger.error(`[pier-connector] Chat err: ${err.message}`); }
130
+ }
131
+ })();
132
+ } catch (err: any) {
133
+ this.logger.error(`[pier-connector][${this.accountId}] Failed to setup chat consumer: ${err.message}`);
134
+ }
135
+ }
136
+
137
+ async handleMessage(msg: any) {
138
+ const rawData = new TextDecoder().decode(msg.data);
139
+ let payload: any;
140
+ try {
141
+ payload = JSON.parse(rawData);
142
+ } catch (e) {
143
+ msg.ack();
144
+ return;
145
+ }
146
+
147
+ if (payload.type === 'wakeup') {
148
+ const { jobId } = payload;
149
+ if (jobId) {
150
+ this.logger.info(`[pier-connector][${this.accountId}] ⏰ Received wakeup signal for job ${jobId}`);
151
+ if (!this.jobSubscriptions.has(jobId)) {
152
+ this.activeNodeJobs.set(jobId, { pierJobId: jobId, isTargeted: true });
153
+ await this.subscribeToJobMessages(jobId);
154
+ }
155
+ }
156
+ msg.ack();
157
+ return;
158
+ }
159
+
160
+ if (this.isBusy) {
161
+ msg.nak();
162
+ return;
163
+ }
164
+
165
+ if (payload.assigned_node_id && payload.assigned_node_id !== this.config.nodeId) {
166
+ msg.nak();
167
+ return;
168
+ }
169
+
170
+ const jobIdToClaim = payload.id;
171
+ const isTargeted = !!(payload.assigned_node_id || payload.targetNodeId || payload.TargetNodeID);
172
+
173
+ if (jobIdToClaim && isTargeted) {
174
+ const claimResult = await this.claimJob(jobIdToClaim);
175
+ if (!claimResult.ok) {
176
+ if (claimResult.alreadyClaimed) msg.ack();
177
+ else msg.nak();
178
+ return;
179
+ }
180
+ }
181
+
182
+ this.isBusy = true;
183
+ msg.ack();
184
+
185
+ this.stats.received++;
186
+ const parsed = parseJob(msg, this.logger);
187
+ if (!parsed.ok) {
188
+ this.stats.failed++;
189
+ this.isBusy = false;
190
+ safeRespond(msg, createErrorPayload({
191
+ id: jobIdToClaim || 'unknown',
192
+ errorCode: 'PARSE_ERROR',
193
+ errorMessage: parsed.error,
194
+ workerId: this.config.nodeId,
195
+ walletAddress: this.config.walletAddress,
196
+ }));
197
+ return;
198
+ }
199
+
200
+ const { job } = parsed;
201
+ const senderCore = job.meta?.sender ?? 'anonymous';
202
+
203
+ this.activeNodeJobs.set(job.id, {
204
+ pierJobId: job.id,
205
+ pierNatsMsg: msg,
206
+ pierStartTime: performance.now(),
207
+ pierMeta: job.meta,
208
+ isTargeted: isTargeted
209
+ });
210
+
211
+ let finalText = job.task;
212
+ if (!isTargeted) {
213
+ finalText = `【PIER MARKETPLACE OPEN JOB】\nJob ID: ${job.id}\nTask: ${job.task}\n\n=== CRITICAL ===\nYou MUST use \`pier_bid_task\` to bid. Do not solve directly.`;
214
+ } else {
215
+ await this.subscribeToJobMessages(job.id);
216
+ }
217
+
218
+ await this.onInbound({
219
+ accountId: this.accountId,
220
+ senderId: `pier:${senderCore}`,
221
+ body: finalText,
222
+ jobId: job.id
223
+ }, job.id);
224
+
225
+ this.isBusy = false;
226
+ }
227
+
228
+ async setupMarketplaceConsumer(streamName: string, subjectName: string, durableName: string) {
229
+ if (!this.js || !this.jsm) return;
230
+ try {
231
+ let consumer;
232
+ try {
233
+ consumer = await this.js.consumers.get(streamName, durableName);
234
+ } catch {
235
+ await this.jsm.consumers.add(streamName, {
236
+ durable_name: durableName,
237
+ filter_subject: subjectName,
238
+ deliver_policy: DeliverPolicy.New,
239
+ ack_policy: AckPolicy.Explicit,
240
+ });
241
+ consumer = await this.js.consumers.get(streamName, durableName);
242
+ }
243
+
244
+ const iter = await consumer.consume();
245
+ (async () => {
246
+ for await (const msg of iter) {
247
+ await this.handleMessage(msg);
248
+ }
249
+ })().catch(err => {
250
+ this.logger.error(`[pier-connector][${this.accountId}] Consumer error: ${err.message}`);
251
+ });
252
+ } catch (err: any) {
253
+ this.logger.error(`[pier-connector][${this.accountId}] Setup failed: ${err.message}`);
254
+ throw err;
255
+ }
256
+ }
257
+
258
+ async autoRegister() {
259
+ if (!this.config.privateKey) return;
260
+ const hostName = `${(this.api as any).getRuntimeInfo?.()?.hostname ?? 'Auto'}-${this.accountId}`;
261
+ const { nodeId, secretKey } = await this.client.autoRegister(this.config.privateKey, hostName);
262
+ this.config.nodeId = nodeId;
263
+ this.config.secretKey = secretKey;
264
+ }
265
+
266
+ async start() {
267
+ this.connectionStatus = 'connecting';
268
+ try {
269
+ if (!this.config.nodeId && this.config.privateKey) await this.autoRegister();
270
+ const natsConfig = await this.heartbeat();
271
+ if (!natsConfig) throw new Error('No NATS config');
272
+ if (natsConfig.url) {
273
+ this.client.nats.url = natsConfig.url;
274
+ }
275
+
276
+ this.nc = await this.client.connectNats();
277
+ this.js = jetstream(this.nc);
278
+ this.jsm = await jetstreamManager(this.nc);
279
+ this.connectionStatus = 'connected';
280
+
281
+ const streamName = 'PIER_JOBS';
282
+ const durableNameMarket = `pier_market_${this.config.nodeId.replace(/-/g, '_')}_${this.accountId}`;
283
+ const durableNameDirect = `pier_node_${this.config.nodeId.replace(/-/g, '_')}_${this.accountId}`;
284
+
285
+ await this.setupMarketplaceConsumer(streamName, this.config.subject, durableNameMarket);
286
+ await this.setupMarketplaceConsumer(streamName, `jobs.node.${this.config.nodeId}`, durableNameDirect);
287
+
288
+ this.heartbeatTimer = setInterval(() => this.heartbeat(), 60000);
289
+ } catch (err: any) {
290
+ this.connectionStatus = 'error';
291
+ this.logger.error(`[pier-connector][${this.accountId}] Start failed: ${err.message}`);
292
+ }
293
+ }
294
+
295
+ async stop() {
296
+ if (this.heartbeatTimer) clearInterval(this.heartbeatTimer);
297
+ if (this.nc) await this.client.drainNats();
298
+ this.connectionStatus = 'disconnected';
299
+ }
300
+ }
@@ -0,0 +1,20 @@
1
+ declare module '@gholl-studio/pier-sdk' {
2
+ export class PierClient {
3
+ constructor(config: { apiUrl: string; natsUrl?: string; logger?: any });
4
+ connectNats(): Promise<any>;
5
+ getNatsConnection(): Promise<any>;
6
+ heartbeat(nodeId: string, secretKey: string, capabilities: string[]): Promise<any>;
7
+ claimJob(jobId: string, nodeId: string, secretKey: string): Promise<any>;
8
+ autoRegister(privateKey: string, hostName: string): Promise<{ nodeId: string; secretKey: string; walletAddress: string }>;
9
+ getUserProfile(secretKey: string): Promise<any>;
10
+ drainNats(): Promise<void>;
11
+ nats: { url: string };
12
+ }
13
+
14
+ export const protocol: {
15
+ normalizeInboundPayload(payload: any): { ok: boolean; job?: any; error?: string };
16
+ createRequestPayload(data: any): any;
17
+ createResultPayload(data: any): any;
18
+ createErrorPayload(data: any): any;
19
+ };
20
+ }
package/src/types.ts ADDED
@@ -0,0 +1,32 @@
1
+ import type { OpenClawPluginApi } from 'openclaw/plugin-sdk/plugin-entry';
2
+
3
+ export interface PierAccountConfig {
4
+ accountId: string;
5
+ pierApiUrl: string;
6
+ nodeId: string;
7
+ secretKey: string;
8
+ privateKey: string;
9
+ natsUrl: string;
10
+ subject: string;
11
+ publishSubject: string;
12
+ queueGroup: string;
13
+ agentId: string;
14
+ walletAddress: string;
15
+ capabilities: string[];
16
+ }
17
+
18
+ export interface PierJob {
19
+ id: string;
20
+ subject: string;
21
+ data: any;
22
+ metadata?: any;
23
+ }
24
+
25
+ export interface InboundMessage {
26
+ accountId: string;
27
+ senderId: string;
28
+ body: string;
29
+ jobId: string;
30
+ }
31
+
32
+ export type PierPluginApi = OpenClawPluginApi;
package/src/config.js DELETED
@@ -1,37 +0,0 @@
1
- /**
2
- * Default configuration constants for pier-connector.
3
- * Values can be overridden via OpenClaw plugin config
4
- * (plugins.entries.pier-connector.config).
5
- */
6
-
7
- export const DEFAULTS = Object.freeze({
8
- /** Pier Backend API Base URL */
9
- PIER_API_URL: 'https://pier-connector.gholl.com/api/v1',
10
-
11
- /** NATS WebSocket server URL (usually provided by Heartbeat) */
12
- NATS_URL: 'wss://pier.gholl.com/nexus',
13
-
14
- /** NATS subject to subscribe for incoming jobs */
15
- SUBJECT: 'jobs.worker',
16
-
17
- /** NATS subject for outbound task publishing */
18
- PUBLISH_SUBJECT: 'jobs.submit',
19
-
20
- /** Default Queue Group for processing jobs (prevents duplicate work) */
21
- QUEUE_GROUP: 'openclaw-workers',
22
-
23
- /** Unique identifier for this agent worker node (UUID) */
24
- NODE_ID: '',
25
-
26
- /** Secret key for node authentication and heartbeats */
27
- SECRET_KEY: '',
28
-
29
- /** Optional Private Key for auto-registration via Wallet Signature (Advanced) */
30
- PRIVATE_KEY: '',
31
-
32
- /** Optional Wallet address to receive points/payments for completed tasks */
33
- WALLET_ADDRESS: '',
34
-
35
- /** Optional Agent ID to bind tasks to, instead of creating new sessions */
36
- AGENT_ID: '',
37
- });