@gholl-studio/pier-connector 0.0.7 → 0.1.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/package.json +4 -2
- package/src/compression.js +12 -0
- package/src/index.js +269 -21
- 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.1.0",
|
|
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,12 +46,17 @@ 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
|
|
|
45
56
|
function resolveConfig() {
|
|
46
57
|
const rawAccounts = api.config?.channels?.['pier']?.accounts || {};
|
|
47
|
-
const
|
|
58
|
+
const accountId = Object.keys(rawAccounts)[0] || 'default';
|
|
59
|
+
const firstAccount = rawAccounts[accountId] || {};
|
|
48
60
|
|
|
49
61
|
const legacyCfg = api.config?.plugins?.entries?.['pier-connector']?.config || {};
|
|
50
62
|
|
|
@@ -54,12 +66,13 @@ export default function register(api) {
|
|
|
54
66
|
};
|
|
55
67
|
|
|
56
68
|
return {
|
|
69
|
+
accountId: accountId,
|
|
57
70
|
pierApiUrl: mergedCfg.pierApiUrl || DEFAULTS.PIER_API_URL,
|
|
58
71
|
nodeId: mergedCfg.nodeId || DEFAULTS.NODE_ID,
|
|
59
72
|
secretKey: mergedCfg.secretKey || DEFAULTS.SECRET_KEY,
|
|
60
73
|
privateKey: mergedCfg.privateKey || process.env.PIER_PRIVATE_KEY || DEFAULTS.PRIVATE_KEY,
|
|
61
74
|
natsUrl: mergedCfg.natsUrl || DEFAULTS.NATS_URL,
|
|
62
|
-
subject: mergedCfg.subject || DEFAULTS.
|
|
75
|
+
subject: mergedCfg.subject || DEFAULTS.SUBJECT,
|
|
63
76
|
publishSubject: mergedCfg.publishSubject || DEFAULTS.PUBLISH_SUBJECT,
|
|
64
77
|
queueGroup: mergedCfg.queueGroup || DEFAULTS.QUEUE_GROUP,
|
|
65
78
|
agentId: mergedCfg.agentId || DEFAULTS.AGENT_ID,
|
|
@@ -138,6 +151,12 @@ export default function register(api) {
|
|
|
138
151
|
},
|
|
139
152
|
|
|
140
153
|
setup: {
|
|
154
|
+
checkAccountStatus: async ({ accountId }) => {
|
|
155
|
+
if (connectionStatus === 'connected') return { status: 'connected' };
|
|
156
|
+
if (connectionStatus === 'error') return { status: 'error', details: 'NATS connection error' };
|
|
157
|
+
if (connectionStatus === 'connecting') return { status: 'connecting' };
|
|
158
|
+
return { status: 'disconnected', details: connectionStatus };
|
|
159
|
+
},
|
|
141
160
|
applyAccountConfig: ({ cfg, accountId, input }) => {
|
|
142
161
|
const draft = structuredClone(cfg);
|
|
143
162
|
draft.channels = draft.channels || {};
|
|
@@ -172,8 +191,9 @@ export default function register(api) {
|
|
|
172
191
|
sendText: async ({ text, metadata }) => {
|
|
173
192
|
const jobId = metadata?.pierJobId;
|
|
174
193
|
const msg = metadata?.pierNatsMsg;
|
|
194
|
+
const isRealtimeMsg = metadata?.isRealtimeMsg;
|
|
175
195
|
|
|
176
|
-
if (msg) {
|
|
196
|
+
if (msg || isRealtimeMsg) {
|
|
177
197
|
const elapsed = metadata?.pierStartTime
|
|
178
198
|
? (performance.now() - metadata.pierStartTime).toFixed(1)
|
|
179
199
|
: null;
|
|
@@ -186,13 +206,48 @@ export default function register(api) {
|
|
|
186
206
|
walletAddress: getActiveConfig().walletAddress,
|
|
187
207
|
});
|
|
188
208
|
|
|
189
|
-
|
|
209
|
+
if (isRealtimeMsg && js) {
|
|
210
|
+
try {
|
|
211
|
+
const config = getActiveConfig();
|
|
212
|
+
const replySubject = `jobs.job.${jobId}.msg`;
|
|
213
|
+
await js.publish(replySubject, new TextEncoder().encode(JSON.stringify({
|
|
214
|
+
sender_id: config.nodeId,
|
|
215
|
+
content: text,
|
|
216
|
+
created_at: new Date().toISOString()
|
|
217
|
+
})));
|
|
218
|
+
logger.info(`[pier-connector] 💬 Published agent reply to realtime chat for job ${jobId}`);
|
|
219
|
+
} catch (err) {
|
|
220
|
+
logger.error(`[pier-connector] Failed to send realtime reply: ${err.message}`);
|
|
221
|
+
}
|
|
222
|
+
} else if (msg) {
|
|
223
|
+
safeRespond(msg, responsePayload);
|
|
224
|
+
|
|
225
|
+
jobsCompleted++;
|
|
226
|
+
logger.info(
|
|
227
|
+
`[pier-connector] ✔ Job ${jobId} completed` +
|
|
228
|
+
(elapsed ? ` — latency: ${elapsed}ms` : ''),
|
|
229
|
+
);
|
|
230
|
+
}
|
|
190
231
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
(
|
|
195
|
-
|
|
232
|
+
// Delayed stop for job-specific message listener
|
|
233
|
+
// This allows for follow-up chat messages even after a job is "completed"
|
|
234
|
+
if (jobSubscriptions.has(jobId)) {
|
|
235
|
+
if (jobStopTimeouts.has(jobId)) {
|
|
236
|
+
clearTimeout(jobStopTimeouts.get(jobId));
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const timeout = setTimeout(() => {
|
|
240
|
+
const sub = jobSubscriptions.get(jobId);
|
|
241
|
+
if (sub) {
|
|
242
|
+
logger.info(`[pier-connector] 🛑 Stopping listener for job ${jobId} (inactivity)`);
|
|
243
|
+
sub.stop();
|
|
244
|
+
jobSubscriptions.delete(jobId);
|
|
245
|
+
jobStopTimeouts.delete(jobId);
|
|
246
|
+
}
|
|
247
|
+
}, 60000); // 60s grace period
|
|
248
|
+
|
|
249
|
+
jobStopTimeouts.set(jobId, timeout);
|
|
250
|
+
}
|
|
196
251
|
}
|
|
197
252
|
|
|
198
253
|
return { ok: true };
|
|
@@ -290,6 +345,8 @@ export default function register(api) {
|
|
|
290
345
|
|
|
291
346
|
// 4. Connect to NATS
|
|
292
347
|
nc = await createNatsConnection(activeNatsUrl, logger);
|
|
348
|
+
js = jetstream(nc);
|
|
349
|
+
jsm = await jetstreamManager(nc);
|
|
293
350
|
connectionStatus = 'connected';
|
|
294
351
|
connectedAt = new Date();
|
|
295
352
|
|
|
@@ -297,18 +354,140 @@ export default function register(api) {
|
|
|
297
354
|
const publicSubject = config.subject;
|
|
298
355
|
const privateSubject = `jobs.node.${config.nodeId}`;
|
|
299
356
|
|
|
300
|
-
|
|
301
|
-
|
|
357
|
+
// Public pool with load balancing via JetStream Durable Consumer
|
|
358
|
+
if (publicSubject) {
|
|
359
|
+
const durableName = `pier_marketplace_${config.queueGroup || 'default'}`;
|
|
360
|
+
const streamName = 'PIER_JOBS';
|
|
361
|
+
logger.info(`[pier-connector] 👂 Listening to Marketplace (JetStream): ${publicSubject} (Durable: ${durableName})`);
|
|
362
|
+
|
|
363
|
+
(async () => {
|
|
364
|
+
try {
|
|
365
|
+
let consumer = await js.consumers.get(streamName, durableName).catch(async () => {
|
|
366
|
+
logger.info(`[pier-connector] Creating new Marketplace Consumer: ${durableName}`);
|
|
367
|
+
await jsm.consumers.add(streamName, {
|
|
368
|
+
durable_name: durableName,
|
|
369
|
+
filter_subject: publicSubject,
|
|
370
|
+
deliver_policy: DeliverPolicy.All,
|
|
371
|
+
ack_policy: AckPolicy.Explicit,
|
|
372
|
+
deliver_group: config.queueGroup
|
|
373
|
+
});
|
|
374
|
+
return await js.consumers.get(streamName, durableName);
|
|
375
|
+
});
|
|
302
376
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
377
|
+
const iter = await consumer.consume();
|
|
378
|
+
for await (const msg of iter) {
|
|
379
|
+
await handleMessage(msg);
|
|
380
|
+
msg.ack();
|
|
381
|
+
}
|
|
382
|
+
} catch (err) {
|
|
383
|
+
logger.error(`[pier-connector] Marketplace JetStream error: ${err.message}`);
|
|
384
|
+
}
|
|
385
|
+
})();
|
|
386
|
+
} else {
|
|
387
|
+
logger.warn('[pier-connector] ⚠ No Marketplace subject defined. Skipping.');
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Private direct channel (JetStream Durable per Node)
|
|
391
|
+
if (privateSubject) {
|
|
392
|
+
const durableName = `pier_node_${config.nodeId.replace(/-/g, '_')}`;
|
|
393
|
+
const streamName = 'PIER_JOBS';
|
|
394
|
+
logger.info(`[pier-connector] 👂 Listening to Direct Messages (JetStream): ${privateSubject} (Durable: ${durableName})`);
|
|
395
|
+
|
|
396
|
+
(async () => {
|
|
397
|
+
try {
|
|
398
|
+
let consumer = await js.consumers.get(streamName, durableName).catch(async () => {
|
|
399
|
+
logger.info(`[pier-connector] Creating new Direct Consumer: ${durableName}`);
|
|
400
|
+
await jsm.consumers.add(streamName, {
|
|
401
|
+
durable_name: durableName,
|
|
402
|
+
filter_subject: privateSubject,
|
|
403
|
+
deliver_policy: DeliverPolicy.All,
|
|
404
|
+
ack_policy: AckPolicy.Explicit
|
|
405
|
+
});
|
|
406
|
+
return await js.consumers.get(streamName, durableName);
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
const iter = await consumer.consume();
|
|
410
|
+
for await (const msg of iter) {
|
|
411
|
+
await handleMessage(msg);
|
|
412
|
+
msg.ack();
|
|
413
|
+
}
|
|
414
|
+
} catch (err) {
|
|
415
|
+
logger.error(`[pier-connector] Direct JetStream error: ${err.message}`);
|
|
416
|
+
}
|
|
417
|
+
})();
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// Reusable job message subscriber
|
|
421
|
+
async function subscribeToJobMessages(jobId) {
|
|
422
|
+
if (jobSubscriptions.has(jobId)) {
|
|
423
|
+
// Already subscribed, just reset timeout
|
|
424
|
+
if (jobStopTimeouts.has(jobId)) {
|
|
425
|
+
clearTimeout(jobStopTimeouts.get(jobId));
|
|
426
|
+
jobStopTimeouts.delete(jobId);
|
|
427
|
+
}
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const msgSubject = `jobs.job.${jobId}.msg`;
|
|
432
|
+
const streamName = 'PIER_JOBS';
|
|
433
|
+
|
|
434
|
+
try {
|
|
435
|
+
const cInfo = await jsm.consumers.add(streamName, {
|
|
436
|
+
filter_subject: msgSubject,
|
|
437
|
+
deliver_policy: DeliverPolicy.All,
|
|
438
|
+
ack_policy: AckPolicy.Explicit
|
|
439
|
+
});
|
|
440
|
+
const consumer = await js.consumers.get(streamName, cInfo.name);
|
|
441
|
+
|
|
442
|
+
const iter = await consumer.consume();
|
|
443
|
+
jobSubscriptions.set(jobId, iter);
|
|
444
|
+
|
|
445
|
+
for await (const msg of iter) {
|
|
446
|
+
msg.ack();
|
|
447
|
+
const rawMsg = new TextDecoder().decode(msg.data);
|
|
448
|
+
let msgPayload;
|
|
449
|
+
try {
|
|
450
|
+
msgPayload = JSON.parse(rawMsg);
|
|
451
|
+
} catch (e) { continue; }
|
|
452
|
+
|
|
453
|
+
// Ignore my own messages
|
|
454
|
+
if (msgPayload.sender_id === config.nodeId) continue;
|
|
455
|
+
|
|
456
|
+
if (msgPayload.type === 'receipt') continue;
|
|
457
|
+
|
|
458
|
+
// Send read receipt back
|
|
459
|
+
if (js && msgPayload.id) {
|
|
460
|
+
try {
|
|
461
|
+
const replySubject = `jobs.job.${jobId}.msg`;
|
|
462
|
+
await js.publish(replySubject, new TextEncoder().encode(JSON.stringify({
|
|
463
|
+
type: 'receipt',
|
|
464
|
+
msg_id: msgPayload.id,
|
|
465
|
+
reader_id: config.nodeId
|
|
466
|
+
})));
|
|
467
|
+
logger.info(`[pier-connector] 👁️ Sent read receipt for message ${msgPayload.id}`);
|
|
468
|
+
} catch (err) {
|
|
469
|
+
logger.error(`[pier-connector] Failed to send read receipt: ${err.message}`);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
const content = msgPayload.content;
|
|
474
|
+
logger.info(`[pier-connector] 💬 Message for job ${jobId}: "${truncate(content, 40)}"`);
|
|
475
|
+
|
|
476
|
+
await api.runtime.sendIncoming({
|
|
477
|
+
channelId: 'pier',
|
|
478
|
+
accountId: config.accountId || 'default',
|
|
479
|
+
senderId: `pier:${msgPayload.sender_id}`,
|
|
480
|
+
text: content,
|
|
481
|
+
metadata: {
|
|
482
|
+
pierJobId: jobId,
|
|
483
|
+
isRealtimeMsg: true
|
|
484
|
+
}
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
} catch (err) {
|
|
488
|
+
logger.error(`[pier-connector] Job JS message error for ${jobId}: ${err.message}`);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
312
491
|
|
|
313
492
|
// Unified Message Handler
|
|
314
493
|
async function handleMessage(msg) {
|
|
@@ -323,6 +502,16 @@ export default function register(api) {
|
|
|
323
502
|
return;
|
|
324
503
|
}
|
|
325
504
|
|
|
505
|
+
// WAKEUP SIGNAL HANDLING
|
|
506
|
+
if (payload.type === 'wakeup') {
|
|
507
|
+
const { jobId } = payload;
|
|
508
|
+
if (jobId) {
|
|
509
|
+
logger.info(`[pier-connector] ⏰ Received wakeup signal for job ${jobId}`);
|
|
510
|
+
subscribeToJobMessages(jobId);
|
|
511
|
+
}
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
|
|
326
515
|
// V1.1 FEATURE: Task Poisoning / Poaching Protection
|
|
327
516
|
if (payload.assigned_node_id && payload.assigned_node_id !== config.nodeId) {
|
|
328
517
|
// Silent ignore - don't reply, don't log heavily
|
|
@@ -350,7 +539,7 @@ export default function register(api) {
|
|
|
350
539
|
try {
|
|
351
540
|
const inbound = {
|
|
352
541
|
channelId: 'pier',
|
|
353
|
-
accountId: 'default',
|
|
542
|
+
accountId: config.accountId || 'default',
|
|
354
543
|
senderId: `pier:${job.meta?.sender ?? 'anonymous'}`,
|
|
355
544
|
text: job.task,
|
|
356
545
|
assignedAgentId: config.agentId || undefined,
|
|
@@ -366,6 +555,9 @@ export default function register(api) {
|
|
|
366
555
|
|
|
367
556
|
if (api.runtime?.sendIncoming) {
|
|
368
557
|
await api.runtime.sendIncoming(inbound);
|
|
558
|
+
|
|
559
|
+
// SUBSCRIBE to job-specific messages for real-time communication
|
|
560
|
+
subscribeToJobMessages(job.id);
|
|
369
561
|
} else {
|
|
370
562
|
throw new Error('Agent runtime not available');
|
|
371
563
|
}
|
|
@@ -516,6 +708,62 @@ export default function register(api) {
|
|
|
516
708
|
{ optional: true },
|
|
517
709
|
);
|
|
518
710
|
|
|
711
|
+
api.registerTool(
|
|
712
|
+
{
|
|
713
|
+
name: 'pier_chat',
|
|
714
|
+
description: 'Send a message to the employer regarding a specific job. Use this for clarification or progress updates.',
|
|
715
|
+
parameters: {
|
|
716
|
+
type: 'object',
|
|
717
|
+
properties: {
|
|
718
|
+
jobId: {
|
|
719
|
+
type: 'string',
|
|
720
|
+
description: 'The ID of the job to send the message for'
|
|
721
|
+
},
|
|
722
|
+
text: {
|
|
723
|
+
type: 'string',
|
|
724
|
+
description: 'The message content'
|
|
725
|
+
}
|
|
726
|
+
},
|
|
727
|
+
required: ['jobId', 'text']
|
|
728
|
+
},
|
|
729
|
+
async execute(_id, params) {
|
|
730
|
+
if (!nc || connectionStatus !== 'connected') {
|
|
731
|
+
return { content: [{ type: 'text', text: 'Error: NATS not connected' }] };
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
try {
|
|
735
|
+
const payload = {
|
|
736
|
+
sender_id: getActiveConfig().nodeId,
|
|
737
|
+
content: params.text,
|
|
738
|
+
created_at: new Date().toISOString()
|
|
739
|
+
};
|
|
740
|
+
|
|
741
|
+
const subject = `jobs.job.${params.jobId}.msg`;
|
|
742
|
+
await js.publish(subject, new TextEncoder().encode(JSON.stringify(payload)));
|
|
743
|
+
|
|
744
|
+
// Also SAVE to database for persistence
|
|
745
|
+
const config = getActiveConfig();
|
|
746
|
+
await fetch(`${config.pierApiUrl}/jobs/${params.jobId}/messages`, {
|
|
747
|
+
method: 'POST',
|
|
748
|
+
headers: {
|
|
749
|
+
'Content-Type': 'application/json',
|
|
750
|
+
'X-API-Key': config.secretKey // Assuming secretKey can be used as API key for node-specific actions
|
|
751
|
+
},
|
|
752
|
+
body: JSON.stringify({
|
|
753
|
+
sender_id: config.nodeId,
|
|
754
|
+
content: params.text
|
|
755
|
+
})
|
|
756
|
+
});
|
|
757
|
+
|
|
758
|
+
return { content: [{ type: 'text', text: 'Message sent' }] };
|
|
759
|
+
} catch (err) {
|
|
760
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }] };
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
},
|
|
764
|
+
{ optional: true }
|
|
765
|
+
);
|
|
766
|
+
|
|
519
767
|
// ── 4. Register /pier status command ───────────────────────────────
|
|
520
768
|
|
|
521
769
|
api.registerCommand({
|