@gholl-studio/pier-connector 0.2.30 → 0.2.32

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.
@@ -59,6 +59,12 @@
59
59
  "type": "string",
60
60
  "default": "",
61
61
  "description": "(Optional) Wallet address or account ID for receiving rewards"
62
+ },
63
+ "capabilities": {
64
+ "type": "array",
65
+ "items": { "type": "string" },
66
+ "default": ["translation", "code-execution", "reasoning", "vision"],
67
+ "description": "List of AI capabilities this node supports (e.g., translation, gpu:4090)"
62
68
  }
63
69
  }
64
70
  },
@@ -102,6 +108,10 @@
102
108
  "walletAddress": {
103
109
  "label": "Wallet Address (Rewards)",
104
110
  "placeholder": "0x..."
111
+ },
112
+ "capabilities": {
113
+ "label": "Capabilities",
114
+ "placeholder": "translation, reasoning, ..."
105
115
  }
106
116
  }
107
117
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@gholl-studio/pier-connector",
3
3
  "author": "gholl",
4
- "version": "0.2.30",
4
+ "version": "0.2.32",
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",
@@ -19,8 +19,11 @@
19
19
  ]
20
20
  },
21
21
  "scripts": {
22
- "start": "node src/index.js",
23
- "dev": "node --watch src/index.js"
22
+ "dev": "vite",
23
+ "build": "tsc -b && vite build",
24
+ "lint": "eslint .",
25
+ "preview": "vite preview",
26
+ "test:watch": "node --watch test-standalone.js"
24
27
  },
25
28
  "keywords": [
26
29
  "openclaw",
@@ -31,6 +34,7 @@
31
34
  ],
32
35
  "license": "MIT",
33
36
  "dependencies": {
37
+ "@gholl-studio/pier-sdk": "^1.0.0",
34
38
  "@nats-io/jetstream": "^3.3.1",
35
39
  "@nats-io/transport-node": "^3.0.0",
36
40
  "ethers": "^6.16.0",
@@ -44,5 +48,8 @@
44
48
  "openclaw": {
45
49
  "optional": true
46
50
  }
51
+ },
52
+ "devDependencies": {
53
+ "dotenv": "^17.3.1"
47
54
  }
48
55
  }
package/src/index.js CHANGED
@@ -8,13 +8,12 @@
8
8
  * 4. A command ("/pier") for checking connection status
9
9
  */
10
10
 
11
- import { createNatsConnection, drainConnection } from './nats-client.js';
11
+ import { PierClient, protocol } from '@gholl-studio/pier-sdk';
12
+ const { createRequestPayload, createResultPayload, createErrorPayload } = protocol;
12
13
  import { parseJob, safeRespond, truncate } from './job-handler.js';
13
- import { createRequestPayload, createResultPayload, createErrorPayload } from './protocol.js';
14
14
  import { DEFAULTS } from './config.js';
15
15
  import { jetstream, jetstreamManager, AckPolicy, DeliverPolicy } from '@nats-io/jetstream';
16
16
  import inquirer from 'inquirer';
17
- import { ethers } from 'ethers';
18
17
  import fs from 'fs';
19
18
  import path from 'path';
20
19
 
@@ -69,6 +68,7 @@ export default function register(api) {
69
68
  queueGroup: merged.queueGroup || DEFAULTS.QUEUE_GROUP,
70
69
  agentId: merged.agentId || DEFAULTS.AGENT_ID,
71
70
  walletAddress: merged.walletAddress || DEFAULTS.WALLET_ADDRESS,
71
+ capabilities: merged.capabilities || ['translation', 'code-execution', 'reasoning', 'vision'],
72
72
  };
73
73
  }
74
74
 
@@ -80,6 +80,11 @@ export default function register(api) {
80
80
  constructor(config) {
81
81
  this.config = config;
82
82
  this.accountId = config.accountId;
83
+ this.client = new PierClient({
84
+ apiUrl: config.pierApiUrl,
85
+ natsUrl: config.natsUrl,
86
+ logger
87
+ });
83
88
  this.nc = null;
84
89
  this.js = null;
85
90
  this.jsm = null;
@@ -95,26 +100,37 @@ export default function register(api) {
95
100
  this.connectedAt = null;
96
101
  }
97
102
 
103
+ async init() {
104
+ try {
105
+ this.nc = await this.client.connectNats();
106
+ this.js = jetstream(this.nc);
107
+ this.jsm = await jetstreamManager(this.nc);
108
+
109
+ this.connectedAt = new Date();
110
+ this.connectionStatus = 'connected';
111
+
112
+ // Subscribe to tasks
113
+ await this.subscribeToTasks();
114
+
115
+ // Start heartbeat
116
+ this.startHeartbeat();
117
+
118
+ return true;
119
+ } catch (err) {
120
+ this.connectionStatus = 'error';
121
+ logger.error(`[pier-connector] Account ${this.accountId} failed to connect: ${err.message}`);
122
+ return false;
123
+ }
124
+ }
125
+
98
126
  async heartbeat() {
99
127
  if (!this.config.nodeId || !this.config.secretKey) return null;
100
128
  try {
101
- const resp = await fetch(`${this.config.pierApiUrl}/nodes/heartbeat`, {
102
- method: 'POST',
103
- headers: { 'Content-Type': 'application/json' },
104
- body: JSON.stringify({
105
- node_id: this.config.nodeId,
106
- secret_key: this.config.secretKey,
107
- capabilities: ['translation', 'code-execution', 'reasoning', 'vision'],
108
- description: `OpenClaw Node (${this.config.nodeId.substring(0, 8)}) [${this.accountId}]`,
109
- }),
110
- });
111
-
112
- if (!resp.ok) {
113
- const errText = await resp.text();
114
- throw new Error(`Heartbeat failed (${resp.status}): ${errText}`);
115
- }
116
-
117
- const data = await resp.json();
129
+ const data = await this.client.heartbeat(
130
+ this.config.nodeId,
131
+ this.config.secretKey,
132
+ this.config.capabilities
133
+ );
118
134
  this.lastHeartbeatError = null;
119
135
  return data.nats_config || null;
120
136
  } catch (err) {
@@ -125,28 +141,7 @@ export default function register(api) {
125
141
  }
126
142
 
127
143
  async claimJob(jobId) {
128
- try {
129
- const resp = await fetch(`${this.config.pierApiUrl}/jobs/${jobId}/claim`, {
130
- method: 'POST',
131
- headers: {
132
- 'Content-Type': 'application/json',
133
- 'Authorization': `Bearer ${this.config.secretKey}`,
134
- 'X-Node-Id': this.config.nodeId
135
- }
136
- });
137
-
138
- if (!resp.ok) {
139
- if (resp.status === 409) {
140
- logger.warn(`[pier-connector][${this.accountId}] 🚫 Job ${jobId} already claimed.`);
141
- return { ok: false, alreadyClaimed: true };
142
- }
143
- const errData = await resp.json().catch(() => ({}));
144
- return { ok: false, error: errData.error || resp.statusText };
145
- }
146
- return { ok: true };
147
- } catch (err) {
148
- return { ok: false, error: err.message };
149
- }
144
+ return await this.client.claimJob(jobId, this.config.nodeId, this.config.secretKey);
150
145
  }
151
146
 
152
147
  /**
@@ -462,30 +457,10 @@ export default function register(api) {
462
457
  }
463
458
 
464
459
  async autoRegister() {
465
- const wallet = new ethers.Wallet(this.config.privateKey);
466
- const address = wallet.address;
467
-
468
- const challengeRes = await fetch(`${this.config.pierApiUrl}/auth/challenge?wallet_address=${address}`);
469
- const { challenge } = await challengeRes.json();
470
- const signature = await wallet.signMessage(challenge);
471
-
472
- const loginRes = await fetch(`${this.config.pierApiUrl}/auth/login`, {
473
- method: 'POST',
474
- headers: { 'Content-Type': 'application/json' },
475
- body: JSON.stringify({ wallet_address: address, challenge, signature })
476
- });
477
- const { api_key } = await loginRes.json();
478
-
479
460
  const hostName = `${api?.getRuntimeInfo?.()?.hostname ?? 'Auto'}-${this.accountId}`;
480
- const regRes = await fetch(`${this.config.pierApiUrl}/nodes/register`, {
481
- method: 'POST',
482
- headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${api_key}` },
483
- body: JSON.stringify({ wallet_address: address, name: hostName })
484
- });
485
- const { id, secret_key } = await regRes.json();
486
-
487
- this.config.nodeId = id;
488
- this.config.secretKey = secret_key;
461
+ const { nodeId, secretKey } = await this.client.autoRegister(this.config.privateKey, hostName);
462
+ this.config.nodeId = nodeId;
463
+ this.config.secretKey = secretKey;
489
464
  }
490
465
 
491
466
  async start() {
@@ -856,6 +831,41 @@ export default function register(api) {
856
831
  comment: { type: 'string', description: 'A short review' }
857
832
  }, 'user');
858
833
 
834
+ registerSystemActionTool('pier_reject_task', 'Reject an offered task from the employer in the current chat', 'task_reject', {
835
+ reason: { type: 'string', description: 'Reason for rejection' }
836
+ });
837
+
838
+ registerSystemActionTool('pier_fail_task', 'Report that the task has failed during execution', 'task_error', {
839
+ error: { type: 'string', description: 'Description of the error' }
840
+ });
841
+
842
+ registerSystemActionTool('pier_cancel_task', 'Cancel a previously proposed task', 'task_cancel', {
843
+ reason: { type: 'string', description: 'Reason for cancellation' }
844
+ }, 'user');
845
+
846
+ api.registerTool({
847
+ name: 'pier_get_profile',
848
+ description: 'Get the current Pier profile, balance, and node stats.',
849
+ parameters: {
850
+ type: 'object',
851
+ properties: {
852
+ accountId: { type: 'string' }
853
+ }
854
+ },
855
+ async execute(_id, params) {
856
+ const accountId = params.accountId || 'default';
857
+ const robot = instances.get(accountId) || instances.values().next().value;
858
+ if (!robot) return { content: [{ type: 'text', text: 'Error: Robot not found' }] };
859
+
860
+ try {
861
+ const profile = await robot.client.getUserProfile(robot.config.secretKey);
862
+ return { content: [{ type: 'text', text: JSON.stringify(profile, null, 2) }] };
863
+ } catch (err) {
864
+ return { content: [{ type: 'text', text: `Error: ${err.message}` }] };
865
+ }
866
+ }
867
+ }, { optional: true });
868
+
859
869
  // ── 4. Register /pier status command ───────────────────────────────
860
870
 
861
871
  api.registerCommand({
@@ -939,55 +949,35 @@ export default function register(api) {
939
949
  { type: 'input', name: 'pierApiUrl', message: 'Pier API URL:', default: currentConfig.pierApiUrl },
940
950
  { type: 'input', name: 'nodeId', message: 'Bot Node ID (UUID):', default: finalNodeId },
941
951
  { type: 'password', name: 'secretKey', message: 'Bot Secret Key:', default: finalSecretKey },
942
- { type: 'input', name: 'walletAddress', message: 'Your Wallet Address (for payout):', default: finalWallet }
952
+ { type: 'input', name: 'walletAddress', message: 'Your Wallet Address (for payout):', default: finalWallet },
953
+ { type: 'input', name: 'capabilities', message: 'Capabilities (comma-separated):', default: (currentConfig.capabilities || []).join(', ') }
943
954
  ]);
944
955
  finalNodeId = manualAnswers.nodeId;
945
956
  finalSecretKey = manualAnswers.secretKey;
946
957
  finalWallet = manualAnswers.walletAddress;
958
+ currentConfig.capabilities = manualAnswers.capabilities ? manualAnswers.capabilities.split(',').map(s => s.trim()) : currentConfig.capabilities;
947
959
  currentConfig.pierApiUrl = manualAnswers.pierApiUrl;
948
960
  } else if (setupMethod === 'auto') {
949
961
  const autoAnswers = await inquirer.prompt([
950
962
  { type: 'input', name: 'pierApiUrl', message: 'Pier API URL:', default: currentConfig.pierApiUrl },
951
- { type: 'password', name: 'privateKey', message: 'Your Wallet Private Key (Hex, 0x...):', default: finalPrivateKey }
963
+ { type: 'password', name: 'privateKey', message: 'Your Wallet Private Key (Hex, 0x...):', default: finalPrivateKey },
964
+ { type: 'input', name: 'capabilities', message: 'Capabilities (comma-separated):', default: (currentConfig.capabilities || []).join(', ') }
952
965
  ]);
953
966
  console.log('\n\x1b[36mRegistering your node...\x1b[0m');
954
967
  try {
955
- const wallet = new ethers.Wallet(autoAnswers.privateKey);
956
- console.log(`\x1b[32m✔ Loaded wallet: ${wallet.address}\x1b[0m`);
957
- finalWallet = finalWallet || wallet.address;
958
- finalPrivateKey = autoAnswers.privateKey;
959
- currentConfig.pierApiUrl = autoAnswers.pierApiUrl;
960
-
961
- const challengeRes = await fetch(`${currentConfig.pierApiUrl}/auth/challenge?wallet_address=${wallet.address}`);
962
- if (!challengeRes.ok) throw new Error("Failed to get challenge");
963
- const { challenge } = await challengeRes.json();
964
-
965
- const signature = await wallet.signMessage(challenge);
966
-
967
- const loginRes = await fetch(`${currentConfig.pierApiUrl}/auth/login`, {
968
- method: 'POST',
969
- headers: { 'Content-Type': 'application/json' },
970
- body: JSON.stringify({ wallet_address: wallet.address, challenge, signature })
971
- });
972
- if (!loginRes.ok) throw new Error("Failed to login");
973
- const { api_key } = await loginRes.json();
974
-
968
+ const tempClient = new PierClient({ apiUrl: autoAnswers.pierApiUrl });
975
969
  const hostName = api?.getRuntimeInfo?.()?.hostname ?? 'Auto-Node';
976
- const regRes = await fetch(`${currentConfig.pierApiUrl}/nodes/register`, {
977
- method: 'POST',
978
- headers: {
979
- 'Content-Type': 'application/json',
980
- 'Authorization': `Bearer ${api_key}`
981
- },
982
- body: JSON.stringify({ wallet_address: wallet.address, name: hostName })
983
- });
984
970
 
985
- if (!regRes.ok) throw new Error(`Failed to auto-register node (${regRes.status})`);
986
- const { id, secret_key } = await regRes.json();
971
+ const { nodeId, secretKey, walletAddress } = await tempClient.autoRegister(autoAnswers.privateKey, hostName);
972
+
973
+ finalWallet = finalWallet || walletAddress;
974
+ finalPrivateKey = autoAnswers.privateKey;
975
+ currentConfig.pierApiUrl = autoAnswers.pierApiUrl;
976
+ currentConfig.capabilities = autoAnswers.capabilities ? autoAnswers.capabilities.split(',').map(s => s.trim()) : currentConfig.capabilities;
977
+ finalNodeId = nodeId;
978
+ finalSecretKey = secretKey;
987
979
 
988
- finalNodeId = id;
989
- finalSecretKey = secret_key;
990
- console.log(`\x1b[32m✔ Node registered automatically! Node ID: ${id}\x1b[0m`);
980
+ console.log(`\x1b[32m✔ Node registered automatically! Node ID: ${nodeId}\x1b[0m`);
991
981
  } catch (err) {
992
982
  console.error(`\x1b[31m✖ Failed to auto-register: ${err.message}\x1b[0m`);
993
983
  return; // Stop setup
@@ -1033,7 +1023,8 @@ export default function register(api) {
1033
1023
  nodeId: finalNodeId,
1034
1024
  secretKey: finalSecretKey,
1035
1025
  walletAddress: finalWallet,
1036
- agentId: finalAgentId
1026
+ agentId: finalAgentId,
1027
+ capabilities: currentConfig.capabilities
1037
1028
  };
1038
1029
 
1039
1030
  if (setupMethod === 'auto') {
@@ -20,7 +20,8 @@
20
20
  * @param {object} logger
21
21
  * @returns {{ ok: true, job: object } | { ok: false, error: string }}
22
22
  */
23
- import { normalizeInboundPayload } from './protocol.js';
23
+ import { protocol } from '@gholl-studio/pier-sdk';
24
+ const { normalizeInboundPayload } = protocol;
24
25
 
25
26
  export function parseJob(msg, logger) {
26
27
  let raw;
@@ -1,12 +0,0 @@
1
- /**
2
- * Simple pass-through utility for messaging.
3
- * Compression was removed for simplicity and ease of debugging.
4
- */
5
-
6
- export function compress(text) {
7
- return text;
8
- }
9
-
10
- export function decompress(content) {
11
- return content;
12
- }
@@ -1,75 +0,0 @@
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
- import { jetstream } from '@nats-io/jetstream';
10
-
11
- /**
12
- * Create and return a NATS connection over WebSocket.
13
- *
14
- * @param {string} url – WebSocket URL, e.g. "wss://pier.gholl.com/nexus"
15
- * @param {object} logger – OpenClaw logger (api.logger)
16
- * @returns {Promise<import('@nats-io/transport-node').NatsConnection>}
17
- */
18
- export async function createNatsConnection(url, logger) {
19
- logger.info(`[pier-connector] Connecting to NATS at ${url} …`);
20
-
21
- const nc = await wsconnect({ servers: url });
22
-
23
- logger.info('[pier-connector] ✔ NATS connected successfully');
24
-
25
- // ---------- lifecycle event monitoring ----------
26
-
27
- // Fired when the client is disconnected from the server
28
- (async () => {
29
- for await (const status of nc.status()) {
30
- switch (status.type) {
31
- case 'disconnect':
32
- logger.warn(`[pier-connector] NATS disconnected: ${status.data}`);
33
- break;
34
-
35
- case 'reconnect':
36
- logger.info(`[pier-connector] NATS reconnected to ${status.data}`);
37
- break;
38
-
39
- case 'reconnecting':
40
- logger.warn('[pier-connector] NATS reconnecting …');
41
- break;
42
-
43
- case 'error':
44
- logger.error(`[pier-connector] NATS error: ${status.data}`);
45
- break;
46
-
47
- case 'update':
48
- logger.info(`[pier-connector] NATS cluster update: ${JSON.stringify(status.data)}`);
49
- break;
50
-
51
- default:
52
- logger.info(`[pier-connector] NATS status: ${status.type}`);
53
- }
54
- }
55
- })();
56
-
57
- return nc;
58
- }
59
-
60
- /**
61
- * Gracefully drain (flush pending + close) a NATS connection.
62
- *
63
- * @param {import('@nats-io/transport-node').NatsConnection} nc
64
- * @param {object} logger
65
- */
66
- export async function drainConnection(nc, logger) {
67
- if (!nc) return;
68
- try {
69
- logger.info('[pier-connector] Draining NATS connection …');
70
- await nc.drain();
71
- logger.info('[pier-connector] ✔ NATS connection drained');
72
- } catch (err) {
73
- logger.error(`[pier-connector] Error draining NATS: ${err.message}`);
74
- }
75
- }
package/src/protocol.js DELETED
@@ -1,142 +0,0 @@
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.1';
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
- * @param {string} [params.targetNodeId]
18
- * @param {string} [params.parentJobId]
19
- * @returns {object}
20
- */
21
- export function createRequestPayload({ task, systemPrompt, timeoutMs, meta, targetNodeId, parentJobId }) {
22
- return {
23
- version: PROTOCOL_VERSION,
24
- id: crypto.randomUUID(),
25
- type: 'task', // Changed from task_request to task to align with backend models
26
- task,
27
- systemPrompt,
28
- timeoutMs: timeoutMs || 60000,
29
- meta: meta || {},
30
- targetNodeId: targetNodeId || null,
31
- parentJobId: parentJobId || null,
32
- };
33
- }
34
-
35
- /**
36
- * Creates a standard task result payload to respond to Pier.
37
- *
38
- * @param {object} params
39
- * @param {string} params.id
40
- * @param {string} params.reply
41
- * @param {number} [params.latencyMs]
42
- * @param {string} [params.workerId]
43
- * @param {string} [params.walletAddress]
44
- * @returns {object}
45
- */
46
- export function createResultPayload({ id, reply, latencyMs, workerId, walletAddress }) {
47
- return {
48
- version: PROTOCOL_VERSION,
49
- id,
50
- type: 'task_result',
51
- ok: true,
52
- result: reply,
53
- latencyMs,
54
- worker: {
55
- id: workerId || 'anonymous-worker',
56
- wallet: walletAddress || null,
57
- },
58
- };
59
- }
60
-
61
- /**
62
- * Creates a standard task error payload to respond to Pier.
63
- *
64
- * @param {object} params
65
- * @param {string} params.id
66
- * @param {string} params.errorCode
67
- * @param {string} params.errorMessage
68
- * @param {string} [params.workerId]
69
- * @param {string} [params.walletAddress]
70
- * @returns {object}
71
- */
72
- export function createErrorPayload({ id, errorCode, errorMessage, workerId, walletAddress }) {
73
- return {
74
- version: PROTOCOL_VERSION,
75
- id,
76
- type: 'task_error',
77
- ok: false,
78
- error: {
79
- code: errorCode,
80
- message: errorMessage,
81
- },
82
- worker: {
83
- id: workerId || 'anonymous-worker',
84
- wallet: walletAddress || null,
85
- },
86
- };
87
- }
88
-
89
- /**
90
- * Validate and normalize an incoming NATS payload to a standard job format.
91
- *
92
- * @param {object} payload
93
- * @returns {{ ok: true, job: object } | { ok: false, error: string }}
94
- */
95
- export function normalizeInboundPayload(payload) {
96
- // If it's a strict v1.1+ protocol request
97
- if (payload.type === 'task' || payload.type === 'task_request') {
98
- if (!payload.task) {
99
- return { ok: false, error: 'Missing "task" field in task payload' };
100
- }
101
- return {
102
- ok: true,
103
- job: {
104
- id: payload.id || crypto.randomUUID(),
105
- task: payload.task,
106
- systemPrompt: payload.systemPrompt,
107
- meta: payload.meta || {},
108
- targetNodeId: payload.targetNodeId || null,
109
- parentJobId: payload.parentJobId || null,
110
- },
111
- };
112
- }
113
-
114
- // Support for Join-Chat wakeup signals (BUG-26/29)
115
- if (payload.type === 'wakeup') {
116
- const jobId = payload.jobId || payload.id;
117
- if (!jobId) return { ok: false, error: 'Missing jobId in wakeup payload' };
118
- return {
119
- ok: true,
120
- job: {
121
- id: jobId,
122
- type: 'wakeup'
123
- }
124
- };
125
- }
126
-
127
- // Fallback for legacy / generic JSON structures
128
- const task = payload.task ?? payload.prompt ?? payload.content ?? '';
129
- if (!task) {
130
- return { ok: false, error: 'Missing "task" or "prompt" field in payload' };
131
- }
132
-
133
- return {
134
- ok: true,
135
- job: {
136
- id: payload.id || crypto.randomUUID(),
137
- task,
138
- systemPrompt: payload.systemPrompt,
139
- meta: payload.meta || {},
140
- },
141
- };
142
- }