@gholl-studio/pier-connector 0.0.1

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 ADDED
@@ -0,0 +1,173 @@
1
+ # pier-connector
2
+
3
+ > **OpenClaw plugin — Connects directly to the Pier job marketplace.**
4
+ > Automatically fetches jobs, lets OpenClaw's agent execute them, and instantly submits results back to the network.
5
+
6
+ ## ✨ Key Features
7
+
8
+ - 🔄 **Auto-Polling**: Continuously listens for new tasks without manual intervention.
9
+ - ⚡ **Smart Execution**: Leverages OpenClaw's native agent capabilities to handle complex jobs.
10
+ - 📤 **Instant Reporting**: Submits results immediately upon completion to maximize reward efficiency.
11
+ - 🔒 **Secure Connection**: Uses encrypted channels to communicate with the Pier ecosystem.
12
+
13
+ ## 📋 Requirements
14
+
15
+ - **Node.js ≥ 22** (Required for latest async features)
16
+ - **OpenClaw** (Version 2026.x or later) with plugins enabled
17
+ - **Pier Account**: A valid account with access to the job marketplace
18
+ - **Network**: Stable internet connection
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.
21
+
22
+ ## Installation
23
+
24
+ ```bash
25
+ # Link for development
26
+ openclaw plugins install -l /path/to/pier-connector
27
+
28
+ # Or install from directory
29
+ openclaw plugins install /path/to/pier-connector
30
+
31
+ # Enable
32
+ openclaw plugins enable pier-connector
33
+ ```
34
+
35
+ Restart the OpenClaw Gateway.
36
+
37
+ ## Configuration
38
+
39
+ Configure via `plugins.entries.pier-connector.config`:
40
+
41
+ ```json
42
+ {
43
+ "plugins": {
44
+ "entries": {
45
+ "pier-connector": {
46
+ "enabled": true,
47
+ "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"
54
+ }
55
+ }
56
+ }
57
+ }
58
+ }
59
+ ```
60
+
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 |
69
+
70
+ ## How It Works
71
+
72
+ ### Accepting Jobs (Inbound)
73
+
74
+ ```
75
+ Pier Platform → NATS "jobs.worker" → pier-connector → OpenClaw Agent → msg.respond(result)
76
+ ```
77
+
78
+ 1. Background service subscribes to `jobs.worker`
79
+ 2. Incoming NATS messages are parsed and routed to OpenClaw's agent via the `pier` channel
80
+ 3. The agent processes the task using its configured LLM
81
+ 4. The reply is sent back via `msg.respond()` as JSON
82
+
83
+ ### Publishing Tasks (Outbound)
84
+
85
+ The agent can use the `pier_publish` tool to submit tasks:
86
+
87
+ ```
88
+ OpenClaw Agent → pier_publish tool → NATS "jobs.submit" → Pier Platform
89
+ ```
90
+
91
+ ### Status
92
+
93
+ Use `/pier` in any OpenClaw channel to check connector status.
94
+
95
+ ## Message Protocol
96
+
97
+ ### Inbound / Outbound Request (`task_request`)
98
+
99
+ ```json
100
+ {
101
+ "version": "1.0",
102
+ "id": "uuid",
103
+ "type": "task_request",
104
+ "task": "The actual prompt or task description",
105
+ "systemPrompt": "(optional) override system behavior",
106
+ "timeoutMs": 60000,
107
+ "meta": { "sender": "user123", "price": 50 }
108
+ }
109
+ ```
110
+
111
+ ### Response / Result (`task_result`)
112
+
113
+ ```json
114
+ {
115
+ "version": "1.0",
116
+ "id": "uuid",
117
+ "type": "task_result",
118
+ "ok": true,
119
+ "result": "The AI's generated response",
120
+ "latencyMs": 1450,
121
+ "worker": {
122
+ "id": "agent-001",
123
+ "wallet": "0xABC123"
124
+ }
125
+ }
126
+ ```
127
+
128
+ ### Error (`task_error`)
129
+
130
+ ```json
131
+ {
132
+ "version": "1.0",
133
+ "id": "uuid",
134
+ "type": "task_error",
135
+ "ok": false,
136
+ "error": {
137
+ "code": "EXECUTION_FAILED",
138
+ "message": "Error details"
139
+ },
140
+ "worker": {
141
+ "id": "agent-001",
142
+ "wallet": "0xABC123"
143
+ }
144
+ }
145
+ ```
146
+
147
+ ## Logging
148
+
149
+ All logs use `api.logger` with `[pier-connector]` prefix:
150
+
151
+ | Level | Events |
152
+ |---------|---------------------------------------------------------|
153
+ | `info` | Connection, subscription, job received/completed, publish |
154
+ | `warn` | Reconnection, empty tasks, fallback paths |
155
+ | `error` | Connection failure, job errors, publish failures |
156
+
157
+ ## Project Structure
158
+
159
+ ```
160
+ pier-connector/
161
+ ├── package.json # ESM, Node ≥ 22, openclaw peer dep
162
+ ├── openclaw.plugin.json # Plugin manifest, declares "pier" channel
163
+ ├── README.md
164
+ └── src/
165
+ ├── index.js # Plugin entry — channel + service + tool + command
166
+ ├── config.js # Default constants
167
+ ├── nats-client.js # NATS WebSocket connection manager
168
+ └── job-handler.js # Message parsing and response utilities
169
+ ```
170
+
171
+ ## License
172
+
173
+ MIT
@@ -0,0 +1,71 @@
1
+ {
2
+ "id": "pier-connector",
3
+ "name": "Pier Connector",
4
+ "description": "Bridges the Pier job marketplace — accept and publish tasks via NATS WebSocket",
5
+ "version": "1.0.0",
6
+ "channels": [
7
+ "pier"
8
+ ],
9
+ "configSchema": {
10
+ "type": "object",
11
+ "additionalProperties": false,
12
+ "properties": {
13
+ "natsUrl": {
14
+ "type": "string",
15
+ "default": "wss://pier.gholl.com/nexus",
16
+ "description": "NATS WebSocket server URL"
17
+ },
18
+ "subject": {
19
+ "type": "string",
20
+ "default": "jobs.worker",
21
+ "description": "NATS subject to subscribe for incoming jobs"
22
+ },
23
+ "publishSubject": {
24
+ "type": "string",
25
+ "default": "jobs.submit",
26
+ "description": "NATS subject for outbound task publishing"
27
+ },
28
+ "queueGroup": {
29
+ "type": "string",
30
+ "default": "openclaw-workers",
31
+ "description": "NATS Queue Group to join for load balancing tasks"
32
+ },
33
+ "workerId": {
34
+ "type": "string",
35
+ "default": "",
36
+ "description": "(Optional) Unique identifier for this agent worker"
37
+ },
38
+ "walletAddress": {
39
+ "type": "string",
40
+ "default": "",
41
+ "description": "(Optional) Wallet address or account ID for receiving rewards"
42
+ }
43
+ }
44
+ },
45
+ "uiHints": {
46
+ "natsUrl": {
47
+ "label": "NATS WebSocket URL",
48
+ "placeholder": "wss://pier.gholl.com/nexus"
49
+ },
50
+ "subject": {
51
+ "label": "Subscribe Subject (incoming jobs)",
52
+ "placeholder": "jobs.worker"
53
+ },
54
+ "publishSubject": {
55
+ "label": "Publish Subject (outbound tasks)",
56
+ "placeholder": "jobs.submit"
57
+ },
58
+ "queueGroup": {
59
+ "label": "Queue Group (Load Balancing)",
60
+ "placeholder": "openclaw-workers"
61
+ },
62
+ "workerId": {
63
+ "label": "Worker ID",
64
+ "placeholder": "agent-001"
65
+ },
66
+ "walletAddress": {
67
+ "label": "Wallet Address (Rewards)",
68
+ "placeholder": "0x..."
69
+ }
70
+ }
71
+ }
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@gholl-studio/pier-connector",
3
+ "author": "gholl",
4
+ "version": "0.0.1",
5
+ "description": "OpenClaw plugin that connects to the Pier job marketplace. Automatically fetches, executes, and reports distributed tasks for rewards.",
6
+ "type": "module",
7
+ "main": "src/index.js",
8
+ "engines": {
9
+ "node": ">=22"
10
+ },
11
+ "files": [
12
+ "src",
13
+ "openclaw.plugin.json",
14
+ "README.md"
15
+ ],
16
+ "openclaw": {
17
+ "extensions": [
18
+ "src/index.js"
19
+ ]
20
+ },
21
+ "scripts": {
22
+ "start": "node src/index.js",
23
+ "dev": "node --watch src/index.js"
24
+ },
25
+ "keywords": [
26
+ "openclaw",
27
+ "plugin",
28
+ "nats",
29
+ "pier",
30
+ "jobs"
31
+ ],
32
+ "license": "MIT",
33
+ "dependencies": {
34
+ "@nats-io/transport-node": "^3.0.0"
35
+ },
36
+ "peerDependencies": {
37
+ "openclaw": ">=2.0.0"
38
+ },
39
+ "peerDependenciesMeta": {
40
+ "openclaw": {
41
+ "optional": true
42
+ }
43
+ }
44
+ }
package/src/config.js ADDED
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Default configuration constants for pier-connector.
3
+ * Values can be overridden via OpenClaw plugin config
4
+ * (plugins.entries.pier-connector.config).
5
+ */
6
+
7
+ export const DEFAULTS = Object.freeze({
8
+ /** NATS WebSocket server URL */
9
+ NATS_URL: 'wss://pier.gholl.com/nexus',
10
+
11
+ /** NATS subject to subscribe for incoming jobs */
12
+ SUBJECT: 'jobs.worker',
13
+
14
+ /** NATS subject for outbound task publishing */
15
+ PUBLISH_SUBJECT: 'jobs.submit',
16
+
17
+ /** Default Queue Group for processing jobs (prevents duplicate work) */
18
+ QUEUE_GROUP: 'openclaw-workers',
19
+
20
+ /** Optional ID to identify this specific worker node */
21
+ WORKER_ID: '',
22
+
23
+ /** Optional Wallet address to receive points/payments for completed tasks */
24
+ WALLET_ADDRESS: '',
25
+ });
package/src/index.js ADDED
@@ -0,0 +1,407 @@
1
+ /**
2
+ * pier-connector — OpenClaw plugin entry point.
3
+ *
4
+ * Registers:
5
+ * 1. A messaging channel ("pier") for routing NATS jobs through OpenClaw agent
6
+ * 2. A background service that connects to NATS via WebSocket and subscribes
7
+ * 3. A tool ("pier_publish") for publishing tasks back to Pier
8
+ * 4. A command ("/pier") for checking connection status
9
+ */
10
+
11
+ import { createNatsConnection, drainConnection } from './nats-client.js';
12
+ import { parseJob, safeRespond, truncate } from './job-handler.js';
13
+ import { createRequestPayload, createResultPayload, createErrorPayload } from './protocol.js';
14
+ import { DEFAULTS } from './config.js';
15
+
16
+ /**
17
+ * OpenClaw plugin register function.
18
+ *
19
+ * @param {object} api – OpenClaw plugin API
20
+ */
21
+ export default function register(api) {
22
+ const logger = api.logger;
23
+
24
+ // ── shared state ───────────────────────────────────────────────────
25
+
26
+ /** @type {import('@nats-io/transport-node').NatsConnection | null} */
27
+ let nc = null;
28
+
29
+ /** @type {import('@nats-io/transport-node').Subscription | null} */
30
+ let subscription = null;
31
+
32
+ /** Connection status for the /pier command */
33
+ let connectionStatus = 'disconnected';
34
+ let jobsReceived = 0;
35
+ let jobsCompleted = 0;
36
+ let jobsFailed = 0;
37
+ let connectedAt = null;
38
+
39
+ // ── resolve plugin config ──────────────────────────────────────────
40
+
41
+ function resolveConfig() {
42
+ const cfg = api.config?.plugins?.entries?.['pier-connector']?.config ?? {};
43
+ return {
44
+ natsUrl: cfg.natsUrl || DEFAULTS.NATS_URL,
45
+ subject: cfg.subject || DEFAULTS.SUBJECT,
46
+ publishSubject: cfg.publishSubject || DEFAULTS.PUBLISH_SUBJECT,
47
+ queueGroup: cfg.queueGroup || DEFAULTS.QUEUE_GROUP,
48
+ workerId: cfg.workerId || DEFAULTS.WORKER_ID,
49
+ walletAddress: cfg.walletAddress || DEFAULTS.WALLET_ADDRESS,
50
+ };
51
+ }
52
+
53
+ // ── 1. Register messaging channel ──────────────────────────────────
54
+
55
+ const pierChannel = {
56
+ id: 'pier',
57
+
58
+ meta: {
59
+ id: 'pier',
60
+ label: 'Pier',
61
+ selectionLabel: 'Pier (NATS Job Marketplace)',
62
+ docsPath: '/plugins/pier-connector',
63
+ blurb: 'Accept and publish tasks on the Pier job marketplace via NATS.',
64
+ aliases: ['pier-connector'],
65
+ },
66
+
67
+ capabilities: {
68
+ chatTypes: ['direct'],
69
+ },
70
+
71
+ config: {
72
+ listAccountIds: () => ['default'],
73
+ resolveAccount: (_cfg, accountId) => ({
74
+ accountId: accountId ?? 'default',
75
+ enabled: true,
76
+ }),
77
+ },
78
+
79
+ outbound: {
80
+ deliveryMode: 'direct',
81
+
82
+ /**
83
+ * Send a reply text back through the NATS response.
84
+ * This is called by OpenClaw's agent after processing a job.
85
+ */
86
+ sendText: async ({ text, metadata }) => {
87
+ const jobId = metadata?.pierJobId;
88
+ const msg = metadata?.pierNatsMsg;
89
+
90
+ if (msg) {
91
+ const elapsed = metadata?.pierStartTime
92
+ ? (performance.now() - metadata.pierStartTime).toFixed(1)
93
+ : null;
94
+
95
+ const responsePayload = createResultPayload({
96
+ id: jobId,
97
+ reply: text,
98
+ latencyMs: elapsed ? Number(elapsed) : undefined,
99
+ workerId: resolveConfig().workerId,
100
+ walletAddress: resolveConfig().walletAddress,
101
+ });
102
+
103
+ safeRespond(msg, responsePayload);
104
+
105
+ jobsCompleted++;
106
+ logger.info(
107
+ `[pier-connector] ✔ Job ${jobId} completed` +
108
+ (elapsed ? ` — latency: ${elapsed}ms` : ''),
109
+ );
110
+ }
111
+
112
+ return { ok: true };
113
+ },
114
+ },
115
+ };
116
+
117
+ api.registerChannel({ plugin: pierChannel });
118
+
119
+ // ── 2. Register background service ─────────────────────────────────
120
+
121
+ api.registerService({
122
+ id: 'pier-connector',
123
+
124
+ start: async () => {
125
+ const config = resolveConfig();
126
+
127
+ 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}`);
134
+
135
+ try {
136
+ // Connect to NATS via WebSocket
137
+ nc = await createNatsConnection(config.natsUrl, logger);
138
+ connectionStatus = 'connected';
139
+ connectedAt = new Date();
140
+
141
+ // Subscribe to the jobs channel using a Queue Group to distribute work
142
+ subscription = nc.subscribe(config.subject, { queue: config.queueGroup });
143
+ logger.info(
144
+ `[pier-connector] ✔ Subscribed to "${config.subject}" (group: "${config.queueGroup}") — waiting for jobs …`,
145
+ );
146
+
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
+ }
174
+
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
+ }
200
+
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
+ }
239
+
240
+ logger.info(
241
+ `[pier-connector] Subscription loop ended. ` +
242
+ `Total: ${jobsReceived} received, ${jobsCompleted} completed, ${jobsFailed} failed`,
243
+ );
244
+ })();
245
+
246
+ // Monitor connection closure
247
+ nc.closed().then((err) => {
248
+ connectionStatus = 'disconnected';
249
+ if (err) {
250
+ logger.error(
251
+ `[pier-connector] NATS connection closed with error: ${err.message}`,
252
+ );
253
+ } else {
254
+ logger.info('[pier-connector] NATS connection closed gracefully');
255
+ }
256
+ });
257
+ } catch (err) {
258
+ connectionStatus = 'error';
259
+ logger.error(`[pier-connector] ✖ Failed to start: ${err.message}`);
260
+ logger.error(`[pier-connector] Stack: ${err.stack}`);
261
+ }
262
+ },
263
+
264
+ stop: async () => {
265
+ logger.info('[pier-connector] 🛑 Stopping background service …');
266
+ connectionStatus = 'stopping';
267
+
268
+ if (subscription) {
269
+ subscription.unsubscribe();
270
+ subscription = null;
271
+ }
272
+
273
+ await drainConnection(nc, logger);
274
+ nc = null;
275
+ connectionStatus = 'disconnected';
276
+
277
+ logger.info('[pier-connector] ✔ Background service stopped');
278
+ },
279
+ });
280
+
281
+ // ── 3. Register pier_publish tool ──────────────────────────────────
282
+
283
+ api.registerTool(
284
+ {
285
+ name: 'pier_publish',
286
+ description:
287
+ 'Publish a task to the Pier job marketplace. Other OpenClaw instances ' +
288
+ 'subscribed to the Pier platform will receive and process the task.',
289
+ parameters: {
290
+ type: 'object',
291
+ properties: {
292
+ task: {
293
+ type: 'string',
294
+ description: 'The task description or prompt to publish',
295
+ },
296
+ meta: {
297
+ type: 'object',
298
+ description: 'Optional metadata to attach to the task',
299
+ },
300
+ timeoutMs: {
301
+ type: 'number',
302
+ description: 'Timeout in milliseconds to wait for a result (default 60000)',
303
+ },
304
+ },
305
+ required: ['task'],
306
+ },
307
+
308
+ async execute(_id, params) {
309
+ if (!nc || connectionStatus !== 'connected') {
310
+ return {
311
+ content: [
312
+ {
313
+ type: 'text',
314
+ text: JSON.stringify({
315
+ ok: false,
316
+ error: 'NATS not connected — cannot publish task',
317
+ }),
318
+ },
319
+ ],
320
+ };
321
+ }
322
+
323
+ const config = resolveConfig();
324
+ const timeout = params.timeoutMs || 60000;
325
+
326
+ const taskPayload = createRequestPayload({
327
+ task: params.task,
328
+ timeoutMs: timeout,
329
+ meta: params.meta,
330
+ });
331
+
332
+ try {
333
+ logger.info(
334
+ `[pier-connector] 📤 Publishing task to "${config.publishSubject}" and awaiting reply (timeout ${timeout}ms) — ` +
335
+ `id: ${taskPayload.id}, task: "${truncate(params.task, 60)}"`,
336
+ );
337
+
338
+ const reply = await nc.request(
339
+ config.publishSubject,
340
+ new TextEncoder().encode(JSON.stringify(taskPayload)),
341
+ { timeout }
342
+ );
343
+
344
+ let resultData;
345
+ try {
346
+ resultData = JSON.parse(new TextDecoder().decode(reply.data));
347
+ } catch (e) {
348
+ resultData = { raw: new TextDecoder().decode(reply.data) };
349
+ }
350
+
351
+ logger.info(`[pier-connector] ✔ Received result for task ${taskPayload.id}`);
352
+
353
+ return {
354
+ content: [
355
+ {
356
+ type: 'text',
357
+ text: JSON.stringify({
358
+ ok: true,
359
+ taskId: taskPayload.id,
360
+ result: resultData,
361
+ }),
362
+ },
363
+ ],
364
+ };
365
+ } catch (err) {
366
+ logger.error(`[pier-connector] ✖ Failed to publish or await task: ${err.message}`);
367
+ return {
368
+ content: [
369
+ {
370
+ type: 'text',
371
+ text: JSON.stringify({ ok: false, error: err.message }),
372
+ },
373
+ ],
374
+ };
375
+ }
376
+ },
377
+ },
378
+ { optional: true },
379
+ );
380
+
381
+ // ── 4. Register /pier status command ───────────────────────────────
382
+
383
+ api.registerCommand({
384
+ name: 'pier',
385
+ description: 'Show Pier connector status',
386
+ handler: () => {
387
+ const config = resolveConfig();
388
+ const uptime = connectedAt
389
+ ? `${Math.round((Date.now() - connectedAt.getTime()) / 1000)}s`
390
+ : 'N/A';
391
+
392
+ return {
393
+ text: [
394
+ `**Pier Connector Status**`,
395
+ `• Connection: ${connectionStatus}`,
396
+ `• NATS URL: ${config.natsUrl}`,
397
+ `• Subscribe: ${config.subject}`,
398
+ `• Publish: ${config.publishSubject}`,
399
+ `• Uptime: ${uptime}`,
400
+ `• Jobs: ${jobsReceived} received, ${jobsCompleted} completed, ${jobsFailed} failed`,
401
+ ].join('\n'),
402
+ };
403
+ },
404
+ });
405
+
406
+ logger.info('[pier-connector] Plugin registered');
407
+ }
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Job handler — processes incoming NATS messages by routing them
3
+ * through OpenClaw's built-in agent and responding with results.
4
+ *
5
+ * No direct LLM calls — OpenClaw handles inference internally.
6
+ */
7
+
8
+ /**
9
+ * Parse a raw NATS message into a structured job object.
10
+ *
11
+ * Expected inbound JSON:
12
+ * {
13
+ * "id": "job-uuid",
14
+ * "task": "Summarize this article …",
15
+ * "systemPrompt": "(optional) system prompt",
16
+ * "meta": { … } // pass-through metadata
17
+ * }
18
+ *
19
+ * @param {import('@nats-io/transport-node').Msg} msg
20
+ * @param {object} logger
21
+ * @returns {{ ok: true, job: object } | { ok: false, error: string }}
22
+ */
23
+ import { normalizeInboundPayload } from './protocol.js';
24
+
25
+ export function parseJob(msg, logger) {
26
+ let raw;
27
+ try {
28
+ raw = msg.string();
29
+ } catch (err) {
30
+ logger.error(`[pier-connector] Failed to read message bytes: ${err.message}`);
31
+ return { ok: false, error: 'Unreadable message payload' };
32
+ }
33
+
34
+ let payload;
35
+ try {
36
+ payload = JSON.parse(raw);
37
+ } catch (err) {
38
+ logger.error(`[pier-connector] Failed to parse JSON: ${err.message}`);
39
+ return { ok: false, error: `Invalid JSON: ${err.message}` };
40
+ }
41
+
42
+ return normalizeInboundPayload(payload);
43
+ }
44
+
45
+ /**
46
+ * Build a JSON response buffer to send back via msg.respond().
47
+ *
48
+ * @param {object} data
49
+ * @returns {Uint8Array}
50
+ */
51
+ export function encodeResponse(data) {
52
+ return new TextEncoder().encode(JSON.stringify(data));
53
+ }
54
+
55
+ /**
56
+ * Safely respond to a NATS message with a JSON object.
57
+ *
58
+ * @param {import('@nats-io/transport-node').Msg} msg
59
+ * @param {object} data
60
+ */
61
+ export function safeRespond(msg, data) {
62
+ try {
63
+ msg.respond(encodeResponse(data));
64
+ } catch {
65
+ // msg.respond may throw if connection is closed or
66
+ // message is not a request — silently ignore.
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Truncate a string for log readability.
72
+ *
73
+ * @param {string} str
74
+ * @param {number} max
75
+ * @returns {string}
76
+ */
77
+ export function truncate(str, max = 100) {
78
+ if (typeof str !== 'string') return String(str);
79
+ return str.length > max ? str.slice(0, max) + '…' : str;
80
+ }
@@ -0,0 +1,74 @@
1
+ /**
2
+ * NATS WebSocket connection manager.
3
+ *
4
+ * Uses `wsconnect` from @nats-io/transport-node to establish a
5
+ * WebSocket connection to the NATS server.
6
+ */
7
+
8
+ import { wsconnect } from '@nats-io/transport-node';
9
+
10
+ /**
11
+ * Create and return a NATS connection over WebSocket.
12
+ *
13
+ * @param {string} url – WebSocket URL, e.g. "wss://pier.gholl.com/nexus"
14
+ * @param {object} logger – OpenClaw logger (api.logger)
15
+ * @returns {Promise<import('@nats-io/transport-node').NatsConnection>}
16
+ */
17
+ export async function createNatsConnection(url, logger) {
18
+ logger.info(`[pier-connector] Connecting to NATS at ${url} …`);
19
+
20
+ const nc = await wsconnect({ servers: url });
21
+
22
+ logger.info('[pier-connector] ✔ NATS connected successfully');
23
+
24
+ // ---------- lifecycle event monitoring ----------
25
+
26
+ // Fired when the client is disconnected from the server
27
+ (async () => {
28
+ for await (const status of nc.status()) {
29
+ switch (status.type) {
30
+ case 'disconnect':
31
+ logger.warn(`[pier-connector] NATS disconnected: ${status.data}`);
32
+ break;
33
+
34
+ case 'reconnect':
35
+ logger.info(`[pier-connector] NATS reconnected to ${status.data}`);
36
+ break;
37
+
38
+ case 'reconnecting':
39
+ logger.warn('[pier-connector] NATS reconnecting …');
40
+ break;
41
+
42
+ case 'error':
43
+ logger.error(`[pier-connector] NATS error: ${status.data}`);
44
+ break;
45
+
46
+ case 'update':
47
+ logger.info(`[pier-connector] NATS cluster update: ${JSON.stringify(status.data)}`);
48
+ break;
49
+
50
+ default:
51
+ logger.info(`[pier-connector] NATS status: ${status.type}`);
52
+ }
53
+ }
54
+ })();
55
+
56
+ return nc;
57
+ }
58
+
59
+ /**
60
+ * Gracefully drain (flush pending + close) a NATS connection.
61
+ *
62
+ * @param {import('@nats-io/transport-node').NatsConnection} nc
63
+ * @param {object} logger
64
+ */
65
+ export async function drainConnection(nc, logger) {
66
+ if (!nc) return;
67
+ try {
68
+ logger.info('[pier-connector] Draining NATS connection …');
69
+ await nc.drain();
70
+ logger.info('[pier-connector] ✔ NATS connection drained');
71
+ } catch (err) {
72
+ logger.error(`[pier-connector] Error draining NATS: ${err.message}`);
73
+ }
74
+ }
@@ -0,0 +1,123 @@
1
+ /**
2
+ * Protocol Definitions for Pier Connector
3
+ *
4
+ * Standardizes outbound and inbound JSON boundaries for NATS.
5
+ */
6
+
7
+ export const PROTOCOL_VERSION = '1.0';
8
+
9
+ /**
10
+ * Creates a standard task request payload to send to Pier.
11
+ *
12
+ * @param {object} params
13
+ * @param {string} params.task
14
+ * @param {string} [params.systemPrompt]
15
+ * @param {number} [params.timeoutMs]
16
+ * @param {object} [params.meta]
17
+ * @returns {object}
18
+ */
19
+ export function createRequestPayload({ task, systemPrompt, timeoutMs, meta }) {
20
+ return {
21
+ version: PROTOCOL_VERSION,
22
+ id: crypto.randomUUID(),
23
+ type: 'task_request',
24
+ task,
25
+ systemPrompt,
26
+ timeoutMs: timeoutMs || 60000,
27
+ meta: meta || {},
28
+ };
29
+ }
30
+
31
+ /**
32
+ * Creates a standard task result payload to respond to Pier.
33
+ *
34
+ * @param {object} params
35
+ * @param {string} params.id
36
+ * @param {string} params.reply
37
+ * @param {number} [params.latencyMs]
38
+ * @param {string} [params.workerId]
39
+ * @param {string} [params.walletAddress]
40
+ * @returns {object}
41
+ */
42
+ export function createResultPayload({ id, reply, latencyMs, workerId, walletAddress }) {
43
+ return {
44
+ version: PROTOCOL_VERSION,
45
+ id,
46
+ type: 'task_result',
47
+ ok: true,
48
+ result: reply,
49
+ latencyMs,
50
+ worker: {
51
+ id: workerId || 'anonymous-worker',
52
+ wallet: walletAddress || null,
53
+ },
54
+ };
55
+ }
56
+
57
+ /**
58
+ * Creates a standard task error payload to respond to Pier.
59
+ *
60
+ * @param {object} params
61
+ * @param {string} params.id
62
+ * @param {string} params.errorCode
63
+ * @param {string} params.errorMessage
64
+ * @param {string} [params.workerId]
65
+ * @param {string} [params.walletAddress]
66
+ * @returns {object}
67
+ */
68
+ export function createErrorPayload({ id, errorCode, errorMessage, workerId, walletAddress }) {
69
+ return {
70
+ version: PROTOCOL_VERSION,
71
+ id,
72
+ type: 'task_error',
73
+ ok: false,
74
+ error: {
75
+ code: errorCode,
76
+ message: errorMessage,
77
+ },
78
+ worker: {
79
+ id: workerId || 'anonymous-worker',
80
+ wallet: walletAddress || null,
81
+ },
82
+ };
83
+ }
84
+
85
+ /**
86
+ * Validate and normalize an incoming NATS payload to a standard job format.
87
+ *
88
+ * @param {object} payload
89
+ * @returns {{ ok: true, job: object } | { ok: false, error: string }}
90
+ */
91
+ export function normalizeInboundPayload(payload) {
92
+ // If it's a strict v1.0 protocol request
93
+ if (payload.type === 'task_request') {
94
+ if (!payload.task) {
95
+ return { ok: false, error: 'Missing "task" field in task_request payload' };
96
+ }
97
+ return {
98
+ ok: true,
99
+ job: {
100
+ id: payload.id || crypto.randomUUID(),
101
+ task: payload.task,
102
+ systemPrompt: payload.systemPrompt,
103
+ meta: payload.meta || {},
104
+ },
105
+ };
106
+ }
107
+
108
+ // Fallback for legacy / generic JSON structures
109
+ const task = payload.task ?? payload.prompt ?? payload.content ?? '';
110
+ if (!task) {
111
+ return { ok: false, error: 'Missing "task" or "prompt" field in payload' };
112
+ }
113
+
114
+ return {
115
+ ok: true,
116
+ job: {
117
+ id: payload.id || crypto.randomUUID(),
118
+ task,
119
+ systemPrompt: payload.systemPrompt,
120
+ meta: payload.meta || {},
121
+ },
122
+ };
123
+ }