@gholl-studio/pier-connector 0.0.2 → 0.0.4

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,25 @@
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"
15
+ "default": "https://pier-connector.gholl.com/api/v1",
16
+ "description": "Pier API Endpoint (from Website)"
17
17
  },
18
- "apiKey": {
18
+ "nodeId": {
19
19
  "type": "string",
20
20
  "default": "",
21
- "description": "User API Key for platform authentication"
21
+ "description": "Bot Node ID (obtain from pier-connector.gholl.com)"
22
22
  },
23
- "nodeId": {
23
+ "secretKey": {
24
24
  "type": "string",
25
25
  "default": "",
26
- "description": "Unique identifier for this agent worker node (UUID)"
26
+ "description": "Bot Secret Key (obtain from pier-connector.gholl.com)"
27
27
  },
28
- "secretKey": {
28
+ "privateKey": {
29
29
  "type": "string",
30
30
  "default": "",
31
- "description": "Secret key for node authentication and heartbeats"
31
+ "description": "(Optional/Advanced) Wallet Private Key for AI Auto-Hosting"
32
32
  },
33
33
  "natsUrl": {
34
34
  "type": "string",
@@ -50,6 +50,11 @@
50
50
  "default": "openclaw-workers",
51
51
  "description": "NATS Queue Group to join for load balancing tasks"
52
52
  },
53
+ "agentId": {
54
+ "type": "string",
55
+ "default": "",
56
+ "description": "(Optional) The Agent ID to bind this connector to for context handling"
57
+ },
53
58
  "walletAddress": {
54
59
  "type": "string",
55
60
  "default": "",
@@ -58,22 +63,22 @@
58
63
  }
59
64
  },
60
65
  "uiHints": {
61
- "apiUrl": {
66
+ "pierApiUrl": {
62
67
  "label": "Pier API URL",
63
- "placeholder": "https://pier.gholl.com"
64
- },
65
- "apiKey": {
66
- "label": "API Key",
67
- "placeholder": "Bearer token ..."
68
+ "placeholder": "https://pier-connector.gholl.com/api/v1"
68
69
  },
69
70
  "nodeId": {
70
- "label": "Node ID",
71
+ "label": "Bot Node ID",
71
72
  "placeholder": "uuid-..."
72
73
  },
73
74
  "secretKey": {
74
- "label": "Secret Key",
75
+ "label": "Bot Secret Key (Secret)",
75
76
  "placeholder": "sk_..."
76
77
  },
78
+ "privateKey": {
79
+ "label": "Private Key (Optional Auto-Host)",
80
+ "placeholder": "0x..."
81
+ },
77
82
  "natsUrl": {
78
83
  "label": "NATS WebSocket URL (Override)",
79
84
  "placeholder": "wss://pier.gholl.com/nexus"
@@ -90,6 +95,10 @@
90
95
  "label": "Queue Group",
91
96
  "placeholder": "openclaw-workers"
92
97
  },
98
+ "agentId": {
99
+ "label": "Bound Agent ID",
100
+ "placeholder": "uuid-..."
101
+ },
93
102
  "walletAddress": {
94
103
  "label": "Wallet Address (Rewards)",
95
104
  "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.2",
4
+ "version": "0.0.4",
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",
@@ -32,6 +32,7 @@
32
32
  "license": "MIT",
33
33
  "dependencies": {
34
34
  "@nats-io/transport-node": "^3.0.0",
35
+ "ethers": "^6.16.0",
35
36
  "inquirer": "^13.3.0"
36
37
  },
37
38
  "peerDependencies": {
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 */
@@ -29,6 +26,12 @@ export const DEFAULTS = Object.freeze({
29
26
  /** Secret key for node authentication and heartbeats */
30
27
  SECRET_KEY: '',
31
28
 
29
+ /** Optional Private Key for auto-registration via Wallet Signature (Advanced) */
30
+ PRIVATE_KEY: '',
31
+
32
32
  /** Optional Wallet address to receive points/payments for completed tasks */
33
33
  WALLET_ADDRESS: '',
34
+
35
+ /** Optional Agent ID to bind tasks to, instead of creating new sessions */
36
+ AGENT_ID: '',
34
37
  });
package/src/index.js CHANGED
@@ -13,6 +13,9 @@ 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
15
  import inquirer from 'inquirer';
16
+ import { ethers } from 'ethers';
17
+ import fs from 'fs';
18
+ import path from 'path';
16
19
 
17
20
  /**
18
21
  * OpenClaw plugin register function.
@@ -42,70 +45,61 @@ export default function register(api) {
42
45
  function resolveConfig() {
43
46
  const cfg = api.config?.plugins?.entries?.['pier-connector']?.config ?? {};
44
47
  return {
45
- apiUrl: cfg.apiUrl || DEFAULTS.API_URL,
46
- apiKey: cfg.apiKey || DEFAULTS.API_KEY,
48
+ pierApiUrl: cfg.pierApiUrl || DEFAULTS.PIER_API_URL,
47
49
  nodeId: cfg.nodeId || DEFAULTS.NODE_ID,
48
50
  secretKey: cfg.secretKey || DEFAULTS.SECRET_KEY,
51
+ privateKey: cfg.privateKey || process.env.PIER_PRIVATE_KEY || DEFAULTS.PRIVATE_KEY,
49
52
  natsUrl: cfg.natsUrl || DEFAULTS.NATS_URL,
50
- subject: cfg.subject || DEFAULTS.SUBJECT,
53
+ subject: cfg.subject || DEFAULTS.PIER_SUBJECT,
51
54
  publishSubject: cfg.publishSubject || DEFAULTS.PUBLISH_SUBJECT,
52
55
  queueGroup: cfg.queueGroup || DEFAULTS.QUEUE_GROUP,
56
+ agentId: cfg.agentId || DEFAULTS.AGENT_ID,
53
57
  walletAddress: cfg.walletAddress || DEFAULTS.WALLET_ADDRESS,
54
58
  };
55
59
  }
56
60
 
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
- });
61
+ /** Runtime config caching to remember newly generated auto-credentials */
62
+ let runtimeConfigCache = null;
77
63
 
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 };
64
+ function getActiveConfig() {
65
+ if (runtimeConfigCache) return runtimeConfigCache;
66
+ return resolveConfig();
87
67
  }
88
68
 
69
+ /** Heartbeat timer reference */
70
+ let heartbeatTimer = null;
71
+
72
+ // ── Lifecycle: Heartbeat ──────────────────────────────────────────
73
+
74
+ /**
75
+ * Sends a heartbeat to the Pier Backend to stay active in the marketplace.
76
+ */
89
77
  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
- });
78
+ if (!config.nodeId || !config.secretKey) return null;
79
+
80
+ try {
81
+ const resp = await fetch(`${config.pierApiUrl}/nodes/heartbeat`, {
82
+ method: 'POST',
83
+ headers: { 'Content-Type': 'application/json' },
84
+ body: JSON.stringify({
85
+ node_id: config.nodeId,
86
+ secret_key: config.secretKey,
87
+ capabilities: ['translation', 'code-execution', 'reasoning', 'vision'],
88
+ description: `OpenClaw Node (${config.nodeId.substring(0, 8)})`,
89
+ }),
90
+ });
91
+
92
+ if (!resp.ok) {
93
+ const errText = await resp.text();
94
+ throw new Error(`Heartbeat failed (${resp.status}): ${errText}`);
95
+ }
103
96
 
104
- if (!resp.ok) {
105
- throw new Error(`Heartbeat failed (${resp.status})`);
97
+ const data = await resp.json();
98
+ return data.nats_config || null;
99
+ } catch (err) {
100
+ logger.warn(`[pier-connector] Heartbeat failed: ${err.message}`);
101
+ return null;
106
102
  }
107
-
108
- return await resp.json();
109
103
  }
110
104
 
111
105
  // ── 1. Register messaging channel ──────────────────────────────────
@@ -154,8 +148,8 @@ export default function register(api) {
154
148
  id: jobId,
155
149
  reply: text,
156
150
  latencyMs: elapsed ? Number(elapsed) : undefined,
157
- workerId: resolveConfig().nodeId,
158
- walletAddress: resolveConfig().walletAddress,
151
+ workerId: getActiveConfig().nodeId,
152
+ walletAddress: getActiveConfig().walletAddress,
159
153
  });
160
154
 
161
155
  safeRespond(msg, responsePayload);
@@ -180,64 +174,134 @@ export default function register(api) {
180
174
  id: 'pier-connector',
181
175
 
182
176
  start: async () => {
177
+ const config = resolveConfig();
178
+ runtimeConfigCache = config;
183
179
  logger.info('[pier-connector] 🚀 Starting background service …');
184
- let config = resolveConfig();
185
180
 
186
181
  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
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}`);
182
+ // 1. Mandatory credentials check or Auto Register
183
+ if (!config.nodeId || !config.secretKey) {
184
+ if (config.privateKey) {
185
+ logger.info('[pier-connector] 🛠 No Node ID found. Executing automatic registration via Private Key...');
186
+ try {
187
+ const wallet = new ethers.Wallet(config.privateKey);
188
+ const address = wallet.address;
189
+ logger.info(`[pier-connector] 🛠 Wallet Address: ${address}`);
190
+
191
+ const challengeRes = await fetch(`${config.pierApiUrl}/auth/challenge?wallet_address=${address}`);
192
+ if (!challengeRes.ok) throw new Error("Failed to get challenge");
193
+ const { challenge } = await challengeRes.json();
194
+
195
+ const signature = await wallet.signMessage(challenge);
196
+
197
+ const loginRes = await fetch(`${config.pierApiUrl}/auth/login`, {
198
+ method: 'POST',
199
+ headers: { 'Content-Type': 'application/json' },
200
+ body: JSON.stringify({ wallet_address: address, challenge, signature })
201
+ });
202
+ if (!loginRes.ok) throw new Error("Failed to login");
203
+ const { api_key } = await loginRes.json();
204
+
205
+ logger.info(`[pier-connector] 🛠 Authenticated. Registering node...`);
206
+
207
+ const hostName = api?.getRuntimeInfo?.()?.hostname ?? 'Auto-Node';
208
+ const regRes = await fetch(`${config.pierApiUrl}/nodes/register`, {
209
+ method: 'POST',
210
+ headers: {
211
+ 'Content-Type': 'application/json',
212
+ 'Authorization': `Bearer ${api_key}`
213
+ },
214
+ body: JSON.stringify({ wallet_address: address, name: hostName })
215
+ });
216
+
217
+ if (!regRes.ok) throw new Error("Failed to register node");
218
+ const { id, secret_key } = await regRes.json();
219
+
220
+ logger.info(`[pier-connector] ✔ Node registered automatically: ${id}`);
221
+ config.nodeId = id;
222
+ config.secretKey = secret_key;
223
+ if (!config.walletAddress) {
224
+ config.walletAddress = address;
225
+ }
226
+ } catch (err) {
227
+ logger.error(`[pier-connector] ✖ Auto-registration failed: ${err.message}`);
228
+ connectionStatus = 'error';
229
+ return;
230
+ }
231
+ } else {
232
+ logger.warn('[pier-connector] ⚠️ Bot Node ID or Secret is missing.');
233
+ logger.warn('[pier-connector] Please run "openclaw pier setup" or provide a privateKey for auto-hosting.');
234
+ connectionStatus = 'error';
235
+ return;
206
236
  }
237
+ }
238
+
239
+ connectionStatus = 'connecting';
240
+
241
+ // 2. Initial Pulse & Config fetch
242
+ logger.info(`[pier-connector] 💓 Sending initial pulse to ${config.pierApiUrl}...`);
243
+ const natsUpdate = await heartbeatNode(config);
244
+ const activeNatsUrl = natsUpdate?.url || config.natsUrl;
245
+
246
+ logger.info(`[pier-connector] Node ID : ${config.nodeId}`);
247
+ logger.info(`[pier-connector] NATS URL : ${activeNatsUrl}`);
248
+
249
+ // 3. Start Heartbeat Timer
250
+ const runHeartbeat = async () => {
251
+ const currentConfig = getActiveConfig();
252
+ if (!currentConfig.nodeId) return;
253
+ await heartbeatNode(currentConfig);
207
254
  };
208
- await runHeartbeat();
209
255
  heartbeatTimer = setInterval(runHeartbeat, 60000);
210
256
 
211
- // 3. Connect to NATS
257
+ // 4. Connect to NATS
212
258
  nc = await createNatsConnection(activeNatsUrl, logger);
213
259
  connectionStatus = 'connected';
214
260
  connectedAt = new Date();
215
261
 
216
- // 4. Subscribe to Subjects
217
- // Public pool
218
- nc.subscribe(config.subject, {
262
+ // 5. Subscribe to Subjects
263
+ const publicSubject = config.subject;
264
+ const privateSubject = `jobs.node.${config.nodeId}`;
265
+
266
+ logger.info(`[pier-connector] 👂 Listening to Marketplace: ${publicSubject}`);
267
+ logger.info(`[pier-connector] 👂 Listening to Direct Messages: ${privateSubject}`);
268
+
269
+ // Public pool with load balancing
270
+ nc.subscribe(publicSubject, {
219
271
  queue: config.queueGroup,
220
272
  callback: (err, msg) => handleMessage(msg)
221
273
  });
222
- // Private direct channel
223
- nc.subscribe(`jobs.node.${config.nodeId}`, {
274
+ // Private direct channel (no queue group for broadcast/direct)
275
+ nc.subscribe(privateSubject, {
224
276
  callback: (err, msg) => handleMessage(msg)
225
277
  });
226
278
 
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
279
+ // Unified Message Handler
232
280
  async function handleMessage(msg) {
233
- jobsReceived++;
234
281
  const startTime = performance.now();
282
+ const rawData = new TextDecoder().decode(msg.data);
283
+
284
+ let payload;
285
+ try {
286
+ payload = JSON.parse(rawData);
287
+ } catch (e) {
288
+ logger.error(`[pier-connector] Failed to parse JSON: ${rawData.substring(0, 100)}`);
289
+ return;
290
+ }
235
291
 
292
+ // V1.1 FEATURE: Task Poisoning / Poaching Protection
293
+ if (payload.assigned_node_id && payload.assigned_node_id !== config.nodeId) {
294
+ // Silent ignore - don't reply, don't log heavily
295
+ return;
296
+ }
297
+
298
+ jobsReceived++;
236
299
  const parsed = parseJob(msg, logger);
237
300
  if (!parsed.ok) {
238
301
  jobsFailed++;
302
+ logger.error(`[pier-connector] Job parse error: ${parsed.error}`);
239
303
  safeRespond(msg, createErrorPayload({
240
- id: msg.subject,
304
+ id: 'unknown',
241
305
  errorCode: 'PARSE_ERROR',
242
306
  errorMessage: parsed.error,
243
307
  workerId: config.nodeId,
@@ -255,6 +319,7 @@ export default function register(api) {
255
319
  accountId: 'default',
256
320
  senderId: `pier:${job.meta?.sender ?? 'anonymous'}`,
257
321
  text: job.task,
322
+ assignedAgentId: config.agentId || undefined,
258
323
  metadata: {
259
324
  pierJobId: job.id,
260
325
  pierNatsMsg: msg,
@@ -281,6 +346,7 @@ export default function register(api) {
281
346
  }));
282
347
  }
283
348
  }
349
+
284
350
  // Monitor connection closure
285
351
  nc.closed().then((err) => {
286
352
  connectionStatus = 'disconnected';
@@ -295,6 +361,7 @@ export default function register(api) {
295
361
  connectionStatus = 'error';
296
362
  if (heartbeatTimer) clearInterval(heartbeatTimer);
297
363
  logger.error(`[pier-connector] ✖ Failed to start: ${err.message}`);
364
+ logger.error(err.stack);
298
365
  }
299
366
  },
300
367
 
@@ -357,7 +424,7 @@ export default function register(api) {
357
424
  };
358
425
  }
359
426
 
360
- const config = resolveConfig();
427
+ const config = getActiveConfig();
361
428
  const timeout = params.timeoutMs || 60000;
362
429
 
363
430
  const taskPayload = createRequestPayload({
@@ -421,7 +488,7 @@ export default function register(api) {
421
488
  name: 'pier',
422
489
  description: 'Show Pier connector status',
423
490
  handler: () => {
424
- const config = resolveConfig();
491
+ const config = getActiveConfig();
425
492
  const uptime = connectedAt
426
493
  ? `${Math.round((Date.now() - connectedAt.getTime()) / 1000)}s`
427
494
  : 'N/A';
@@ -430,8 +497,8 @@ export default function register(api) {
430
497
  text: [
431
498
  `**Pier Connector Status**`,
432
499
  `• Connection: ${connectionStatus}`,
433
- `• Node ID: ${config.nodeId || 'Unregistered'}`,
434
- `• API URL: ${config.apiUrl}`,
500
+ `• Node ID: ${config.nodeId || 'N/A'}`,
501
+ `• API URL: ${config.pierApiUrl}`,
435
502
  `• NATS URL: ${config.natsUrl}`,
436
503
  `• Subscribe: ${config.subject}`,
437
504
  `• Publish: ${config.publishSubject}`,
@@ -446,67 +513,180 @@ export default function register(api) {
446
513
 
447
514
  api.registerCli(
448
515
  ({ program }) => {
449
- program
516
+ // Find or create the 'pier' command namespace to avoid root conflicts
517
+ let pier = program.commands.find(c => c.name() === 'pier');
518
+ if (!pier) {
519
+ pier = program.command('pier').description('Pier connector commands');
520
+ }
521
+
522
+ pier
450
523
  .command('setup')
451
524
  .description('Interactively configure the Pier connector settings')
452
525
  .action(async () => {
453
526
  const currentConfig = resolveConfig();
454
527
 
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');
528
+ console.log('\n🚢 \x1b[1m\x1b[36mPier Connector Setup (V1.1)\x1b[0m');
529
+ console.log('You can register manually or let the Auto-Host (Advanced) feature do it for you using a Wallet Private Key.\n');
457
530
 
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
- },
531
+ const { setupMethod } = await inquirer.prompt([
477
532
  {
478
- type: 'input',
479
- name: 'walletAddress',
480
- message: 'Wallet Address (for rewards):',
481
- default: currentConfig.walletAddress,
482
- },
533
+ type: 'list',
534
+ name: 'setupMethod',
535
+ message: 'How would you like to configure your node credentials?',
536
+ choices: [
537
+ { name: 'Manual Entry (Recommended) - I have a Node ID and Secret Key', value: 'manual' },
538
+ { name: 'Auto-Host (Advanced) - Register automatically using my Wallet Private Key', value: 'auto' }
539
+ ]
540
+ }
483
541
  ]);
484
542
 
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.
543
+ let finalNodeId = currentConfig.nodeId;
544
+ let finalSecretKey = currentConfig.secretKey;
545
+ let finalPrivateKey = currentConfig.privateKey;
546
+ let finalWallet = currentConfig.walletAddress;
547
+
548
+ if (setupMethod === 'manual') {
549
+ const manualAnswers = await inquirer.prompt([
550
+ { type: 'input', name: 'pierApiUrl', message: 'Pier API URL:', default: currentConfig.pierApiUrl },
551
+ { type: 'input', name: 'nodeId', message: 'Bot Node ID (UUID):', default: finalNodeId },
552
+ { type: 'password', name: 'secretKey', message: 'Bot Secret Key:', default: finalSecretKey },
553
+ { type: 'input', name: 'walletAddress', message: 'Your Wallet Address (for payout):', default: finalWallet }
554
+ ]);
555
+ finalNodeId = manualAnswers.nodeId;
556
+ finalSecretKey = manualAnswers.secretKey;
557
+ finalWallet = manualAnswers.walletAddress;
558
+ currentConfig.pierApiUrl = manualAnswers.pierApiUrl;
559
+ } else {
560
+ const autoAnswers = await inquirer.prompt([
561
+ { type: 'input', name: 'pierApiUrl', message: 'Pier API URL:', default: currentConfig.pierApiUrl },
562
+ { type: 'password', name: 'privateKey', message: 'Your Wallet Private Key (Hex, 0x...):', default: finalPrivateKey }
563
+ ]);
564
+ console.log('\n\x1b[36mRegistering your node...\x1b[0m');
565
+ try {
566
+ const wallet = new ethers.Wallet(autoAnswers.privateKey);
567
+ console.log(`\x1b[32m✔ Loaded wallet: ${wallet.address}\x1b[0m`);
568
+ finalWallet = finalWallet || wallet.address;
569
+ finalPrivateKey = autoAnswers.privateKey;
570
+ currentConfig.pierApiUrl = autoAnswers.pierApiUrl;
571
+
572
+ const challengeRes = await fetch(`${currentConfig.pierApiUrl}/auth/challenge?wallet_address=${wallet.address}`);
573
+ if (!challengeRes.ok) throw new Error("Failed to get challenge");
574
+ const { challenge } = await challengeRes.json();
575
+
576
+ const signature = await wallet.signMessage(challenge);
577
+
578
+ const loginRes = await fetch(`${currentConfig.pierApiUrl}/auth/login`, {
579
+ method: 'POST',
580
+ headers: { 'Content-Type': 'application/json' },
581
+ body: JSON.stringify({ wallet_address: wallet.address, challenge, signature })
582
+ });
583
+ if (!loginRes.ok) throw new Error("Failed to login");
584
+ const { api_key } = await loginRes.json();
585
+
586
+ const hostName = api?.getRuntimeInfo?.()?.hostname ?? 'Auto-Node';
587
+ const regRes = await fetch(`${currentConfig.pierApiUrl}/nodes/register`, {
588
+ method: 'POST',
589
+ headers: {
590
+ 'Content-Type': 'application/json',
591
+ 'Authorization': `Bearer ${api_key}`
592
+ },
593
+ body: JSON.stringify({ wallet_address: wallet.address, name: hostName })
594
+ });
595
+
596
+ if (!regRes.ok) throw new Error(`Failed to auto-register node (${regRes.status})`);
597
+ const { id, secret_key } = await regRes.json();
598
+
599
+ finalNodeId = id;
600
+ finalSecretKey = secret_key;
601
+ console.log(`\x1b[32m✔ Node registered automatically! Node ID: ${id}\x1b[0m`);
602
+ } catch (err) {
603
+ console.error(`\x1b[31m✖ Failed to auto-register: ${err.message}\x1b[0m`);
604
+ return; // Stop setup
605
+ }
606
+ }
607
+
608
+ // ── Select Agent Binding ──
609
+ let finalAgentId = currentConfig.agentId;
610
+ try {
611
+ let agentsInfo = [];
612
+ if (api.runtime && typeof api.runtime.getAgents === 'function') {
613
+ const agentsMap = await api.runtime.getAgents();
614
+ if (agentsMap) {
615
+ agentsInfo = Object.entries(agentsMap).map(([id, info]) => ({
616
+ name: `${info.name || 'Unnamed Agent'} (${id})`,
617
+ value: id
618
+ }));
619
+ }
620
+ }
621
+
622
+ if (agentsInfo.length > 0) {
623
+ agentsInfo.unshift({ name: 'None (Default Routing)', value: '' });
624
+ const agentAnswer = await inquirer.prompt([
625
+ {
626
+ type: 'list',
627
+ name: 'agentId',
628
+ message: 'Select an Agent to bind for processing these tasks:',
629
+ choices: agentsInfo,
630
+ default: finalAgentId || ''
631
+ }
632
+ ]);
633
+ finalAgentId = agentAnswer.agentId;
634
+ }
635
+ } catch (err) {
636
+ logger.warn(`Could not load agents list: ${err.message}`);
637
+ }
638
+
639
+ // Determine how to save.
488
640
  console.log('\n✅ \x1b[32mConfiguration Captured!\x1b[0m\n');
489
641
 
490
642
  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
- }
643
+ pierApiUrl: currentConfig.pierApiUrl,
644
+ nodeId: finalNodeId,
645
+ secretKey: finalSecretKey,
646
+ walletAddress: finalWallet,
647
+ agentId: finalAgentId
504
648
  };
649
+
650
+ if (setupMethod === 'auto') {
651
+ newConfigBlock.privateKey = finalPrivateKey;
652
+ console.log('\x1b[33mNote: Private Key is included for future automatic reconnects.\x1b[0m');
653
+ }
654
+
655
+ // Write to openclaw.json
656
+ try {
657
+ const cwd = process.cwd();
658
+ const configPath = path.join(cwd, 'openclaw.json');
659
+ let existingConfig = {};
660
+ if (fs.existsSync(configPath)) {
661
+ const raw = fs.readFileSync(configPath, 'utf8');
662
+ existingConfig = JSON.parse(raw);
663
+ }
664
+
665
+ if (!existingConfig.plugins) existingConfig.plugins = {};
666
+ if (!existingConfig.plugins.entries) existingConfig.plugins.entries = {};
667
+ if (!existingConfig.plugins.entries['pier-connector']) {
668
+ existingConfig.plugins.entries['pier-connector'] = { enabled: true };
669
+ }
670
+
671
+ existingConfig.plugins.entries['pier-connector'].config = {
672
+ ...(existingConfig.plugins.entries['pier-connector'].config || {}),
673
+ ...newConfigBlock
674
+ };
675
+
676
+ fs.writeFileSync(configPath, JSON.stringify(existingConfig, null, 2), 'utf8');
677
+ console.log(`\x1b[32m✔ Successfully wrote configuration to ${configPath}\x1b[0m`);
678
+ } catch (err) {
679
+ console.error(`\x1b[31m✖ Failed to write to openclaw.json automatically: ${err.message}\x1b[0m`);
680
+ console.log('Please ensure your \x1b[1m\x1b[33mopenclaw.json\x1b[0m includes the following block in plugins.entries:\n');
681
+ console.log(JSON.stringify({
682
+ "pier-connector": {
683
+ enabled: true,
684
+ config: newConfigBlock
685
+ }
686
+ }, null, 2));
687
+ }
505
688
 
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.');
689
+ console.log('\nRestart OpenClaw (or reload plugins) to apply changes.');
510
690
  });
511
691
  },
512
692
  { commands: ['pier'] }