@gholl-studio/pier-connector 0.2.29 → 0.2.31

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.29",
4
+ "version": "0.2.31",
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
+ "pier-sdk": "file:../packages/pier-sdk",
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 '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
  /**
@@ -416,15 +411,17 @@ export default function register(api) {
416
411
  let finalText = job.task;
417
412
  if (!isTargeted) {
418
413
  finalText = `【PIER MARKETPLACE OPEN JOB】\nJob ID: ${job.id}\nTask: ${job.task}\n\n=== CRITICAL ===\nYou MUST use \`pier_bid_task\` to bid. Do not solve directly.`;
414
+ } else {
415
+ // Only subscribe to chat for targeted/assigned jobs
416
+ await this.subscribeToJobMessages(job.id);
419
417
  }
420
418
 
421
- await this.subscribeToJobMessages(job.id);
422
419
  await this.receiveIncoming({
423
420
  accountId: this.accountId,
424
421
  senderId: `pier:${senderCore}`,
425
422
  text: finalText,
426
423
  }, job.id);
427
-
424
+
428
425
  this.isBusy = false;
429
426
  }
430
427
 
@@ -460,30 +457,10 @@ export default function register(api) {
460
457
  }
461
458
 
462
459
  async autoRegister() {
463
- const wallet = new ethers.Wallet(this.config.privateKey);
464
- const address = wallet.address;
465
-
466
- const challengeRes = await fetch(`${this.config.pierApiUrl}/auth/challenge?wallet_address=${address}`);
467
- const { challenge } = await challengeRes.json();
468
- const signature = await wallet.signMessage(challenge);
469
-
470
- const loginRes = await fetch(`${this.config.pierApiUrl}/auth/login`, {
471
- method: 'POST',
472
- headers: { 'Content-Type': 'application/json' },
473
- body: JSON.stringify({ wallet_address: address, challenge, signature })
474
- });
475
- const { api_key } = await loginRes.json();
476
-
477
460
  const hostName = `${api?.getRuntimeInfo?.()?.hostname ?? 'Auto'}-${this.accountId}`;
478
- const regRes = await fetch(`${this.config.pierApiUrl}/nodes/register`, {
479
- method: 'POST',
480
- headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${api_key}` },
481
- body: JSON.stringify({ wallet_address: address, name: hostName })
482
- });
483
- const { id, secret_key } = await regRes.json();
484
-
485
- this.config.nodeId = id;
486
- 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;
487
464
  }
488
465
 
489
466
  async start() {
@@ -599,18 +576,24 @@ export default function register(api) {
599
576
  try {
600
577
  const replySubject = `chat.${jobId}`;
601
578
 
602
- const chatPayload = {
603
- id: crypto.randomUUID ? crypto.randomUUID() : (Math.random().toString(36).substring(2)),
604
- job_id: jobId,
605
- sender_id: robot.config.nodeId || 'anonymous',
606
- sender_type: 'node',
607
- content: text,
608
- created_at: new Date().toISOString(),
609
- auth_token: robot.config.secretKey
610
- };
611
-
612
- await robot.js.publish(replySubject, new TextEncoder().encode(JSON.stringify(chatPayload)));
613
- logger.info(`[pier-connector][${accountId}] 💬 Agent reply published to NATS for job ${jobId} (Subject: ${replySubject})`);
579
+ // Only publish replies for jobs assigned to this robot
580
+ if (!metadata?.isTargeted) {
581
+ logger.debug(`[pier-connector][${accountId}] 🫨 Suppressing reply for non-assigned job ${jobId}`);
582
+ } else {
583
+ const chatPayload = {
584
+ id: crypto.randomUUID ? crypto.randomUUID() : (Math.random().toString(36).substring(2)),
585
+ job_id: jobId,
586
+ sender_id: robot.config.nodeId || 'anonymous',
587
+ sender_name: accountId,
588
+ sender_type: 'node',
589
+ content: text,
590
+ created_at: new Date().toISOString(),
591
+ auth_token: robot.config.secretKey
592
+ };
593
+
594
+ await robot.js.publish(replySubject, new TextEncoder().encode(JSON.stringify(chatPayload)));
595
+ logger.info(`[pier-connector][${accountId}] 💬 Agent reply published to NATS for job ${jobId} (Subject: ${replySubject})`);
596
+ }
614
597
  } catch (err) {
615
598
  logger.error(`[pier-connector][${accountId}] Failed to publish reply: ${err.message}`);
616
599
  }
@@ -745,8 +728,9 @@ export default function register(api) {
745
728
  },
746
729
  required: ['jobId', 'text']
747
730
  },
748
- async execute(_id, params) {
731
+ async execute(_id, params, ctx) {
749
732
  const accountId = params.accountId || 'default';
733
+ logger.info(`[pier-connector] 🛠️ Tool called: pier_chat | jobId=${params.jobId} | accountId=${accountId}`);
750
734
  const robot = instances.get(accountId) || instances.values().next().value;
751
735
  if (!robot || robot.connectionStatus !== 'connected') {
752
736
  return { content: [{ type: 'text', text: 'Error: Robot not connected' }] };
@@ -754,10 +738,20 @@ export default function register(api) {
754
738
 
755
739
  try {
756
740
  const subject = `chat.${params.jobId}`;
741
+ let metadata = robot.activeNodeJobs.get(params.jobId);
742
+
743
+ if (!metadata && ctx.to) {
744
+ const toId = ctx.to.replace(/^pier:/, '');
745
+ metadata = robot.activeNodeJobs.get(toId);
746
+ }
747
+
748
+ const jobId = metadata?.pierJobId || params.jobId;
749
+
757
750
  const payload = {
758
751
  id: crypto.randomUUID ? crypto.randomUUID() : (Math.random().toString(36).substring(2)),
759
- job_id: params.jobId,
752
+ job_id: jobId,
760
753
  sender_id: robot.config.nodeId,
754
+ sender_name: accountId,
761
755
  sender_type: 'node',
762
756
  content: params.text,
763
757
  created_at: new Date().toISOString(),
@@ -837,6 +831,41 @@ export default function register(api) {
837
831
  comment: { type: 'string', description: 'A short review' }
838
832
  }, 'user');
839
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
+
840
869
  // ── 4. Register /pier status command ───────────────────────────────
841
870
 
842
871
  api.registerCommand({
@@ -920,55 +949,35 @@ export default function register(api) {
920
949
  { type: 'input', name: 'pierApiUrl', message: 'Pier API URL:', default: currentConfig.pierApiUrl },
921
950
  { type: 'input', name: 'nodeId', message: 'Bot Node ID (UUID):', default: finalNodeId },
922
951
  { type: 'password', name: 'secretKey', message: 'Bot Secret Key:', default: finalSecretKey },
923
- { 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(', ') }
924
954
  ]);
925
955
  finalNodeId = manualAnswers.nodeId;
926
956
  finalSecretKey = manualAnswers.secretKey;
927
957
  finalWallet = manualAnswers.walletAddress;
958
+ currentConfig.capabilities = manualAnswers.capabilities ? manualAnswers.capabilities.split(',').map(s => s.trim()) : currentConfig.capabilities;
928
959
  currentConfig.pierApiUrl = manualAnswers.pierApiUrl;
929
960
  } else if (setupMethod === 'auto') {
930
961
  const autoAnswers = await inquirer.prompt([
931
962
  { type: 'input', name: 'pierApiUrl', message: 'Pier API URL:', default: currentConfig.pierApiUrl },
932
- { 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(', ') }
933
965
  ]);
934
966
  console.log('\n\x1b[36mRegistering your node...\x1b[0m');
935
967
  try {
936
- const wallet = new ethers.Wallet(autoAnswers.privateKey);
937
- console.log(`\x1b[32m✔ Loaded wallet: ${wallet.address}\x1b[0m`);
938
- finalWallet = finalWallet || wallet.address;
939
- finalPrivateKey = autoAnswers.privateKey;
940
- currentConfig.pierApiUrl = autoAnswers.pierApiUrl;
941
-
942
- const challengeRes = await fetch(`${currentConfig.pierApiUrl}/auth/challenge?wallet_address=${wallet.address}`);
943
- if (!challengeRes.ok) throw new Error("Failed to get challenge");
944
- const { challenge } = await challengeRes.json();
945
-
946
- const signature = await wallet.signMessage(challenge);
947
-
948
- const loginRes = await fetch(`${currentConfig.pierApiUrl}/auth/login`, {
949
- method: 'POST',
950
- headers: { 'Content-Type': 'application/json' },
951
- body: JSON.stringify({ wallet_address: wallet.address, challenge, signature })
952
- });
953
- if (!loginRes.ok) throw new Error("Failed to login");
954
- const { api_key } = await loginRes.json();
955
-
968
+ const tempClient = new PierClient({ apiUrl: autoAnswers.pierApiUrl });
956
969
  const hostName = api?.getRuntimeInfo?.()?.hostname ?? 'Auto-Node';
957
- const regRes = await fetch(`${currentConfig.pierApiUrl}/nodes/register`, {
958
- method: 'POST',
959
- headers: {
960
- 'Content-Type': 'application/json',
961
- 'Authorization': `Bearer ${api_key}`
962
- },
963
- body: JSON.stringify({ wallet_address: wallet.address, name: hostName })
964
- });
965
970
 
966
- if (!regRes.ok) throw new Error(`Failed to auto-register node (${regRes.status})`);
967
- 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;
968
979
 
969
- finalNodeId = id;
970
- finalSecretKey = secret_key;
971
- 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`);
972
981
  } catch (err) {
973
982
  console.error(`\x1b[31m✖ Failed to auto-register: ${err.message}\x1b[0m`);
974
983
  return; // Stop setup
@@ -1014,7 +1023,8 @@ export default function register(api) {
1014
1023
  nodeId: finalNodeId,
1015
1024
  secretKey: finalSecretKey,
1016
1025
  walletAddress: finalWallet,
1017
- agentId: finalAgentId
1026
+ agentId: finalAgentId,
1027
+ capabilities: currentConfig.capabilities
1018
1028
  };
1019
1029
 
1020
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 '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
- }