@gholl-studio/pier-connector 0.0.7 → 0.0.9
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/package.json +4 -2
- package/src/compression.js +12 -0
- package/src/index.js +257 -18
- package/src/nats-client.js +1 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gholl-studio/pier-connector",
|
|
3
3
|
"author": "gholl",
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.9",
|
|
5
5
|
"description": "OpenClaw plugin that connects to the Pier job marketplace. Automatically fetches, executes, and reports distributed tasks for rewards.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "src/index.js",
|
|
@@ -31,9 +31,11 @@
|
|
|
31
31
|
],
|
|
32
32
|
"license": "MIT",
|
|
33
33
|
"dependencies": {
|
|
34
|
+
"@nats-io/jetstream": "^3.3.1",
|
|
34
35
|
"@nats-io/transport-node": "^3.0.0",
|
|
35
36
|
"ethers": "^6.16.0",
|
|
36
|
-
"inquirer": "^13.3.0"
|
|
37
|
+
"inquirer": "^13.3.0",
|
|
38
|
+
"pako": "^2.1.0"
|
|
37
39
|
},
|
|
38
40
|
"peerDependencies": {
|
|
39
41
|
"openclaw": ">=2.0.0"
|
package/src/index.js
CHANGED
|
@@ -12,6 +12,7 @@ import { createNatsConnection, drainConnection } from './nats-client.js';
|
|
|
12
12
|
import { parseJob, safeRespond, truncate } from './job-handler.js';
|
|
13
13
|
import { createRequestPayload, createResultPayload, createErrorPayload } from './protocol.js';
|
|
14
14
|
import { DEFAULTS } from './config.js';
|
|
15
|
+
import { jetstream, jetstreamManager, AckPolicy, DeliverPolicy } from '@nats-io/jetstream';
|
|
15
16
|
import inquirer from 'inquirer';
|
|
16
17
|
import { ethers } from 'ethers';
|
|
17
18
|
import fs from 'fs';
|
|
@@ -29,6 +30,12 @@ export default function register(api) {
|
|
|
29
30
|
|
|
30
31
|
/** @type {import('@nats-io/transport-node').NatsConnection | null} */
|
|
31
32
|
let nc = null;
|
|
33
|
+
|
|
34
|
+
/** @type {import('@nats-io/transport-node').JetStreamContext | null} */
|
|
35
|
+
let js = null;
|
|
36
|
+
|
|
37
|
+
/** @type {any | null} */
|
|
38
|
+
let jsm = null;
|
|
32
39
|
|
|
33
40
|
/** @type {import('@nats-io/transport-node').Subscription | null} */
|
|
34
41
|
let subscription = null;
|
|
@@ -39,6 +46,10 @@ export default function register(api) {
|
|
|
39
46
|
let jobsCompleted = 0;
|
|
40
47
|
let jobsFailed = 0;
|
|
41
48
|
let connectedAt = null;
|
|
49
|
+
|
|
50
|
+
/** @type {Map<string, import('@nats-io/transport-node').Subscription>} */
|
|
51
|
+
const jobSubscriptions = new Map();
|
|
52
|
+
const jobStopTimeouts = new Map();
|
|
42
53
|
|
|
43
54
|
// ── resolve plugin config ──────────────────────────────────────────
|
|
44
55
|
|
|
@@ -59,7 +70,7 @@ export default function register(api) {
|
|
|
59
70
|
secretKey: mergedCfg.secretKey || DEFAULTS.SECRET_KEY,
|
|
60
71
|
privateKey: mergedCfg.privateKey || process.env.PIER_PRIVATE_KEY || DEFAULTS.PRIVATE_KEY,
|
|
61
72
|
natsUrl: mergedCfg.natsUrl || DEFAULTS.NATS_URL,
|
|
62
|
-
subject: mergedCfg.subject || DEFAULTS.
|
|
73
|
+
subject: mergedCfg.subject || DEFAULTS.SUBJECT,
|
|
63
74
|
publishSubject: mergedCfg.publishSubject || DEFAULTS.PUBLISH_SUBJECT,
|
|
64
75
|
queueGroup: mergedCfg.queueGroup || DEFAULTS.QUEUE_GROUP,
|
|
65
76
|
agentId: mergedCfg.agentId || DEFAULTS.AGENT_ID,
|
|
@@ -186,13 +197,48 @@ export default function register(api) {
|
|
|
186
197
|
walletAddress: getActiveConfig().walletAddress,
|
|
187
198
|
});
|
|
188
199
|
|
|
189
|
-
|
|
200
|
+
if (isRealtimeMsg && js) {
|
|
201
|
+
try {
|
|
202
|
+
const config = getActiveConfig();
|
|
203
|
+
const replySubject = `jobs.job.${jobId}.msg`;
|
|
204
|
+
await js.publish(replySubject, new TextEncoder().encode(JSON.stringify({
|
|
205
|
+
sender_id: config.nodeId,
|
|
206
|
+
content: text,
|
|
207
|
+
created_at: new Date().toISOString()
|
|
208
|
+
})));
|
|
209
|
+
logger.info(`[pier-connector] 💬 Published agent reply to realtime chat for job ${jobId}`);
|
|
210
|
+
} catch (err) {
|
|
211
|
+
logger.error(`[pier-connector] Failed to send realtime reply: ${err.message}`);
|
|
212
|
+
}
|
|
213
|
+
} else {
|
|
214
|
+
safeRespond(msg, responsePayload);
|
|
190
215
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
216
|
+
jobsCompleted++;
|
|
217
|
+
logger.info(
|
|
218
|
+
`[pier-connector] ✔ Job ${jobId} completed` +
|
|
219
|
+
(elapsed ? ` — latency: ${elapsed}ms` : ''),
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Delayed stop for job-specific message listener
|
|
224
|
+
// This allows for follow-up chat messages even after a job is "completed"
|
|
225
|
+
if (jobSubscriptions.has(jobId)) {
|
|
226
|
+
if (jobStopTimeouts.has(jobId)) {
|
|
227
|
+
clearTimeout(jobStopTimeouts.get(jobId));
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const timeout = setTimeout(() => {
|
|
231
|
+
const sub = jobSubscriptions.get(jobId);
|
|
232
|
+
if (sub) {
|
|
233
|
+
logger.info(`[pier-connector] 🛑 Stopping listener for job ${jobId} (inactivity)`);
|
|
234
|
+
sub.stop();
|
|
235
|
+
jobSubscriptions.delete(jobId);
|
|
236
|
+
jobStopTimeouts.delete(jobId);
|
|
237
|
+
}
|
|
238
|
+
}, 60000); // 60s grace period
|
|
239
|
+
|
|
240
|
+
jobStopTimeouts.set(jobId, timeout);
|
|
241
|
+
}
|
|
196
242
|
}
|
|
197
243
|
|
|
198
244
|
return { ok: true };
|
|
@@ -290,6 +336,8 @@ export default function register(api) {
|
|
|
290
336
|
|
|
291
337
|
// 4. Connect to NATS
|
|
292
338
|
nc = await createNatsConnection(activeNatsUrl, logger);
|
|
339
|
+
js = jetstream(nc);
|
|
340
|
+
jsm = await jetstreamManager(nc);
|
|
293
341
|
connectionStatus = 'connected';
|
|
294
342
|
connectedAt = new Date();
|
|
295
343
|
|
|
@@ -297,18 +345,140 @@ export default function register(api) {
|
|
|
297
345
|
const publicSubject = config.subject;
|
|
298
346
|
const privateSubject = `jobs.node.${config.nodeId}`;
|
|
299
347
|
|
|
300
|
-
|
|
301
|
-
|
|
348
|
+
// Public pool with load balancing via JetStream Durable Consumer
|
|
349
|
+
if (publicSubject) {
|
|
350
|
+
const durableName = `pier_marketplace_${config.queueGroup || 'default'}`;
|
|
351
|
+
const streamName = 'PIER_JOBS';
|
|
352
|
+
logger.info(`[pier-connector] 👂 Listening to Marketplace (JetStream): ${publicSubject} (Durable: ${durableName})`);
|
|
353
|
+
|
|
354
|
+
(async () => {
|
|
355
|
+
try {
|
|
356
|
+
let consumer = await js.consumers.get(streamName, durableName).catch(async () => {
|
|
357
|
+
logger.info(`[pier-connector] Creating new Marketplace Consumer: ${durableName}`);
|
|
358
|
+
await jsm.consumers.add(streamName, {
|
|
359
|
+
durable_name: durableName,
|
|
360
|
+
filter_subject: publicSubject,
|
|
361
|
+
deliver_policy: DeliverPolicy.All,
|
|
362
|
+
ack_policy: AckPolicy.Explicit,
|
|
363
|
+
deliver_group: config.queueGroup
|
|
364
|
+
});
|
|
365
|
+
return await js.consumers.get(streamName, durableName);
|
|
366
|
+
});
|
|
302
367
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
368
|
+
const iter = await consumer.consume();
|
|
369
|
+
for await (const msg of iter) {
|
|
370
|
+
await handleMessage(msg);
|
|
371
|
+
msg.ack();
|
|
372
|
+
}
|
|
373
|
+
} catch (err) {
|
|
374
|
+
logger.error(`[pier-connector] Marketplace JetStream error: ${err.message}`);
|
|
375
|
+
}
|
|
376
|
+
})();
|
|
377
|
+
} else {
|
|
378
|
+
logger.warn('[pier-connector] ⚠ No Marketplace subject defined. Skipping.');
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Private direct channel (JetStream Durable per Node)
|
|
382
|
+
if (privateSubject) {
|
|
383
|
+
const durableName = `pier_node_${config.nodeId.replace(/-/g, '_')}`;
|
|
384
|
+
const streamName = 'PIER_JOBS';
|
|
385
|
+
logger.info(`[pier-connector] 👂 Listening to Direct Messages (JetStream): ${privateSubject} (Durable: ${durableName})`);
|
|
386
|
+
|
|
387
|
+
(async () => {
|
|
388
|
+
try {
|
|
389
|
+
let consumer = await js.consumers.get(streamName, durableName).catch(async () => {
|
|
390
|
+
logger.info(`[pier-connector] Creating new Direct Consumer: ${durableName}`);
|
|
391
|
+
await jsm.consumers.add(streamName, {
|
|
392
|
+
durable_name: durableName,
|
|
393
|
+
filter_subject: privateSubject,
|
|
394
|
+
deliver_policy: DeliverPolicy.All,
|
|
395
|
+
ack_policy: AckPolicy.Explicit
|
|
396
|
+
});
|
|
397
|
+
return await js.consumers.get(streamName, durableName);
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
const iter = await consumer.consume();
|
|
401
|
+
for await (const msg of iter) {
|
|
402
|
+
await handleMessage(msg);
|
|
403
|
+
msg.ack();
|
|
404
|
+
}
|
|
405
|
+
} catch (err) {
|
|
406
|
+
logger.error(`[pier-connector] Direct JetStream error: ${err.message}`);
|
|
407
|
+
}
|
|
408
|
+
})();
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// Reusable job message subscriber
|
|
412
|
+
async function subscribeToJobMessages(jobId) {
|
|
413
|
+
if (jobSubscriptions.has(jobId)) {
|
|
414
|
+
// Already subscribed, just reset timeout
|
|
415
|
+
if (jobStopTimeouts.has(jobId)) {
|
|
416
|
+
clearTimeout(jobStopTimeouts.get(jobId));
|
|
417
|
+
jobStopTimeouts.delete(jobId);
|
|
418
|
+
}
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const msgSubject = `jobs.job.${jobId}.msg`;
|
|
423
|
+
const streamName = 'PIER_JOBS';
|
|
424
|
+
|
|
425
|
+
try {
|
|
426
|
+
const cInfo = await jsm.consumers.add(streamName, {
|
|
427
|
+
filter_subject: msgSubject,
|
|
428
|
+
deliver_policy: DeliverPolicy.All,
|
|
429
|
+
ack_policy: AckPolicy.Explicit
|
|
430
|
+
});
|
|
431
|
+
const consumer = await js.consumers.get(streamName, cInfo.name);
|
|
432
|
+
|
|
433
|
+
const iter = await consumer.consume();
|
|
434
|
+
jobSubscriptions.set(jobId, iter);
|
|
435
|
+
|
|
436
|
+
for await (const msg of iter) {
|
|
437
|
+
msg.ack();
|
|
438
|
+
const rawMsg = new TextDecoder().decode(msg.data);
|
|
439
|
+
let msgPayload;
|
|
440
|
+
try {
|
|
441
|
+
msgPayload = JSON.parse(rawMsg);
|
|
442
|
+
} catch (e) { continue; }
|
|
443
|
+
|
|
444
|
+
// Ignore my own messages
|
|
445
|
+
if (msgPayload.sender_id === config.nodeId) continue;
|
|
446
|
+
|
|
447
|
+
if (msgPayload.type === 'receipt') continue;
|
|
448
|
+
|
|
449
|
+
// Send read receipt back
|
|
450
|
+
if (js && msgPayload.id) {
|
|
451
|
+
try {
|
|
452
|
+
const replySubject = `jobs.job.${jobId}.msg`;
|
|
453
|
+
await js.publish(replySubject, new TextEncoder().encode(JSON.stringify({
|
|
454
|
+
type: 'receipt',
|
|
455
|
+
msg_id: msgPayload.id,
|
|
456
|
+
reader_id: config.nodeId
|
|
457
|
+
})));
|
|
458
|
+
logger.info(`[pier-connector] 👁️ Sent read receipt for message ${msgPayload.id}`);
|
|
459
|
+
} catch (err) {
|
|
460
|
+
logger.error(`[pier-connector] Failed to send read receipt: ${err.message}`);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
const content = msgPayload.content;
|
|
465
|
+
logger.info(`[pier-connector] 💬 Message for job ${jobId}: "${truncate(content, 40)}"`);
|
|
466
|
+
|
|
467
|
+
await api.runtime.sendIncoming({
|
|
468
|
+
channelId: 'pier',
|
|
469
|
+
accountId: 'default',
|
|
470
|
+
senderId: `pier:${msgPayload.sender_id}`,
|
|
471
|
+
text: content,
|
|
472
|
+
metadata: {
|
|
473
|
+
pierJobId: jobId,
|
|
474
|
+
isRealtimeMsg: true
|
|
475
|
+
}
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
} catch (err) {
|
|
479
|
+
logger.error(`[pier-connector] Job JS message error for ${jobId}: ${err.message}`);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
312
482
|
|
|
313
483
|
// Unified Message Handler
|
|
314
484
|
async function handleMessage(msg) {
|
|
@@ -323,6 +493,16 @@ export default function register(api) {
|
|
|
323
493
|
return;
|
|
324
494
|
}
|
|
325
495
|
|
|
496
|
+
// WAKEUP SIGNAL HANDLING
|
|
497
|
+
if (payload.type === 'wakeup') {
|
|
498
|
+
const { jobId } = payload;
|
|
499
|
+
if (jobId) {
|
|
500
|
+
logger.info(`[pier-connector] ⏰ Received wakeup signal for job ${jobId}`);
|
|
501
|
+
subscribeToJobMessages(jobId);
|
|
502
|
+
}
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
|
|
326
506
|
// V1.1 FEATURE: Task Poisoning / Poaching Protection
|
|
327
507
|
if (payload.assigned_node_id && payload.assigned_node_id !== config.nodeId) {
|
|
328
508
|
// Silent ignore - don't reply, don't log heavily
|
|
@@ -366,6 +546,9 @@ export default function register(api) {
|
|
|
366
546
|
|
|
367
547
|
if (api.runtime?.sendIncoming) {
|
|
368
548
|
await api.runtime.sendIncoming(inbound);
|
|
549
|
+
|
|
550
|
+
// SUBSCRIBE to job-specific messages for real-time communication
|
|
551
|
+
subscribeToJobMessages(job.id);
|
|
369
552
|
} else {
|
|
370
553
|
throw new Error('Agent runtime not available');
|
|
371
554
|
}
|
|
@@ -516,6 +699,62 @@ export default function register(api) {
|
|
|
516
699
|
{ optional: true },
|
|
517
700
|
);
|
|
518
701
|
|
|
702
|
+
api.registerTool(
|
|
703
|
+
{
|
|
704
|
+
name: 'pier_chat',
|
|
705
|
+
description: 'Send a message to the employer regarding a specific job. Use this for clarification or progress updates.',
|
|
706
|
+
parameters: {
|
|
707
|
+
type: 'object',
|
|
708
|
+
properties: {
|
|
709
|
+
jobId: {
|
|
710
|
+
type: 'string',
|
|
711
|
+
description: 'The ID of the job to send the message for'
|
|
712
|
+
},
|
|
713
|
+
text: {
|
|
714
|
+
type: 'string',
|
|
715
|
+
description: 'The message content'
|
|
716
|
+
}
|
|
717
|
+
},
|
|
718
|
+
required: ['jobId', 'text']
|
|
719
|
+
},
|
|
720
|
+
async execute(_id, params) {
|
|
721
|
+
if (!nc || connectionStatus !== 'connected') {
|
|
722
|
+
return { content: [{ type: 'text', text: 'Error: NATS not connected' }] };
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
try {
|
|
726
|
+
const payload = {
|
|
727
|
+
sender_id: getActiveConfig().nodeId,
|
|
728
|
+
content: params.text,
|
|
729
|
+
created_at: new Date().toISOString()
|
|
730
|
+
};
|
|
731
|
+
|
|
732
|
+
const subject = `jobs.job.${params.jobId}.msg`;
|
|
733
|
+
await js.publish(subject, new TextEncoder().encode(JSON.stringify(payload)));
|
|
734
|
+
|
|
735
|
+
// Also SAVE to database for persistence
|
|
736
|
+
const config = getActiveConfig();
|
|
737
|
+
await fetch(`${config.pierApiUrl}/jobs/${params.jobId}/messages`, {
|
|
738
|
+
method: 'POST',
|
|
739
|
+
headers: {
|
|
740
|
+
'Content-Type': 'application/json',
|
|
741
|
+
'X-API-Key': config.secretKey // Assuming secretKey can be used as API key for node-specific actions
|
|
742
|
+
},
|
|
743
|
+
body: JSON.stringify({
|
|
744
|
+
sender_id: config.nodeId,
|
|
745
|
+
content: params.text
|
|
746
|
+
})
|
|
747
|
+
});
|
|
748
|
+
|
|
749
|
+
return { content: [{ type: 'text', text: 'Message sent' }] };
|
|
750
|
+
} catch (err) {
|
|
751
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }] };
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
},
|
|
755
|
+
{ optional: true }
|
|
756
|
+
);
|
|
757
|
+
|
|
519
758
|
// ── 4. Register /pier status command ───────────────────────────────
|
|
520
759
|
|
|
521
760
|
api.registerCommand({
|