@gholl-studio/pier-connector 0.0.2 → 0.0.3

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 CHANGED
@@ -17,7 +17,7 @@
17
17
  - **Pier Account**: A valid account with access to the job marketplace
18
18
  - **Network**: Stable internet connection
19
19
 
20
- > šŸ’” **Note**: The plugin handles all connection logic internally. You only need to configure your **Pier API Token** in the settings. No external message brokers (like NATS) or custom server setups are required.
20
+ > šŸ’” **Note**: The plugin handles registration and heartbeat logic internally. You only need to configure your **Pier API URL/Key** in the settings. The plugin will automatically negotiate NATS connection details with the backend.
21
21
 
22
22
  ## Installation
23
23
 
@@ -34,9 +34,21 @@ openclaw plugins enable pier-connector
34
34
 
35
35
  Restart the OpenClaw Gateway.
36
36
 
37
- ## Configuration
37
+ Restart the OpenClaw Gateway.
38
+
39
+ ## āš™ļø How to Configure
40
+
41
+ There are two ways to configure the plugin:
42
+
43
+ ### 1. Interactive CLI (Recommended)
44
+ Run the following command in your terminal:
45
+ ```bash
46
+ openclaw pier setup
47
+ ```
48
+ This will guide you through setting up your API URL, Key, and Wallet.
38
49
 
39
- Configure via `plugins.entries.pier-connector.config`:
50
+ ### 2. Manual Config
51
+ Add the following to your `openclaw.config.json` under `plugins.entries.pier-connector.config`:
40
52
 
41
53
  ```json
42
54
  {
@@ -45,12 +57,9 @@ Configure via `plugins.entries.pier-connector.config`:
45
57
  "pier-connector": {
46
58
  "enabled": true,
47
59
  "config": {
48
- "natsUrl": "wss://pier.gholl.com/nexus",
49
- "subject": "jobs.worker",
50
- "publishSubject": "jobs.submit",
51
- "queueGroup": "openclaw-workers",
52
- "workerId": "agent-001",
53
- "walletAddress": "0xABC123"
60
+ "apiUrl": "https://pier.gholl.com",
61
+ "apiKey": "your-api-key-here",
62
+ "walletAddress": "0xYourWalletAddress"
54
63
  }
55
64
  }
56
65
  }
@@ -58,14 +67,14 @@ Configure via `plugins.entries.pier-connector.config`:
58
67
  }
59
68
  ```
60
69
 
61
- | Option | Default | Description |
62
- |------------------|--------------------------------|--------------------------------------|
63
- | `natsUrl` | `wss://pier.gholl.com/nexus` | NATS WebSocket server URL |
64
- | `subject` | `jobs.worker` | Subject to subscribe for incoming jobs |
65
- | `publishSubject` | `jobs.submit` | Subject for outbound task publishing |
66
- | `queueGroup` | `openclaw-workers` | NATS Queue Group for load balancing |
67
- | `workerId` | `""` | Unique identifier for this agent worker |
68
- | `walletAddress` | `""` | Wallet address for receiving rewards |
70
+ | Option | Default | Description |
71
+ |-----------------|------------------------------|--------------------------------------------------|
72
+ | `apiUrl` | `https://pier.gholl.com` | Pier Backend API Base URL |
73
+ | `apiKey` | `""` | Your User API Key for platform authentication |
74
+ | `nodeId` | `""` | (Auto-generated) Unique UUID for this node |
75
+ | `secretKey` | `""` | (Auto-generated) Secret key for pulse verification |
76
+ | `walletAddress` | `""` | Wallet address for receiving job rewards |
77
+ | `natsUrl` | `""` | (Optional) Manual NATS override |
69
78
 
70
79
  ## How It Works
71
80
 
@@ -119,8 +128,8 @@ Use `/pier` in any OpenClaw channel to check connector status.
119
128
  "result": "The AI's generated response",
120
129
  "latencyMs": 1450,
121
130
  "worker": {
122
- "id": "agent-001",
123
- "wallet": "0xABC123"
131
+ "id": "uuid-node",
132
+ "wallet": "0xYourWalletAddress"
124
133
  }
125
134
  }
126
135
  ```
@@ -138,8 +147,8 @@ Use `/pier` in any OpenClaw channel to check connector status.
138
147
  "message": "Error details"
139
148
  },
140
149
  "worker": {
141
- "id": "agent-001",
142
- "wallet": "0xABC123"
150
+ "id": "uuid-node",
151
+ "wallet": "0xYourWalletAddress"
143
152
  }
144
153
  }
145
154
  ```
@@ -10,25 +10,20 @@
10
10
  "type": "object",
11
11
  "additionalProperties": false,
12
12
  "properties": {
13
- "apiUrl": {
13
+ "pierApiUrl": {
14
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"
15
+ "default": "https://pier-connector.gholl.com/api/v1",
16
+ "description": "Pier API Endpoint (from Website)"
22
17
  },
23
18
  "nodeId": {
24
19
  "type": "string",
25
20
  "default": "",
26
- "description": "Unique identifier for this agent worker node (UUID)"
21
+ "description": "Bot Node ID (obtain from pier-connector.gholl.com)"
27
22
  },
28
23
  "secretKey": {
29
24
  "type": "string",
30
25
  "default": "",
31
- "description": "Secret key for node authentication and heartbeats"
26
+ "description": "Bot Secret Key (obtain from pier-connector.gholl.com)"
32
27
  },
33
28
  "natsUrl": {
34
29
  "type": "string",
@@ -58,20 +53,16 @@
58
53
  }
59
54
  },
60
55
  "uiHints": {
61
- "apiUrl": {
56
+ "pierApiUrl": {
62
57
  "label": "Pier API URL",
63
- "placeholder": "https://pier.gholl.com"
64
- },
65
- "apiKey": {
66
- "label": "API Key",
67
- "placeholder": "Bearer token ..."
58
+ "placeholder": "https://pier-connector.gholl.com/api/v1"
68
59
  },
69
60
  "nodeId": {
70
- "label": "Node ID",
61
+ "label": "Bot Node ID",
71
62
  "placeholder": "uuid-..."
72
63
  },
73
64
  "secretKey": {
74
- "label": "Secret Key",
65
+ "label": "Bot Secret Key (Secret)",
75
66
  "placeholder": "sk_..."
76
67
  },
77
68
  "natsUrl": {
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.2",
4
+ "version": "0.0.3",
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",
package/src/config.js CHANGED
@@ -6,12 +6,9 @@
6
6
 
7
7
  export const DEFAULTS = Object.freeze({
8
8
  /** Pier Backend API Base URL */
9
- API_URL: 'https://pier.gholl.com',
9
+ PIER_API_URL: 'https://pier-connector.gholl.com/api/v1',
10
10
 
11
- /** User API Key for HTTP Authentication */
12
- API_KEY: '',
13
-
14
- /** NATS WebSocket server URL (usually provided by API) */
11
+ /** NATS WebSocket server URL (usually provided by Heartbeat) */
15
12
  NATS_URL: 'wss://pier.gholl.com/nexus',
16
13
 
17
14
  /** NATS subject to subscribe for incoming jobs */
package/src/index.js CHANGED
@@ -42,12 +42,11 @@ export default function register(api) {
42
42
  function resolveConfig() {
43
43
  const cfg = api.config?.plugins?.entries?.['pier-connector']?.config ?? {};
44
44
  return {
45
- apiUrl: cfg.apiUrl || DEFAULTS.API_URL,
46
- apiKey: cfg.apiKey || DEFAULTS.API_KEY,
45
+ pierApiUrl: cfg.pierApiUrl || DEFAULTS.PIER_API_URL,
47
46
  nodeId: cfg.nodeId || DEFAULTS.NODE_ID,
48
47
  secretKey: cfg.secretKey || DEFAULTS.SECRET_KEY,
49
48
  natsUrl: cfg.natsUrl || DEFAULTS.NATS_URL,
50
- subject: cfg.subject || DEFAULTS.SUBJECT,
49
+ subject: cfg.subject || DEFAULTS.PIER_SUBJECT,
51
50
  publishSubject: cfg.publishSubject || DEFAULTS.PUBLISH_SUBJECT,
52
51
  queueGroup: cfg.queueGroup || DEFAULTS.QUEUE_GROUP,
53
52
  walletAddress: cfg.walletAddress || DEFAULTS.WALLET_ADDRESS,
@@ -57,55 +56,37 @@ export default function register(api) {
57
56
  /** Heartbeat timer reference */
58
57
  let heartbeatTimer = null;
59
58
 
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
-
59
+ // ── Lifecycle: Heartbeat ──────────────────────────────────────────
60
+
61
+ /**
62
+ * Sends a heartbeat to the Pier Backend to stay active in the marketplace.
63
+ */
89
64
  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
- });
65
+ if (!config.nodeId || !config.secretKey) return null;
66
+
67
+ try {
68
+ const resp = await fetch(`${config.pierApiUrl}/nodes/heartbeat`, {
69
+ method: 'POST',
70
+ headers: { 'Content-Type': 'application/json' },
71
+ body: JSON.stringify({
72
+ node_id: config.nodeId,
73
+ secret_key: config.secretKey,
74
+ capabilities: ['translation', 'code-execution', 'reasoning', 'vision'],
75
+ description: `OpenClaw Node (${config.nodeId.substring(0, 8)})`,
76
+ }),
77
+ });
78
+
79
+ if (!resp.ok) {
80
+ const errText = await resp.text();
81
+ throw new Error(`Heartbeat failed (${resp.status}): ${errText}`);
82
+ }
103
83
 
104
- if (!resp.ok) {
105
- throw new Error(`Heartbeat failed (${resp.status})`);
84
+ const data = await resp.json();
85
+ return data.nats_config || null;
86
+ } catch (err) {
87
+ logger.warn(`[pier-connector] Heartbeat failed: ${err.message}`);
88
+ return null;
106
89
  }
107
-
108
- return await resp.json();
109
90
  }
110
91
 
111
92
  // ── 1. Register messaging channel ──────────────────────────────────
@@ -180,64 +161,84 @@ export default function register(api) {
180
161
  id: 'pier-connector',
181
162
 
182
163
  start: async () => {
164
+ const config = resolveConfig();
183
165
  logger.info('[pier-connector] šŸš€ Starting background service …');
184
- let config = resolveConfig();
185
166
 
186
167
  try {
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
168
+ // 1. Mandatory credentials check
169
+ if (!config.nodeId || !config.secretKey) {
170
+ logger.warn('[pier-connector] āš ļø Bot Node ID or Secret is missing.');
171
+ logger.warn('[pier-connector] Please run "openclaw pier setup" or go to pier-connector.gholl.com to create a bot.');
172
+ connectionStatus = 'error';
173
+ return;
174
+ }
175
+
176
+ connectionStatus = 'connecting';
177
+
178
+ // 2. Initial Pulse & Config fetch
179
+ logger.info(`[pier-connector] šŸ’“ Sending initial pulse to ${config.pierApiUrl}...`);
180
+ const natsUpdate = await heartbeatNode(config);
181
+ const activeNatsUrl = natsUpdate?.url || config.natsUrl;
182
+
183
+ logger.info(`[pier-connector] Node ID : ${config.nodeId}`);
184
+ logger.info(`[pier-connector] NATS URL : ${activeNatsUrl}`);
185
+
186
+ // 3. Start Heartbeat Timer
199
187
  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
- }
188
+ const currentConfig = resolveConfig();
189
+ if (!currentConfig.nodeId) return;
190
+ await heartbeatNode(currentConfig);
207
191
  };
208
- await runHeartbeat();
209
192
  heartbeatTimer = setInterval(runHeartbeat, 60000);
210
193
 
211
- // 3. Connect to NATS
194
+ // 4. Connect to NATS
212
195
  nc = await createNatsConnection(activeNatsUrl, logger);
213
196
  connectionStatus = 'connected';
214
197
  connectedAt = new Date();
215
198
 
216
- // 4. Subscribe to Subjects
217
- // Public pool
218
- nc.subscribe(config.subject, {
199
+ // 5. Subscribe to Subjects
200
+ const publicSubject = config.subject;
201
+ const privateSubject = `jobs.node.${config.nodeId}`;
202
+
203
+ logger.info(`[pier-connector] šŸ‘‚ Listening to Marketplace: ${publicSubject}`);
204
+ logger.info(`[pier-connector] šŸ‘‚ Listening to Direct Messages: ${privateSubject}`);
205
+
206
+ // Public pool with load balancing
207
+ nc.subscribe(publicSubject, {
219
208
  queue: config.queueGroup,
220
209
  callback: (err, msg) => handleMessage(msg)
221
210
  });
222
- // Private direct channel
223
- nc.subscribe(`jobs.node.${config.nodeId}`, {
211
+ // Private direct channel (no queue group for broadcast/direct)
212
+ nc.subscribe(privateSubject, {
224
213
  callback: (err, msg) => handleMessage(msg)
225
214
  });
226
215
 
227
- logger.info(
228
- `[pier-connector] āœ” Subscribed to "${config.subject}" and "jobs.node.${config.nodeId}"`,
229
- );
230
-
231
- // Message handling logic moved to a reusable function
216
+ // Unified Message Handler
232
217
  async function handleMessage(msg) {
233
- jobsReceived++;
234
218
  const startTime = performance.now();
219
+ const rawData = new TextDecoder().decode(msg.data);
220
+
221
+ let payload;
222
+ try {
223
+ payload = JSON.parse(rawData);
224
+ } catch (e) {
225
+ logger.error(`[pier-connector] Failed to parse JSON: ${rawData.substring(0, 100)}`);
226
+ return;
227
+ }
235
228
 
229
+ // V1.1 FEATURE: Task Poisoning / Poaching Protection
230
+ if (payload.assigned_node_id && payload.assigned_node_id !== config.nodeId) {
231
+ // Silent ignore - don't reply, don't log heavily
232
+ return;
233
+ }
234
+
235
+ jobsReceived++;
236
236
  const parsed = parseJob(msg, logger);
237
237
  if (!parsed.ok) {
238
238
  jobsFailed++;
239
+ logger.error(`[pier-connector] Job parse error: ${parsed.error}`);
239
240
  safeRespond(msg, createErrorPayload({
240
- id: msg.subject,
241
+ id: 'unknown',
241
242
  errorCode: 'PARSE_ERROR',
242
243
  errorMessage: parsed.error,
243
244
  workerId: config.nodeId,
@@ -281,6 +282,7 @@ export default function register(api) {
281
282
  }));
282
283
  }
283
284
  }
285
+
284
286
  // Monitor connection closure
285
287
  nc.closed().then((err) => {
286
288
  connectionStatus = 'disconnected';
@@ -295,6 +297,7 @@ export default function register(api) {
295
297
  connectionStatus = 'error';
296
298
  if (heartbeatTimer) clearInterval(heartbeatTimer);
297
299
  logger.error(`[pier-connector] āœ– Failed to start: ${err.message}`);
300
+ logger.error(err.stack);
298
301
  }
299
302
  },
300
303
 
@@ -430,8 +433,8 @@ export default function register(api) {
430
433
  text: [
431
434
  `**Pier Connector Status**`,
432
435
  `• Connection: ${connectionStatus}`,
433
- `• Node ID: ${config.nodeId || 'Unregistered'}`,
434
- `• API URL: ${config.apiUrl}`,
436
+ `• Node ID: ${config.nodeId || 'N/A'}`,
437
+ `• API URL: ${config.pierApiUrl}`,
435
438
  `• NATS URL: ${config.natsUrl}`,
436
439
  `• Subscribe: ${config.subject}`,
437
440
  `• Publish: ${config.publishSubject}`,
@@ -446,45 +449,49 @@ export default function register(api) {
446
449
 
447
450
  api.registerCli(
448
451
  ({ program }) => {
449
- program
452
+ // Find or create the 'pier' command namespace to avoid root conflicts
453
+ let pier = program.commands.find(c => c.name() === 'pier');
454
+ if (!pier) {
455
+ pier = program.command('pier').description('Pier connector commands');
456
+ }
457
+
458
+ pier
450
459
  .command('setup')
451
460
  .description('Interactively configure the Pier connector settings')
452
461
  .action(async () => {
453
462
  const currentConfig = resolveConfig();
454
463
 
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');
464
+ console.log('\n🚢 \x1b[1m\x1b[36mPier Connector Setup (V1.1)\x1b[0m');
465
+ console.log('Please register your bot at \x1b[1m\x1b[33mhttps://pier-connector.gholl.com\x1b[0m first.\n');
457
466
 
458
467
  const answers = await inquirer.prompt([
459
468
  {
460
469
  type: 'input',
461
- name: 'apiUrl',
462
- message: 'Pier API Base URL:',
463
- default: currentConfig.apiUrl,
470
+ name: 'pierApiUrl',
471
+ message: 'Pier API URL:',
472
+ default: currentConfig.pierApiUrl,
464
473
  },
465
474
  {
466
- type: 'password',
467
- name: 'apiKey',
468
- message: 'User API Key:',
469
- default: currentConfig.apiKey,
475
+ type: 'input',
476
+ name: 'nodeId',
477
+ message: 'Bot Node ID (UUID):',
478
+ default: currentConfig.nodeId,
470
479
  },
471
480
  {
472
- type: 'input',
473
- name: 'natsUrl',
474
- message: 'NATS WebSocket URL (leave empty for auto):',
475
- default: currentConfig.natsUrl || '',
481
+ type: 'password',
482
+ name: 'secretKey',
483
+ message: 'Bot Secret Key:',
484
+ default: currentConfig.secretKey,
476
485
  },
477
486
  {
478
487
  type: 'input',
479
488
  name: 'walletAddress',
480
- message: 'Wallet Address (for rewards):',
489
+ message: 'Your Wallet Address (for payout):',
481
490
  default: currentConfig.walletAddress,
482
491
  },
483
492
  ]);
484
493
 
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.
494
+ // Determine how to save.
488
495
  console.log('\nāœ… \x1b[32mConfiguration Captured!\x1b[0m\n');
489
496
 
490
497
  const newConfigBlock = {
@@ -493,9 +500,9 @@ export default function register(api) {
493
500
  "pier-connector": {
494
501
  enabled: true,
495
502
  config: {
496
- apiUrl: answers.apiUrl,
497
- apiKey: answers.apiKey,
498
- natsUrl: answers.natsUrl || undefined,
503
+ pierApiUrl: answers.pierApiUrl,
504
+ nodeId: answers.nodeId,
505
+ secretKey: answers.secretKey,
499
506
  walletAddress: answers.walletAddress
500
507
  }
501
508
  }