@gholl-studio/pier-connector 0.0.1 → 0.0.2

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.
@@ -10,10 +10,30 @@
10
10
  "type": "object",
11
11
  "additionalProperties": false,
12
12
  "properties": {
13
+ "apiUrl": {
14
+ "type": "string",
15
+ "default": "https://pier.gholl.com",
16
+ "description": "Pier Backend API Base URL"
17
+ },
18
+ "apiKey": {
19
+ "type": "string",
20
+ "default": "",
21
+ "description": "User API Key for platform authentication"
22
+ },
23
+ "nodeId": {
24
+ "type": "string",
25
+ "default": "",
26
+ "description": "Unique identifier for this agent worker node (UUID)"
27
+ },
28
+ "secretKey": {
29
+ "type": "string",
30
+ "default": "",
31
+ "description": "Secret key for node authentication and heartbeats"
32
+ },
13
33
  "natsUrl": {
14
34
  "type": "string",
15
35
  "default": "wss://pier.gholl.com/nexus",
16
- "description": "NATS WebSocket server URL"
36
+ "description": "NATS WebSocket server URL (Overrides API dynamic config if set)"
17
37
  },
18
38
  "subject": {
19
39
  "type": "string",
@@ -30,11 +50,6 @@
30
50
  "default": "openclaw-workers",
31
51
  "description": "NATS Queue Group to join for load balancing tasks"
32
52
  },
33
- "workerId": {
34
- "type": "string",
35
- "default": "",
36
- "description": "(Optional) Unique identifier for this agent worker"
37
- },
38
53
  "walletAddress": {
39
54
  "type": "string",
40
55
  "default": "",
@@ -43,26 +58,38 @@
43
58
  }
44
59
  },
45
60
  "uiHints": {
61
+ "apiUrl": {
62
+ "label": "Pier API URL",
63
+ "placeholder": "https://pier.gholl.com"
64
+ },
65
+ "apiKey": {
66
+ "label": "API Key",
67
+ "placeholder": "Bearer token ..."
68
+ },
69
+ "nodeId": {
70
+ "label": "Node ID",
71
+ "placeholder": "uuid-..."
72
+ },
73
+ "secretKey": {
74
+ "label": "Secret Key",
75
+ "placeholder": "sk_..."
76
+ },
46
77
  "natsUrl": {
47
- "label": "NATS WebSocket URL",
78
+ "label": "NATS WebSocket URL (Override)",
48
79
  "placeholder": "wss://pier.gholl.com/nexus"
49
80
  },
50
81
  "subject": {
51
- "label": "Subscribe Subject (incoming jobs)",
82
+ "label": "Subscribe Subject",
52
83
  "placeholder": "jobs.worker"
53
84
  },
54
85
  "publishSubject": {
55
- "label": "Publish Subject (outbound tasks)",
86
+ "label": "Publish Subject",
56
87
  "placeholder": "jobs.submit"
57
88
  },
58
89
  "queueGroup": {
59
- "label": "Queue Group (Load Balancing)",
90
+ "label": "Queue Group",
60
91
  "placeholder": "openclaw-workers"
61
92
  },
62
- "workerId": {
63
- "label": "Worker ID",
64
- "placeholder": "agent-001"
65
- },
66
93
  "walletAddress": {
67
94
  "label": "Wallet Address (Rewards)",
68
95
  "placeholder": "0x..."
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.1",
4
+ "version": "0.0.2",
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,7 +31,8 @@
31
31
  ],
32
32
  "license": "MIT",
33
33
  "dependencies": {
34
- "@nats-io/transport-node": "^3.0.0"
34
+ "@nats-io/transport-node": "^3.0.0",
35
+ "inquirer": "^13.3.0"
35
36
  },
36
37
  "peerDependencies": {
37
38
  "openclaw": ">=2.0.0"
@@ -41,4 +42,4 @@
41
42
  "optional": true
42
43
  }
43
44
  }
44
- }
45
+ }
package/src/config.js CHANGED
@@ -5,7 +5,13 @@
5
5
  */
6
6
 
7
7
  export const DEFAULTS = Object.freeze({
8
- /** NATS WebSocket server URL */
8
+ /** Pier Backend API Base URL */
9
+ API_URL: 'https://pier.gholl.com',
10
+
11
+ /** User API Key for HTTP Authentication */
12
+ API_KEY: '',
13
+
14
+ /** NATS WebSocket server URL (usually provided by API) */
9
15
  NATS_URL: 'wss://pier.gholl.com/nexus',
10
16
 
11
17
  /** NATS subject to subscribe for incoming jobs */
@@ -17,8 +23,11 @@ export const DEFAULTS = Object.freeze({
17
23
  /** Default Queue Group for processing jobs (prevents duplicate work) */
18
24
  QUEUE_GROUP: 'openclaw-workers',
19
25
 
20
- /** Optional ID to identify this specific worker node */
21
- WORKER_ID: '',
26
+ /** Unique identifier for this agent worker node (UUID) */
27
+ NODE_ID: '',
28
+
29
+ /** Secret key for node authentication and heartbeats */
30
+ SECRET_KEY: '',
22
31
 
23
32
  /** Optional Wallet address to receive points/payments for completed tasks */
24
33
  WALLET_ADDRESS: '',
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 inquirer from 'inquirer';
15
16
 
16
17
  /**
17
18
  * OpenClaw plugin register function.
@@ -41,15 +42,72 @@ export default function register(api) {
41
42
  function resolveConfig() {
42
43
  const cfg = api.config?.plugins?.entries?.['pier-connector']?.config ?? {};
43
44
  return {
45
+ apiUrl: cfg.apiUrl || DEFAULTS.API_URL,
46
+ apiKey: cfg.apiKey || DEFAULTS.API_KEY,
47
+ nodeId: cfg.nodeId || DEFAULTS.NODE_ID,
48
+ secretKey: cfg.secretKey || DEFAULTS.SECRET_KEY,
44
49
  natsUrl: cfg.natsUrl || DEFAULTS.NATS_URL,
45
50
  subject: cfg.subject || DEFAULTS.SUBJECT,
46
51
  publishSubject: cfg.publishSubject || DEFAULTS.PUBLISH_SUBJECT,
47
52
  queueGroup: cfg.queueGroup || DEFAULTS.QUEUE_GROUP,
48
- workerId: cfg.workerId || DEFAULTS.WORKER_ID,
49
53
  walletAddress: cfg.walletAddress || DEFAULTS.WALLET_ADDRESS,
50
54
  };
51
55
  }
52
56
 
57
+ /** Heartbeat timer reference */
58
+ let heartbeatTimer = null;
59
+
60
+ // ── Registration & Heartbeat ───────────────────────────────────────
61
+
62
+ async function registerNode(config) {
63
+ if (config.nodeId && config.secretKey) return { nodeId: config.nodeId, secretKey: config.secretKey };
64
+
65
+ logger.info('[pier-connector] 🆕 Registering node with Pier Backend…');
66
+ const resp = await fetch(`${config.apiUrl}/api/v1/nodes/register`, {
67
+ method: 'POST',
68
+ headers: {
69
+ 'Content-Type': 'application/json',
70
+ 'Authorization': `Bearer ${config.apiKey}`,
71
+ },
72
+ body: JSON.stringify({
73
+ wallet_address: config.walletAddress,
74
+ name: 'openclaw-node', // Could use OS hostname
75
+ }),
76
+ });
77
+
78
+ if (!resp.ok) {
79
+ const errText = await resp.text();
80
+ throw new Error(`Registration failed (${resp.status}): ${errText}`);
81
+ }
82
+
83
+ const data = await resp.json();
84
+ logger.info(`[pier-connector] ✅ Node registered! ID: ${data.id}`);
85
+ // Note: In a production plugin, we would call api.updateConfig here if available
86
+ return { nodeId: data.id, secretKey: data.secret_key, natsConfig: data.nats_config };
87
+ }
88
+
89
+ async function heartbeatNode(config) {
90
+ const resp = await fetch(`${config.apiUrl}/api/v1/nodes/heartbeat`, {
91
+ method: 'POST',
92
+ headers: {
93
+ 'Content-Type': 'application/json',
94
+ 'Authorization': `Bearer ${config.apiKey}`,
95
+ },
96
+ body: JSON.stringify({
97
+ node_id: config.nodeId,
98
+ secret_key: config.secretKey,
99
+ capabilities: ['gpt-4o', 'llama3', 'vision'], // Example capabilities
100
+ description: 'OpenClaw powered intelligence node',
101
+ }),
102
+ });
103
+
104
+ if (!resp.ok) {
105
+ throw new Error(`Heartbeat failed (${resp.status})`);
106
+ }
107
+
108
+ return await resp.json();
109
+ }
110
+
53
111
  // ── 1. Register messaging channel ──────────────────────────────────
54
112
 
55
113
  const pierChannel = {
@@ -96,7 +154,7 @@ export default function register(api) {
96
154
  id: jobId,
97
155
  reply: text,
98
156
  latencyMs: elapsed ? Number(elapsed) : undefined,
99
- workerId: resolveConfig().workerId,
157
+ workerId: resolveConfig().nodeId,
100
158
  walletAddress: resolveConfig().walletAddress,
101
159
  });
102
160
 
@@ -122,142 +180,121 @@ export default function register(api) {
122
180
  id: 'pier-connector',
123
181
 
124
182
  start: async () => {
125
- const config = resolveConfig();
126
-
127
183
  logger.info('[pier-connector] 🚀 Starting background service …');
128
- logger.info(`[pier-connector] NATS URL : ${config.natsUrl}`);
129
- logger.info(`[pier-connector] Subscribe subject : ${config.subject}`);
130
- logger.info(`[pier-connector] Queue Group : ${config.queueGroup}`);
131
- logger.info(`[pier-connector] Publish subject : ${config.publishSubject}`);
132
- if (config.workerId) logger.info(`[pier-connector] Worker ID : ${config.workerId}`);
133
- if (config.walletAddress) logger.info(`[pier-connector] Wallet : ${config.walletAddress}`);
184
+ let config = resolveConfig();
134
185
 
135
186
  try {
136
- // Connect to NATS via WebSocket
137
- nc = await createNatsConnection(config.natsUrl, logger);
187
+ // 1. Ensure node is registered
188
+ const reg = await registerNode(config);
189
+ config.nodeId = reg.nodeId;
190
+ config.secretKey = reg.secretKey;
191
+ const activeNatsUrl = config.natsUrl || reg.natsConfig?.url;
192
+
193
+ logger.info(`[pier-connector] API URL : ${config.apiUrl}`);
194
+ logger.info(`[pier-connector] NATS URL : ${activeNatsUrl}`);
195
+ logger.info(`[pier-connector] Node ID : ${config.nodeId}`);
196
+ if (config.walletAddress) logger.info(`[pier-connector] Wallet : ${config.walletAddress}`);
197
+
198
+ // 2. Start Heartbeat Loop
199
+ const runHeartbeat = async () => {
200
+ try {
201
+ const hData = await heartbeatNode(config);
202
+ logger.debug('[pier-connector] Heartbeat sent');
203
+ // Potential NATS URL update could be handled here
204
+ } catch (err) {
205
+ logger.warn(`[pier-connector] Heartbeat failed: ${err.message}`);
206
+ }
207
+ };
208
+ await runHeartbeat();
209
+ heartbeatTimer = setInterval(runHeartbeat, 60000);
210
+
211
+ // 3. Connect to NATS
212
+ nc = await createNatsConnection(activeNatsUrl, logger);
138
213
  connectionStatus = 'connected';
139
214
  connectedAt = new Date();
140
215
 
141
- // Subscribe to the jobs channel using a Queue Group to distribute work
142
- subscription = nc.subscribe(config.subject, { queue: config.queueGroup });
216
+ // 4. Subscribe to Subjects
217
+ // Public pool
218
+ nc.subscribe(config.subject, {
219
+ queue: config.queueGroup,
220
+ callback: (err, msg) => handleMessage(msg)
221
+ });
222
+ // Private direct channel
223
+ nc.subscribe(`jobs.node.${config.nodeId}`, {
224
+ callback: (err, msg) => handleMessage(msg)
225
+ });
226
+
143
227
  logger.info(
144
- `[pier-connector] ✔ Subscribed to "${config.subject}" (group: "${config.queueGroup}") — waiting for jobs …`,
228
+ `[pier-connector] ✔ Subscribed to "${config.subject}" and "jobs.node.${config.nodeId}"`,
145
229
  );
146
230
 
147
- // Async message processing loop
148
- (async () => {
149
- for await (const msg of subscription) {
150
- jobsReceived++;
151
- const startTime = performance.now();
152
-
153
- logger.info(
154
- `[pier-connector] 📨 Job #${jobsReceived} received on "${msg.subject}"`,
155
- );
156
-
157
- // Parse the incoming job
158
- const parsed = parseJob(msg, logger);
159
-
160
- if (!parsed.ok) {
161
- jobsFailed++;
162
- safeRespond(
163
- msg,
164
- createErrorPayload({
165
- id: msg.subject, // Fallback ID
166
- errorCode: 'PARSE_ERROR',
167
- errorMessage: parsed.error,
168
- workerId: config.workerId,
169
- walletAddress: config.walletAddress,
170
- })
171
- );
172
- continue;
173
- }
231
+ // Message handling logic moved to a reusable function
232
+ async function handleMessage(msg) {
233
+ jobsReceived++;
234
+ const startTime = performance.now();
235
+
236
+ const parsed = parseJob(msg, logger);
237
+ if (!parsed.ok) {
238
+ jobsFailed++;
239
+ safeRespond(msg, createErrorPayload({
240
+ id: msg.subject,
241
+ errorCode: 'PARSE_ERROR',
242
+ errorMessage: parsed.error,
243
+ workerId: config.nodeId,
244
+ walletAddress: config.walletAddress,
245
+ }));
246
+ return;
247
+ }
174
248
 
175
- const { job } = parsed;
176
- logger.info(
177
- `[pier-connector] 📋 Job ${job.id}: "${truncate(job.task, 80)}"`,
178
- );
179
-
180
- // Route the job through OpenClaw's agent via the channel
181
- try {
182
- // Build an inbound message for the channel system
183
- const inbound = {
184
- channelId: 'pier',
185
- accountId: 'default',
186
- senderId: `pier:${job.meta?.sender ?? 'anonymous'}`,
187
- text: job.task,
188
- metadata: {
189
- pierJobId: job.id,
190
- pierNatsMsg: msg,
191
- pierStartTime: startTime,
192
- pierMeta: job.meta,
193
- },
194
- };
195
-
196
- // If a system prompt was provided, attach it
197
- if (job.systemPrompt) {
198
- inbound.systemPrompt = job.systemPrompt;
199
- }
249
+ const { job } = parsed;
250
+ logger.info(`[pier-connector] 📥 Received job ${job.id}: "${truncate(job.task, 60)}"`);
200
251
 
201
- // Send to OpenClaw's agent pipeline
202
- if (api.runtime?.sendIncoming) {
203
- await api.runtime.sendIncoming(inbound);
204
- } else {
205
- // Fallback: direct agent invocation if sendIncoming is not available
206
- logger.warn(
207
- '[pier-connector] api.runtime.sendIncoming not available, using direct response',
208
- );
209
- safeRespond(
210
- msg,
211
- createErrorPayload({
212
- id: job.id,
213
- errorCode: 'RUNTIME_UNAVAILABLE',
214
- errorMessage: 'Agent runtime not available — plugin may need reconfiguration',
215
- workerId: config.workerId,
216
- walletAddress: config.walletAddress,
217
- })
218
- );
219
- jobsFailed++;
220
- }
221
- } catch (err) {
222
- const elapsed = (performance.now() - startTime).toFixed(1);
223
- jobsFailed++;
224
- logger.error(
225
- `[pier-connector] ✖ Job ${job.id} failed after ${elapsed}ms — ${err.message}`,
226
- );
227
- safeRespond(
228
- msg,
229
- createErrorPayload({
230
- id: job.id,
231
- errorCode: 'EXECUTION_FAILED',
232
- errorMessage: err.message,
233
- workerId: config.workerId,
234
- walletAddress: config.walletAddress,
235
- })
236
- );
237
- }
238
- }
252
+ try {
253
+ const inbound = {
254
+ channelId: 'pier',
255
+ accountId: 'default',
256
+ senderId: `pier:${job.meta?.sender ?? 'anonymous'}`,
257
+ text: job.task,
258
+ metadata: {
259
+ pierJobId: job.id,
260
+ pierNatsMsg: msg,
261
+ pierStartTime: startTime,
262
+ pierMeta: job.meta,
263
+ },
264
+ };
239
265
 
240
- logger.info(
241
- `[pier-connector] Subscription loop ended. ` +
242
- `Total: ${jobsReceived} received, ${jobsCompleted} completed, ${jobsFailed} failed`,
243
- );
244
- })();
266
+ if (job.systemPrompt) inbound.systemPrompt = job.systemPrompt;
245
267
 
268
+ if (api.runtime?.sendIncoming) {
269
+ await api.runtime.sendIncoming(inbound);
270
+ } else {
271
+ throw new Error('Agent runtime not available');
272
+ }
273
+ } catch (err) {
274
+ jobsFailed++;
275
+ safeRespond(msg, createErrorPayload({
276
+ id: job.id,
277
+ errorCode: 'EXECUTION_FAILED',
278
+ errorMessage: err.message,
279
+ workerId: config.nodeId,
280
+ walletAddress: config.walletAddress,
281
+ }));
282
+ }
283
+ }
246
284
  // Monitor connection closure
247
285
  nc.closed().then((err) => {
248
286
  connectionStatus = 'disconnected';
287
+ if (heartbeatTimer) clearInterval(heartbeatTimer);
249
288
  if (err) {
250
- logger.error(
251
- `[pier-connector] NATS connection closed with error: ${err.message}`,
252
- );
289
+ logger.error(`[pier-connector] NATS connection closed with error: ${err.message}`);
253
290
  } else {
254
291
  logger.info('[pier-connector] NATS connection closed gracefully');
255
292
  }
256
293
  });
257
294
  } catch (err) {
258
295
  connectionStatus = 'error';
296
+ if (heartbeatTimer) clearInterval(heartbeatTimer);
259
297
  logger.error(`[pier-connector] ✖ Failed to start: ${err.message}`);
260
- logger.error(`[pier-connector] Stack: ${err.stack}`);
261
298
  }
262
299
  },
263
300
 
@@ -393,6 +430,8 @@ export default function register(api) {
393
430
  text: [
394
431
  `**Pier Connector Status**`,
395
432
  `• Connection: ${connectionStatus}`,
433
+ `• Node ID: ${config.nodeId || 'Unregistered'}`,
434
+ `• API URL: ${config.apiUrl}`,
396
435
  `• NATS URL: ${config.natsUrl}`,
397
436
  `• Subscribe: ${config.subject}`,
398
437
  `• Publish: ${config.publishSubject}`,
@@ -403,5 +442,75 @@ export default function register(api) {
403
442
  },
404
443
  });
405
444
 
445
+ // ── 5. Register CLI Setup Command ──────────────────────────────────
446
+
447
+ api.registerCli(
448
+ ({ program }) => {
449
+ program
450
+ .command('setup')
451
+ .description('Interactively configure the Pier connector settings')
452
+ .action(async () => {
453
+ const currentConfig = resolveConfig();
454
+
455
+ console.log('\n🚢 \x1b[1m\x1b[36mPier Connector Setup\x1b[0m');
456
+ console.log('Let\'s configure your OpenClaw node for the Pier job marketplace.\n');
457
+
458
+ const answers = await inquirer.prompt([
459
+ {
460
+ type: 'input',
461
+ name: 'apiUrl',
462
+ message: 'Pier API Base URL:',
463
+ default: currentConfig.apiUrl,
464
+ },
465
+ {
466
+ type: 'password',
467
+ name: 'apiKey',
468
+ message: 'User API Key:',
469
+ default: currentConfig.apiKey,
470
+ },
471
+ {
472
+ type: 'input',
473
+ name: 'natsUrl',
474
+ message: 'NATS WebSocket URL (leave empty for auto):',
475
+ default: currentConfig.natsUrl || '',
476
+ },
477
+ {
478
+ type: 'input',
479
+ name: 'walletAddress',
480
+ message: 'Wallet Address (for rewards):',
481
+ default: currentConfig.walletAddress,
482
+ },
483
+ ]);
484
+
485
+ // Determine how to save. According to standard OpenClaw plugin patterns
486
+ // configurations are either written to user config or output instructions.
487
+ // We'll output the configuration block for the user to copy/paste if direct edit fails.
488
+ console.log('\n✅ \x1b[32mConfiguration Captured!\x1b[0m\n');
489
+
490
+ const newConfigBlock = {
491
+ plugins: {
492
+ entries: {
493
+ "pier-connector": {
494
+ enabled: true,
495
+ config: {
496
+ apiUrl: answers.apiUrl,
497
+ apiKey: answers.apiKey,
498
+ natsUrl: answers.natsUrl || undefined,
499
+ walletAddress: answers.walletAddress
500
+ }
501
+ }
502
+ }
503
+ }
504
+ };
505
+
506
+ console.log('Currently, CLI automatic config writes depend on the framework host.');
507
+ console.log('Please ensure your \x1b[1m\x1b[33mopenclaw.config.json\x1b[0m includes the following block:\n');
508
+ console.log(JSON.stringify(newConfigBlock, null, 2));
509
+ console.log('\nRestart OpenClaw to apply changes.');
510
+ });
511
+ },
512
+ { commands: ['pier'] }
513
+ );
514
+
406
515
  logger.info('[pier-connector] Plugin registered');
407
516
  }
package/src/protocol.js CHANGED
@@ -14,9 +14,10 @@ export const PROTOCOL_VERSION = '1.0';
14
14
  * @param {string} [params.systemPrompt]
15
15
  * @param {number} [params.timeoutMs]
16
16
  * @param {object} [params.meta]
17
+ * @param {string} [params.targetNodeId]
17
18
  * @returns {object}
18
19
  */
19
- export function createRequestPayload({ task, systemPrompt, timeoutMs, meta }) {
20
+ export function createRequestPayload({ task, systemPrompt, timeoutMs, meta, targetNodeId }) {
20
21
  return {
21
22
  version: PROTOCOL_VERSION,
22
23
  id: crypto.randomUUID(),
@@ -25,6 +26,7 @@ export function createRequestPayload({ task, systemPrompt, timeoutMs, meta }) {
25
26
  systemPrompt,
26
27
  timeoutMs: timeoutMs || 60000,
27
28
  meta: meta || {},
29
+ targetNodeId: targetNodeId || null,
28
30
  };
29
31
  }
30
32
 
@@ -101,6 +103,7 @@ export function normalizeInboundPayload(payload) {
101
103
  task: payload.task,
102
104
  systemPrompt: payload.systemPrompt,
103
105
  meta: payload.meta || {},
106
+ targetNodeId: payload.targetNodeId || null,
104
107
  },
105
108
  };
106
109
  }