@gholl-studio/pier-connector 0.2.51 → 0.3.0

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/src/index.js DELETED
@@ -1,1222 +0,0 @@
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 { PierClient, protocol } from '@gholl-studio/pier-sdk';
12
- const { createRequestPayload, createResultPayload, createErrorPayload } = protocol;
13
- import { parseJob, safeRespond, truncate } from './job-handler.js';
14
- import { DEFAULTS } from './config.js';
15
- import { jetstream, jetstreamManager, AckPolicy, DeliverPolicy } from '@nats-io/jetstream';
16
- import inquirer from 'inquirer';
17
- import fs from 'fs';
18
- import path from 'path';
19
-
20
- /**
21
- * OpenClaw plugin register function.
22
- *
23
- * @param {object} api – OpenClaw plugin API
24
- */
25
- export default function register(api) {
26
- const logger = api.logger;
27
-
28
- // ── shared state (Instances) ───────────────────────────────────
29
-
30
- /** @type {Map<string, any>} */
31
- const instances = new Map();
32
-
33
- /** Aggregated stats from all robots since start */
34
- let totalJobsReceived = 0;
35
- let totalJobsCompleted = 0;
36
- let totalJobsFailed = 0;
37
-
38
- // ── resolve plugin config ──────────────────────────────────────────
39
-
40
- function resolveConfigs() {
41
- const globalAccounts = api.runtime?.config?.channels?.['pier']?.accounts || {};
42
- const pluginAccounts = api.config?.channels?.['pier']?.accounts || {};
43
- const rawAccounts = { ...globalAccounts, ...pluginAccounts };
44
-
45
- const legacyCfg = api.config?.plugins?.entries?.['pier-connector']?.config || api.config || {};
46
-
47
- // If no accounts defined at all, fallback to 'default' using legacy/env config
48
- if (Object.keys(rawAccounts).length === 0) {
49
- logger.info(`[pier-connector] No accounts found in global or plugin config. Falling back to default.`);
50
- return [{
51
- accountId: 'default',
52
- ...mergedCfgFrom(legacyCfg, {})
53
- }];
54
- }
55
-
56
- const configs = Object.entries(rawAccounts).map(([id, account]) => ({
57
- accountId: id,
58
- ...mergedCfgFrom(legacyCfg, account)
59
- }));
60
-
61
- logger.info(`[pier-connector] Loaded ${configs.length} account(s): ${configs.map(c => c.accountId).join(', ')}`);
62
- configs.forEach(c => {
63
- if (c.agentId) logger.info(`[pier-connector] Account '${c.accountId}' has explicit agentId binding: ${c.agentId}`);
64
- });
65
-
66
- return configs;
67
- }
68
-
69
- function mergedCfgFrom(legacy, account) {
70
- const merged = { ...legacy, ...account };
71
- return {
72
- pierApiUrl: merged.pierApiUrl || DEFAULTS.PIER_API_URL,
73
- nodeId: merged.nodeId || DEFAULTS.NODE_ID,
74
- secretKey: merged.secretKey || DEFAULTS.SECRET_KEY,
75
- privateKey: merged.privateKey || process.env.PIER_PRIVATE_KEY || DEFAULTS.PRIVATE_KEY,
76
- natsUrl: merged.natsUrl || DEFAULTS.NATS_URL,
77
- subject: merged.subject || DEFAULTS.SUBJECT,
78
- publishSubject: merged.publishSubject || DEFAULTS.PUBLISH_SUBJECT,
79
- queueGroup: merged.queueGroup || DEFAULTS.QUEUE_GROUP,
80
- agentId: merged.agentId || DEFAULTS.AGENT_ID,
81
- walletAddress: merged.walletAddress || DEFAULTS.WALLET_ADDRESS,
82
- capabilities: merged.capabilities || ['translation', 'code-execution', 'reasoning', 'vision'],
83
- };
84
- }
85
-
86
- /**
87
- * PierRobot class encapsulates the connection, heartbeat, and job handling
88
- * for a single robot account.
89
- */
90
- class PierRobot {
91
- constructor(config) {
92
- this.config = config;
93
- this.accountId = config.accountId;
94
- this.client = new PierClient({
95
- apiUrl: config.pierApiUrl,
96
- natsUrl: config.natsUrl,
97
- logger
98
- });
99
- this.nc = null;
100
- this.js = null;
101
- this.jsm = null;
102
- this.subscription = null;
103
- this.heartbeatTimer = null;
104
- this.connectionStatus = 'disconnected';
105
- this.jobSubscriptions = new Map();
106
- this.jobStopTimeouts = new Map();
107
- this.activeNodeJobs = new Map();
108
- this.isBusy = false;
109
- this.stats = { received: 0, completed: 0, failed: 0 };
110
- this.lastHeartbeatError = null;
111
- this.connectedAt = null;
112
- }
113
-
114
- async init() {
115
- try {
116
- this.nc = await this.client.connectNats();
117
- this.js = jetstream(this.nc);
118
- this.jsm = await jetstreamManager(this.nc);
119
-
120
- this.connectedAt = new Date();
121
- this.connectionStatus = 'connected';
122
-
123
- // Subscribe to tasks
124
- await this.subscribeToTasks();
125
-
126
- // Start heartbeat
127
- this.startHeartbeat();
128
-
129
- return true;
130
- } catch (err) {
131
- this.connectionStatus = 'error';
132
- logger.error(`[pier-connector] Account ${this.accountId} failed to connect: ${err.message}`);
133
- return false;
134
- }
135
- }
136
-
137
- async heartbeat() {
138
- if (!this.config.nodeId || !this.config.secretKey) return null;
139
- try {
140
- const data = await this.client.heartbeat(
141
- this.config.nodeId,
142
- this.config.secretKey,
143
- this.config.capabilities
144
- );
145
- this.lastHeartbeatError = null;
146
- return data.nats_config || null;
147
- } catch (err) {
148
- this.lastHeartbeatError = err.message;
149
- logger.warn(`[pier-connector][${this.accountId}] Heartbeat failed: ${err.message}`);
150
- return null;
151
- }
152
- }
153
-
154
- async claimJob(jobId) {
155
- return await this.client.claimJob(jobId, this.config.nodeId, this.config.secretKey);
156
- }
157
-
158
- /**
159
- * Routes an incoming message from Pier to the OpenClaw agent.
160
- * Replaces the non-existent api.runtime.sendIncoming.
161
- */
162
- async receiveIncoming(inbound, jobId) {
163
- if (!api.runtime?.channel?.reply) {
164
- logger.error(`[pier-connector][${this.accountId}] SDK Error: api.runtime.channel.reply is not available.`);
165
- return;
166
- }
167
-
168
- logger.info(`[pier-connector:trace] receiveIncoming triggered for jobId=${jobId}. accountId='${inbound.accountId}', senderId='${inbound.senderId}'`);
169
-
170
- // 1. Resolve Global Configuration
171
- // In OpenClaw V2, we need the FULL config for routing to work, but api.runtime.config is often scoped to the plugin.
172
- // We search for the root config and fall back to whatever is available.
173
- const rootConfig = api.rootConfig || api.runtime?.globalConfig || api.runtime?.config || {};
174
-
175
- // Diagnostic logging (only if we suspect it's still failing)
176
- const bindingsCount = Array.isArray(rootConfig.bindings) ? rootConfig.bindings.length : 0;
177
- const agentsCount = rootConfig.agents?.list ? (Array.isArray(rootConfig.agents.list) ? rootConfig.agents.list.length : Object.keys(rootConfig.agents.list).length) : 0;
178
-
179
- logger.info(`[pier-connector:trace] Diagnostic: rootConfig source=${api.rootConfig ? 'api.rootConfig' : (api.runtime?.globalConfig ? 'globalConfig' : 'runtime.config')}, bindings=${bindingsCount}, agents=${agentsCount}`);
180
-
181
- // 2. Resolve Agent Route via SDK
182
- const route = api.runtime.channel.routing.resolveAgentRoute({
183
- cfg: rootConfig,
184
- channel: 'pier',
185
- account: inbound.accountId,
186
- peer: { kind: 'direct', id: jobId }
187
- });
188
-
189
- logger.info(`[pier-connector:trace] resolveAgentRoute returned: route.agentId='${route?.agentId}', route.accountId='${route?.accountId}'`);
190
-
191
- // 3. Robust Routing Decision Tree
192
- let finalAgentId = null;
193
- let routingSource = 'unresolved';
194
-
195
- // A. Check explicit account-level binding in plugin local config (this.config)
196
- if (this.config.agentId && this.config.agentId !== 'main' && this.config.agentId !== 'default') {
197
- finalAgentId = this.config.agentId;
198
- routingSource = 'plugin-local-config';
199
- }
200
-
201
- // B. Check Global Bindings (Manual Scan of rootConfig.bindings)
202
- if (!finalAgentId) {
203
- const bindings = Array.isArray(rootConfig.bindings) ? rootConfig.bindings : [];
204
- const binding = bindings.find(b =>
205
- b.match?.channel === 'pier' &&
206
- (b.match?.accountId === inbound.accountId || b.match?.account === inbound.accountId)
207
- );
208
- if (binding?.agentId && binding.agentId !== 'main') {
209
- finalAgentId = binding.agentId;
210
- routingSource = 'manual-global-bindings-match';
211
- }
212
- }
213
-
214
- // C. SDK Routing Result (if it actually found something non-default)
215
- if (!finalAgentId && route.agentId && route.agentId !== 'main' && route.agentId !== 'default') {
216
- finalAgentId = route.agentId;
217
- routingSource = 'sdk-global-bindings';
218
- }
219
-
220
- // D. Name-Matching Fallback (CRITICAL: account name == agent ID)
221
- // If we see account 'sunwukong', and we have an agent 'sunwukong', route there!
222
- if (!finalAgentId && inbound.accountId && inbound.accountId !== 'default' && inbound.accountId !== 'main') {
223
- // Check if such an agent exists in rootConfig
224
- const agents = rootConfig.agents?.list || {};
225
- const agentExists = Array.isArray(agents) ? agents.some(a => a.id === inbound.accountId) : !!agents[inbound.accountId];
226
-
227
- if (agentExists) {
228
- finalAgentId = inbound.accountId;
229
- routingSource = 'account-agent-name-match';
230
- }
231
- }
232
-
233
- // E. Ultimate Fallback
234
- if (!finalAgentId) {
235
- finalAgentId = route.agentId || 'main'; // Use SDK result or hard default 'main'
236
- routingSource = route.agentId ? 'sdk-default-fallback' : 'hardcoded-main-fallback';
237
- }
238
-
239
- logger.info(`[pier-connector] Routing account '${inbound.accountId}' -> agent '${finalAgentId}' (Source: ${routingSource})`);
240
-
241
- const dynamicSessionKey = `pier-job-${jobId}`;
242
- const metadata = this.activeNodeJobs.get(jobId);
243
- let injectedPrompt = "";
244
-
245
- if (metadata && metadata.isTargeted === false) {
246
- injectedPrompt = [
247
- "=========================================",
248
- "CRITICAL DIRECTIVE - OPEN BIDDING MARKETPLACE",
249
- "=========================================",
250
- "You are an autonomous AI worker on Pier. This job currently has NO assigned worker.",
251
- "1. DO NOT directly solve the user's task or output the final answer right now.",
252
- "2. You MUST USE the \`pier_bid_task\` tool to send a pitch/bid to the employer explaining why you are best equipped to solve this.",
253
- "3. Stop executing after submitting the bid.",
254
- "4. You MUST use the Job ID provided below when calling the tool.",
255
- `Job ID: ${jobId}`,
256
- inbound.systemPrompt ? `\nEmployer Guidelines:\n${inbound.systemPrompt}` : ""
257
- ].join('\n');
258
- } else {
259
- injectedPrompt = [
260
- "=========================================",
261
- "CRITICAL DIRECTIVE - PIER TRADING NETWORK",
262
- "=========================================",
263
- "You are an autonomous AI worker on Pier.",
264
- "1. You MUST USE the \`pier_finish_task\` tool to submit the final result when your work is complete.",
265
- "2. If you need to negotiate or clarify requirements with the employer, use \`pier_chat\`.",
266
- "3. Do not just wait in silence. Always officially finish the task if the employer's goal is met.",
267
- inbound.systemPrompt ? `\nEmployer Guidelines:\n${inbound.systemPrompt}` : ""
268
- ].join('\n');
269
- }
270
-
271
- // MsgContext uses PascalCase for keys in OpenClaw V2 SDK
272
- const ctxPayload = api.runtime.channel.reply.finalizeInboundContext({
273
- agentId: finalAgentId, // Keep lowercase here as it's the target system ID
274
- Body: inbound.text,
275
- BodyForAgent: inbound.text,
276
- RawBody: inbound.text,
277
- From: inbound.senderId,
278
- To: `pier:${jobId}`,
279
- SessionKey: dynamicSessionKey,
280
- AccountId: inbound.accountId,
281
- ChatType: 'direct',
282
- SenderId: inbound.senderId,
283
- Provider: 'pier',
284
- Surface: 'pier',
285
- OriginatingChannel: 'pier',
286
- OriginatingTo: `pier:${jobId}`,
287
- WasMentioned: true,
288
- CommandAuthorized: true,
289
- SystemPrompt: injectedPrompt,
290
- MessageId: inbound.messageId || jobId,
291
- Metadata: {
292
- ...metadata,
293
- accountId: this.accountId,
294
- pierJobId: jobId,
295
- routingSource: routingSource
296
- }
297
- });
298
-
299
- const { dispatcher, markDispatchIdle } = api.runtime.channel.reply.createReplyDispatcherWithTyping({
300
- cfg: api.runtime.config,
301
- agentId: finalAgentId,
302
- deliver: async (payload) => {
303
- const currentMeta = this.activeNodeJobs.get(jobId);
304
- await pierChannel.outbound.sendText({
305
- text: payload.text,
306
- to: `pier:${jobId}`,
307
- metadata: {
308
- ...currentMeta,
309
- accountId: this.accountId,
310
- pierJobId: jobId
311
- },
312
- });
313
- }
314
- });
315
-
316
- if (api.runtime.channel.session?.recordSessionMetaFromInbound) {
317
- try {
318
- const storePath = api.runtime.channel.session.resolveStorePath(api.config, dynamicSessionKey);
319
- await api.runtime.channel.session.recordSessionMetaFromInbound({
320
- storePath, sessionKey: dynamicSessionKey, ctx: ctxPayload
321
- });
322
- } catch (err) {}
323
- }
324
-
325
- try {
326
- await api.runtime.channel.reply.dispatchReplyFromConfig({
327
- ctx: ctxPayload, cfg: api.runtime.config, dispatcher
328
- });
329
- } finally {
330
- markDispatchIdle();
331
- }
332
- }
333
- async subscribeToJobMessages(jobId) {
334
- if (!this.js) return;
335
-
336
- const chatSubject = `chat.${jobId}`;
337
- const streamName = 'PIER_JOBS';
338
- const nodeSlug = this.config.nodeId.replace(/[^a-zA-Z0-9]/g, '_');
339
- const jobSlug = jobId.replace(/[^a-zA-Z0-9]/g, '_');
340
- const durableName = `pier_chat_${nodeSlug}_${jobSlug}`;
341
-
342
- try {
343
- let consumer;
344
- try {
345
- consumer = await this.js.consumers.get(streamName, durableName);
346
- } catch {
347
- await this.jsm.consumers.add(streamName, {
348
- durable_name: durableName,
349
- filter_subject: chatSubject,
350
- deliver_policy: DeliverPolicy.New,
351
- ack_policy: AckPolicy.Explicit,
352
- ack_wait: 1000 * 60 * 60,
353
- });
354
- consumer = await this.js.consumers.get(streamName, durableName);
355
- }
356
-
357
- const iter = await consumer.consume();
358
- this.jobSubscriptions.set(jobId, iter);
359
-
360
- (async () => {
361
- logger.info(`[pier-connector][${this.accountId}] \u{1F442} Monitoring ${chatSubject}`);
362
- const processedMessages = new Set();
363
- for await (const msg of iter) {
364
- try {
365
- const rawMsg = new TextDecoder().decode(msg.data);
366
- let msgPayload = JSON.parse(rawMsg);
367
-
368
- if (msgPayload.id && processedMessages.has(msgPayload.id)) { msg.ack(); continue; }
369
- if (msgPayload.sender_id === this.config.nodeId) { msg.ack(); continue; }
370
- if (msgPayload.type === 'receipt') { msg.ack(); continue; }
371
- // Skip system_action messages — those are for the backend state machine
372
- if (msgPayload.type === 'system_action' || msgPayload.msg_type === 'system_action') { msg.ack(); continue; }
373
-
374
- // Send receipt
375
- try {
376
- await this.js.publish(chatSubject, new TextEncoder().encode(JSON.stringify({
377
- type: 'receipt', msg_id: msgPayload.id, reader_id: this.config.nodeId
378
- })));
379
- } catch (err) {}
380
-
381
- const content = msgPayload.content;
382
- if (!content) { msg.ack(); continue; }
383
-
384
- logger.info(`[pier-connector][${this.accountId}] \u{1F4AC} Chat: [${jobId}] ${msgPayload.sender_id}: "${truncate(content, 40)}"`);
385
- if (msgPayload.id) processedMessages.add(msgPayload.id);
386
-
387
- // Only the assigned robot should process chat messages
388
- const jobMeta = this.activeNodeJobs.get(jobId);
389
- if (!jobMeta || !jobMeta.isTargeted) {
390
- msg.ack();
391
- continue;
392
- }
393
-
394
- msg.ack();
395
-
396
- logger.info(`[pier-connector:trace] NATS Chat Message received on PierRobot instance accountId='${this.accountId}'. Passing to receiveIncoming...`);
397
- await this.receiveIncoming({
398
- accountId: this.accountId,
399
- senderId: `pier:${msgPayload.sender_id}`,
400
- text: content,
401
- }, jobId);
402
- } catch (err) { logger.error(`[pier-connector] Chat err: ${err.message}`); }
403
- }
404
- })();
405
- } catch (err) {
406
- logger.error(`[pier-connector][${this.accountId}] Failed to setup chat consumer: ${err.message}`);
407
- }
408
- }
409
-
410
- async handleMessage(msg) {
411
- const rawData = new TextDecoder().decode(msg.data);
412
- let payload;
413
- try {
414
- payload = JSON.parse(rawData);
415
- } catch (e) {
416
- msg.ack();
417
- return;
418
- }
419
-
420
- if (payload.type === 'wakeup') {
421
- const { jobId } = payload;
422
- if (jobId) {
423
- logger.info(`[pier-connector][${this.accountId}] ⏰ Received wakeup signal for job ${jobId}`);
424
- if (!this.jobSubscriptions.has(jobId)) {
425
- this.activeNodeJobs.set(jobId, { pierJobId: jobId, isTargeted: true });
426
- await this.subscribeToJobMessages(jobId);
427
- }
428
- msg.ack();
429
- } else {
430
- msg.ack();
431
- }
432
- return;
433
- }
434
-
435
- if (this.isBusy) {
436
- logger.debug(`[pier-connector][${this.accountId}] 🚫 Busy. Ignoring payload...`);
437
- msg.nak();
438
- return;
439
- }
440
-
441
- if (payload.assigned_node_id && payload.assigned_node_id !== this.config.nodeId) {
442
- msg.nak();
443
- return;
444
- }
445
-
446
- const jobIdToClaim = payload.id;
447
- const isTargeted = !!(payload.assigned_node_id || payload.targetNodeId || payload.TargetNodeID);
448
-
449
- if (jobIdToClaim && isTargeted) {
450
- const claimResult = await this.claimJob(jobIdToClaim);
451
- if (!claimResult.ok) {
452
- if (claimResult.alreadyClaimed) msg.ack();
453
- else msg.nak();
454
- return;
455
- }
456
- }
457
-
458
- this.isBusy = true;
459
- msg.ack();
460
-
461
- this.stats.received++;
462
- totalJobsReceived++;
463
-
464
- const parsed = parseJob(msg, logger);
465
- if (!parsed.ok) {
466
- this.stats.failed++;
467
- totalJobsFailed++;
468
- this.isBusy = false;
469
- logger.error(`[pier-connector][${this.accountId}] Job parse error: ${parsed.error}`);
470
- safeRespond(msg, createErrorPayload({
471
- id: jobIdToClaim || 'unknown',
472
- errorCode: 'PARSE_ERROR',
473
- errorMessage: parsed.error,
474
- workerId: this.config.nodeId,
475
- walletAddress: this.config.walletAddress,
476
- }));
477
- return;
478
- }
479
-
480
- const { job } = parsed;
481
- const senderCore = job.meta?.sender ?? 'anonymous';
482
-
483
- this.activeNodeJobs.set(job.id, {
484
- pierJobId: job.id,
485
- pierNatsMsg: msg,
486
- pierStartTime: performance.now(),
487
- pierMeta: job.meta,
488
- isRealtimeMsg: false,
489
- isTargeted: isTargeted
490
- });
491
-
492
- let finalText = job.task;
493
- if (!isTargeted) {
494
- 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.`;
495
- } else {
496
- // Only subscribe to chat for targeted/assigned jobs
497
- await this.subscribeToJobMessages(job.id);
498
- }
499
-
500
- logger.info(`[pier-connector:trace] NATS Marketplace Job received on PierRobot instance accountId='${this.accountId}'. Passing to receiveIncoming...`);
501
- await this.receiveIncoming({
502
- accountId: this.accountId,
503
- senderId: `pier:${senderCore}`,
504
- text: finalText,
505
- }, job.id);
506
-
507
- this.isBusy = false;
508
- }
509
-
510
- async setupMarketplaceConsumer(streamName, subjectName, durableName) {
511
- try {
512
- let consumer;
513
- try {
514
- consumer = await this.js.consumers.get(streamName, durableName);
515
- } catch {
516
- await this.jsm.consumers.add(streamName, {
517
- durable_name: durableName,
518
- filter_subject: subjectName,
519
- deliver_policy: DeliverPolicy.New,
520
- ack_policy: AckPolicy.Explicit,
521
- });
522
- consumer = await this.js.consumers.get(streamName, durableName);
523
- }
524
-
525
- const iter = await consumer.consume();
526
- this.subscription = iter;
527
-
528
- (async () => {
529
- for await (const msg of iter) {
530
- await this.handleMessage(msg);
531
- }
532
- })().catch(err => {
533
- logger.error(`[pier-connector][${this.accountId}] Consumer error: ${err.message}`);
534
- });
535
- } catch (err) {
536
- logger.error(`[pier-connector][${this.accountId}] Setup failed: ${err.message}`);
537
- throw err;
538
- }
539
- }
540
-
541
- async autoRegister() {
542
- const hostName = `${api?.getRuntimeInfo?.()?.hostname ?? 'Auto'}-${this.accountId}`;
543
- const { nodeId, secretKey } = await this.client.autoRegister(this.config.privateKey, hostName);
544
- this.config.nodeId = nodeId;
545
- this.config.secretKey = secretKey;
546
- }
547
-
548
- async start() {
549
- this.connectionStatus = 'connecting';
550
- try {
551
- if (!this.config.nodeId && this.config.privateKey) await this.autoRegister();
552
- const natsConfig = await this.heartbeat();
553
- if (!natsConfig) throw new Error('No NATS config');
554
- if (natsConfig.url) {
555
- // Update SDK NATS client with latest URL from heartbeat
556
- this.client.nats.url = natsConfig.url;
557
- }
558
-
559
- this.nc = await this.client.connectNats();
560
- this.js = jetstream(this.nc);
561
- this.jsm = await jetstreamManager(this.nc);
562
- this.connectionStatus = 'connected';
563
- this.connectedAt = new Date();
564
-
565
- const streamName = 'PIER_JOBS';
566
- const durableNameMarket = `pier_market_${this.config.nodeId.replace(/-/g, '_')}`;
567
- const durableNameDirect = `pier_node_${this.config.nodeId.replace(/-/g, '_')}`;
568
-
569
- await this.setupMarketplaceConsumer(streamName, this.config.subject, durableNameMarket);
570
- await this.setupMarketplaceConsumer(streamName, `jobs.node.${this.config.nodeId}`, durableNameDirect);
571
-
572
- this.heartbeatTimer = setInterval(() => this.heartbeat(), 60000);
573
- } catch (err) {
574
- this.connectionStatus = 'error';
575
- logger.error(`[pier-connector][${this.accountId}] Start failed: ${err.message}`);
576
- }
577
- }
578
-
579
- async stop() {
580
- if (this.heartbeatTimer) clearInterval(this.heartbeatTimer);
581
- if (this.nc) await this.client.drainNats();
582
- this.connectionStatus = 'disconnected';
583
- }
584
- }
585
-
586
- // ── 1. Register messaging channel ──────────────────────────────────
587
-
588
- const pierChannel = {
589
- id: 'pier',
590
-
591
- meta: {
592
- id: 'pier',
593
- label: 'Pier',
594
- selectionLabel: 'Pier (NATS Job Marketplace)',
595
- docsPath: '/plugins/pier-connector',
596
- blurb: 'Accept and publish tasks on the Pier job marketplace via NATS.',
597
- aliases: ['pier-connector'],
598
- },
599
-
600
- id: 'pier',
601
-
602
- capabilities: {
603
- chatTypes: ['direct'],
604
- },
605
-
606
- config: {
607
- listAccountIds: (cfg) => Object.keys(cfg.channels?.pier?.accounts ?? {}),
608
- resolveAccount: (cfg, accountId) => cfg.channels?.pier?.accounts?.[accountId ?? 'default'] ?? {
609
- accountId: accountId ?? 'default',
610
- enabled: true,
611
- },
612
- isConfigured: (account) => Boolean(account.nodeId && account.secretKey) || Boolean(account.privateKey),
613
- },
614
-
615
- setup: {
616
- applyAccountConfig: ({ cfg, accountId, input }) => {
617
- const draft = structuredClone(cfg);
618
- draft.channels = draft.channels || {};
619
- draft.channels.pier = draft.channels.pier || {};
620
- draft.channels.pier.accounts = draft.channels.pier.accounts || {};
621
-
622
- const acct = draft.channels.pier.accounts[accountId] || { enabled: true };
623
-
624
- if (input.token) {
625
- const parts = input.token.split(':');
626
- if (parts.length >= 2) {
627
- acct.nodeId = parts[0];
628
- acct.secretKey = parts.slice(1).join(':');
629
- } else {
630
- // fallback if user just pasted one string
631
- acct.nodeId = parts[0];
632
- }
633
- }
634
-
635
- draft.channels.pier.accounts[accountId] = acct;
636
- return draft;
637
- }
638
- },
639
-
640
- // Note: login is now moved to top-level of api.registerChannel
641
-
642
- outbound: {
643
- deliveryMode: 'direct',
644
-
645
- sendText: async (ctx) => {
646
- const text = ctx.text;
647
- let metadata = ctx.metadata;
648
- const accountId = metadata?.accountId || 'default';
649
- const robot = instances.get(accountId);
650
-
651
- logger.info(`[pier-connector][${accountId}] 📤 Agent sending reply: "${truncate(text, 40)}" (To: ${ctx.to})`);
652
-
653
- if (!robot) {
654
- logger.error(`[pier-connector] ✖ No robot instance found for account ${accountId}`);
655
- return { ok: false, error: 'Robot instance not found' };
656
- }
657
-
658
- if (!metadata && ctx.to) {
659
- const toId = ctx.to.replace(/^pier:/, '');
660
- metadata = robot.activeNodeJobs.get(toId);
661
- }
662
-
663
- const jobId = metadata?.pierJobId;
664
-
665
- if (jobId && robot.js) {
666
- try {
667
- const replySubject = `chat.${jobId}`;
668
-
669
- // Only publish replies for jobs assigned to this robot
670
- if (!metadata?.isTargeted) {
671
- logger.debug(`[pier-connector][${accountId}] 🫨 Suppressing reply for non-assigned job ${jobId}`);
672
- } else {
673
- const chatPayload = {
674
- id: (typeof crypto !== 'undefined' && crypto.randomUUID) ? crypto.randomUUID() : (Math.random().toString(36).substring(2) + Date.now().toString(36)),
675
- job_id: jobId,
676
- sender_id: robot.config.nodeId || 'anonymous',
677
- sender_name: accountId,
678
- sender_type: 'node',
679
- content: text,
680
- created_at: new Date().toISOString(),
681
- auth_token: robot.config.secretKey
682
- };
683
-
684
- await robot.js.publish(replySubject, new TextEncoder().encode(JSON.stringify(chatPayload)));
685
- logger.info(`[pier-connector][${accountId}] 💬 Agent reply published to NATS for job ${jobId} (Subject: ${replySubject})`);
686
- }
687
- } catch (err) {
688
- logger.error(`[pier-connector][${accountId}] Failed to publish reply: ${err.message}`);
689
- }
690
-
691
- // Handle listener timeout
692
- if (robot.jobSubscriptions.has(jobId)) {
693
- if (robot.jobStopTimeouts.has(jobId)) clearTimeout(robot.jobStopTimeouts.get(jobId));
694
-
695
- const timeout = setTimeout(() => {
696
- const sub = robot.jobSubscriptions.get(jobId);
697
- if (sub) {
698
- logger.info(`[pier-connector][${accountId}] \u{1F6D1} Stopping listener for job ${jobId} (inactivity)`);
699
- sub.stop();
700
- robot.jobSubscriptions.delete(jobId);
701
- robot.jobStopTimeouts.delete(jobId);
702
- }
703
- }, 3600000); // 1 hour
704
- robot.jobStopTimeouts.set(jobId, timeout);
705
- }
706
- }
707
-
708
- return { ok: true };
709
- },
710
- },
711
-
712
- status: {
713
- defaultRuntime: {
714
- accountId: 'default',
715
- running: false,
716
- connected: false,
717
- },
718
- buildAccountSnapshot: ({ account }) => {
719
- const robot = instances.get(account.accountId);
720
- return {
721
- accountId: account.accountId,
722
- name: account.name || `Robot ${account.accountId}`,
723
- enabled: account.enabled,
724
- configured: Boolean(account.nodeId && account.secretKey) || Boolean(account.privateKey),
725
- running: robot ? (robot.connectionStatus === 'connected' || robot.connectionStatus === 'connecting') : false,
726
- connected: robot ? (robot.connectionStatus === 'connected' && !robot.lastHeartbeatError) : false,
727
- lastError: robot?.lastHeartbeatError || null,
728
- };
729
- }
730
- },
731
- };
732
-
733
- api.registerChannel({
734
- id: 'pier',
735
- plugin: pierChannel,
736
- login: {
737
- handler: async () => {
738
- console.log('\n🚢 \x1b[1m\x1b[36mPier Channel Login\x1b[0m');
739
-
740
- const answers = await inquirer.prompt([
741
- {
742
- type: 'input',
743
- name: 'accountId',
744
- message: 'Account Name (e.g., sunwukong, jolin):',
745
- default: 'default',
746
- validate: (input) => input.trim().length > 0 || 'Account name is required'
747
- },
748
- {
749
- type: 'input',
750
- name: 'pierApiUrl',
751
- message: 'Pier API URL:',
752
- default: DEFAULTS.PIER_API_URL
753
- },
754
- {
755
- type: 'input',
756
- name: 'nodeId',
757
- message: 'Bot Node ID (UUID):',
758
- validate: (input) => input.trim().length > 0 || 'Node ID is required'
759
- },
760
- {
761
- type: 'password',
762
- name: 'secretKey',
763
- message: 'Bot Secret Key:',
764
- validate: (input) => input.trim().length > 0 || 'Secret Key is required'
765
- },
766
- ]);
767
-
768
- console.log('\n\x1b[36mVerifying connection...\x1b[0m');
769
- try {
770
- const tempClient = new PierClient({ apiUrl: answers.pierApiUrl });
771
- await tempClient.heartbeat(answers.nodeId, answers.secretKey);
772
-
773
- console.log('\x1b[32m✔ Verified successfully!\x1b[0m');
774
-
775
- return {
776
- accountId: answers.accountId,
777
- config: {
778
- enabled: true,
779
- pierApiUrl: answers.pierApiUrl,
780
- nodeId: answers.nodeId,
781
- secretKey: answers.secretKey
782
- }
783
- };
784
- } catch (err) {
785
- throw new Error(`Failed to verify Pier credentials: ${err.message}`);
786
- }
787
- }
788
- }
789
- });
790
-
791
- // ── 2. Register background service ─────────────────────────────────
792
-
793
- api.registerService({
794
- id: 'pier-connector',
795
-
796
- start: async () => {
797
- const configs = resolveConfigs();
798
- logger.info(`[pier-connector] 🚀 Starting background service for ${configs.length} robots …`);
799
-
800
- for (const config of configs) {
801
- try {
802
- const robot = new PierRobot(config);
803
- instances.set(config.accountId, robot);
804
- await robot.start();
805
- } catch (err) {
806
- logger.error(`[pier-connector] ✖ Failed to start robot ${config.accountId}: ${err.message}`);
807
- }
808
- }
809
- },
810
-
811
- stop: async () => {
812
- logger.info('[pier-connector] 🛑 Stopping all robots …');
813
- for (const [id, robot] of instances) {
814
- try {
815
- await robot.stop();
816
- } catch (err) {
817
- logger.error(`[pier-connector] ✖ Error stopping robot ${id}: ${err.message}`);
818
- }
819
- }
820
- instances.clear();
821
- logger.info('[pier-connector] ✔ All robots stopped');
822
- },
823
- });
824
-
825
- // ── 3. Register pier_publish tool ──────────────────────────────────
826
-
827
- api.registerTool(
828
- {
829
- name: 'pier_publish',
830
- description: 'Publish a task to the Pier job marketplace.',
831
- parameters: {
832
- type: 'object',
833
- properties: {
834
- task: { type: 'string', description: 'The task description' },
835
- accountId: { type: 'string', description: 'Account to use (default: "default")' },
836
- meta: { type: 'object' },
837
- timeoutMs: { type: 'number' },
838
- },
839
- required: ['task'],
840
- },
841
-
842
- async execute(_id, params) {
843
- const accountId = params.accountId || 'default';
844
- const robot = instances.get(accountId) || instances.values().next().value;
845
- if (!robot || robot.connectionStatus !== 'connected') {
846
- return { content: [{ type: 'text', text: JSON.stringify({ ok: false, error: 'Robot not connected' }) }] };
847
- }
848
-
849
- const timeout = params.timeoutMs || 60000;
850
- const taskPayload = createRequestPayload({ task: params.task, timeoutMs: timeout, meta: params.meta });
851
-
852
- try {
853
- const reply = await robot.nc.request(robot.config.publishSubject, new TextEncoder().encode(JSON.stringify(taskPayload)), { timeout });
854
- const resultData = JSON.parse(new TextDecoder().decode(reply.data));
855
- return { content: [{ type: 'text', text: JSON.stringify({ ok: true, taskId: taskPayload.id, result: resultData }) }] };
856
- } catch (err) {
857
- return { content: [{ type: 'text', text: JSON.stringify({ ok: false, error: err.message }) }] };
858
- }
859
- },
860
- },
861
- { optional: true },
862
- );
863
-
864
- api.registerTool(
865
- {
866
- name: 'pier_chat',
867
- description: 'Send a message to the employer regarding a specific job.',
868
- parameters: {
869
- type: 'object',
870
- properties: {
871
- jobId: { type: 'string' },
872
- text: { type: 'string' },
873
- accountId: { type: 'string' }
874
- },
875
- required: ['jobId', 'text']
876
- },
877
- async execute(_id, params, ctx) {
878
- const accountId = params.accountId || 'default';
879
- logger.info(`[pier-connector] 🛠️ Tool called: pier_chat | jobId=${params.jobId} | accountId=${accountId}`);
880
- const robot = instances.get(accountId) || instances.values().next().value;
881
- if (!robot || robot.connectionStatus !== 'connected') {
882
- return { content: [{ type: 'text', text: 'Error: Robot not connected' }] };
883
- }
884
-
885
- try {
886
- const subject = `chat.${params.jobId}`;
887
- let metadata = robot.activeNodeJobs.get(params.jobId);
888
-
889
- if (!metadata && ctx.to) {
890
- const toId = ctx.to.replace(/^pier:/, '');
891
- metadata = robot.activeNodeJobs.get(toId);
892
- }
893
-
894
- const jobId = metadata?.pierJobId || params.jobId;
895
-
896
- const payload = {
897
- id: crypto.randomUUID ? crypto.randomUUID() : (Math.random().toString(36).substring(2)),
898
- job_id: jobId,
899
- sender_id: robot.config.nodeId,
900
- sender_name: accountId,
901
- sender_type: 'node',
902
- content: params.text,
903
- created_at: new Date().toISOString(),
904
- auth_token: robot.config.secretKey
905
- };
906
-
907
- await robot.js.publish(subject, new TextEncoder().encode(JSON.stringify(payload)));
908
- return { content: [{ type: 'text', text: 'Message sent' }] };
909
- } catch (err) {
910
- return { content: [{ type: 'text', text: `Error: ${err.message}` }] };
911
- }
912
- }
913
- },
914
- { optional: true }
915
- );
916
-
917
- // ── V2 System Action Tools ────────────────────────────────────────
918
-
919
- const registerSystemActionTool = (name, description, action, extraParams, userRole = 'node') => {
920
- api.registerTool({
921
- name,
922
- description,
923
- parameters: {
924
- type: 'object',
925
- properties: {
926
- jobId: { type: 'string', description: 'The ID of the job/chat session' },
927
- accountId: { type: 'string' },
928
- ...extraParams
929
- },
930
- required: ['jobId', ...Object.keys(extraParams)]
931
- },
932
- async execute(_id, params) {
933
- const accountId = params.accountId || 'default';
934
- const robot = instances.get(accountId) || instances.values().next().value;
935
- if (!robot || robot.connectionStatus !== 'connected') {
936
- return { content: [{ type: 'text', text: 'Error: Robot not connected' }] };
937
- }
938
-
939
- try {
940
- const subject = `chat.${params.jobId}`;
941
- const { jobId, accountId: _, ...payload } = params;
942
-
943
- const msgData = {
944
- id: crypto.randomUUID ? crypto.randomUUID() : (Math.random().toString(36).substring(2)),
945
- job_id: params.jobId,
946
- sender_id: userRole === 'user' ? 'user_' + robot.config.nodeId : robot.config.nodeId,
947
- sender_type: userRole,
948
- content: JSON.stringify({ type: 'system_action', action, payload }),
949
- created_at: new Date().toISOString(),
950
- auth_token: robot.config.secretKey,
951
- type: 'system_action',
952
- action: action
953
- };
954
-
955
- await robot.js.publish(subject, new TextEncoder().encode(JSON.stringify(msgData)));
956
- return { content: [{ type: 'text', text: `${action} executed successfully` }] };
957
- } catch (err) {
958
- return { content: [{ type: 'text', text: `Error: ${err.message}` }] };
959
- }
960
- }
961
- }, { optional: true });
962
- };
963
-
964
- // For Node (Worker)
965
- registerSystemActionTool('pier_bid_task', 'Bid on an open marketplace task to offer your services to the employer', 'task_bid', { message: { type: 'string', description: 'Your pitch explaining why you are a good fit for this task' } });
966
- registerSystemActionTool('pier_accept_task', 'Accept an offered task from the employer in the current chat', 'task_accept', {});
967
- registerSystemActionTool('pier_finish_task', 'Submit the final result for a task', 'task_submit', { result: { type: 'string', description: 'The final result or file links' } });
968
-
969
- // For User (Employer Robot) - Spoofed identity using Node's config for A2A testing
970
- registerSystemActionTool('pier_propose_task', 'Offer a task to a node with a specific price', 'task_offer', {
971
- price: { type: 'number', description: 'The price in PIER tokens to offer' },
972
- description: { type: 'string', description: 'The formal task description' }
973
- }, 'user');
974
-
975
- registerSystemActionTool('pier_rate_task', 'Rate the node after completion', 'task_rate', {
976
- score: { type: 'number', description: 'Rating from 1 to 5' },
977
- comment: { type: 'string', description: 'A short review' }
978
- }, 'user');
979
-
980
- registerSystemActionTool('pier_reject_task', 'Reject an offered task from the employer in the current chat', 'task_reject', {
981
- reason: { type: 'string', description: 'Reason for rejection' }
982
- });
983
-
984
- registerSystemActionTool('pier_fail_task', 'Report that the task has failed during execution', 'task_error', {
985
- error: { type: 'string', description: 'Description of the error' }
986
- });
987
-
988
- registerSystemActionTool('pier_cancel_task', 'Cancel a previously proposed task', 'task_cancel', {
989
- reason: { type: 'string', description: 'Reason for cancellation' }
990
- }, 'user');
991
-
992
- api.registerTool({
993
- name: 'pier_get_profile',
994
- description: 'Get the current Pier profile, balance, and node stats.',
995
- parameters: {
996
- type: 'object',
997
- properties: {
998
- accountId: { type: 'string' }
999
- }
1000
- },
1001
- async execute(_id, params) {
1002
- const accountId = params.accountId || 'default';
1003
- const robot = instances.get(accountId) || instances.values().next().value;
1004
- if (!robot) return { content: [{ type: 'text', text: 'Error: Robot not found' }] };
1005
-
1006
- try {
1007
- const profile = await robot.client.getUserProfile(robot.config.secretKey);
1008
- return { content: [{ type: 'text', text: JSON.stringify(profile, null, 2) }] };
1009
- } catch (err) {
1010
- return { content: [{ type: 'text', text: `Error: ${err.message}` }] };
1011
- }
1012
- }
1013
- }, { optional: true });
1014
-
1015
- // ── 4. Register /pier status command ───────────────────────────────
1016
-
1017
- api.registerCommand({
1018
- name: 'pier',
1019
- description: 'Show Pier connector status',
1020
- handler: () => {
1021
- if (instances.size === 0) return { text: 'No Pier robots active.' };
1022
-
1023
- const lines = ['**Pier Connector Status**'];
1024
- for (const [id, robot] of instances) {
1025
- const uptime = robot.connectedAt
1026
- ? `${Math.round((Date.now() - robot.connectedAt.getTime()) / 1000)}s`
1027
- : 'N/A';
1028
-
1029
- lines.push(`\n**Account: ${id}**`);
1030
- lines.push(`• Connection: ${robot.connectionStatus}`);
1031
- lines.push(`• Node ID: ${robot.config.nodeId || 'N/A'}`);
1032
- lines.push(`• Uptime: ${uptime}`);
1033
- lines.push(`• Jobs: ${robot.stats.received} received, ${robot.stats.completed} completed, ${robot.stats.failed} failed`);
1034
- if (robot.lastHeartbeatError) {
1035
- lines.push(`• \x1b[31mHeartbeat Error: ${robot.lastHeartbeatError}\x1b[0m`);
1036
- } else {
1037
- lines.push(`• Heartbeat: OK`);
1038
- }
1039
- }
1040
-
1041
- lines.push(`\n**Global Stats:** ${totalJobsReceived} total jobs received`);
1042
-
1043
- return { text: lines.join('\n') };
1044
- },
1045
- });
1046
-
1047
- // ── 6. Register CLI Setup Command ──────────────────────────────────
1048
-
1049
- api.registerCli(
1050
- ({ program }) => {
1051
- // Find or create the 'pier' command namespace to avoid root conflicts
1052
- let pier = program.commands.find(c => c.name() === 'pier');
1053
- if (!pier) {
1054
- pier = program.command('pier').description('Pier connector commands');
1055
- }
1056
-
1057
- pier
1058
- .command('setup')
1059
- .description('Interactively configure the Pier connector settings')
1060
- .action(async () => {
1061
- const currentConfig = resolveConfig();
1062
-
1063
- console.log('\n🚢 \x1b[1m\x1b[36mPier Connector Setup (V1.1)\x1b[0m');
1064
- console.log('You can register manually or let the Auto-Host (Advanced) feature do it for you using a Wallet Private Key.\n');
1065
-
1066
- let setupMethod = 'manual';
1067
- try {
1068
- const answer = await inquirer.prompt([
1069
- {
1070
- type: 'list',
1071
- name: 'method',
1072
- message: 'How would you like to configure your node credentials?',
1073
- choices: [
1074
- { name: 'Manual Entry (Recommended) - I have a Node ID and Secret Key', value: 'manual' },
1075
- { name: 'Auto-Host (Advanced) - Register automatically using my Wallet Private Key', value: 'auto' }
1076
- ],
1077
- default: 'manual'
1078
- }
1079
- ]);
1080
- if (answer.method) {
1081
- setupMethod = answer.method;
1082
- }
1083
- } catch (err) {
1084
- console.log('\n\x1b[33mFalling back to Manual Entry mode due to terminal configuration.\x1b[0m');
1085
- setupMethod = 'manual';
1086
- }
1087
-
1088
- let finalNodeId = currentConfig.nodeId;
1089
- let finalSecretKey = currentConfig.secretKey;
1090
- let finalPrivateKey = currentConfig.privateKey;
1091
- let finalWallet = currentConfig.walletAddress;
1092
-
1093
- if (setupMethod === 'manual') {
1094
- const manualAnswers = await inquirer.prompt([
1095
- { type: 'input', name: 'pierApiUrl', message: 'Pier API URL:', default: currentConfig.pierApiUrl },
1096
- { type: 'input', name: 'nodeId', message: 'Bot Node ID (UUID):', default: finalNodeId },
1097
- { type: 'password', name: 'secretKey', message: 'Bot Secret Key:', default: finalSecretKey },
1098
- { type: 'input', name: 'walletAddress', message: 'Your Wallet Address (for payout):', default: finalWallet },
1099
- { type: 'input', name: 'capabilities', message: 'Capabilities (comma-separated):', default: (currentConfig.capabilities || []).join(', ') }
1100
- ]);
1101
- finalNodeId = manualAnswers.nodeId;
1102
- finalSecretKey = manualAnswers.secretKey;
1103
- finalWallet = manualAnswers.walletAddress;
1104
- currentConfig.capabilities = manualAnswers.capabilities ? manualAnswers.capabilities.split(',').map(s => s.trim()) : currentConfig.capabilities;
1105
- currentConfig.pierApiUrl = manualAnswers.pierApiUrl;
1106
- } else if (setupMethod === 'auto') {
1107
- const autoAnswers = await inquirer.prompt([
1108
- { type: 'input', name: 'pierApiUrl', message: 'Pier API URL:', default: currentConfig.pierApiUrl },
1109
- { type: 'password', name: 'privateKey', message: 'Your Wallet Private Key (Hex, 0x...):', default: finalPrivateKey },
1110
- { type: 'input', name: 'capabilities', message: 'Capabilities (comma-separated):', default: (currentConfig.capabilities || []).join(', ') }
1111
- ]);
1112
- console.log('\n\x1b[36mRegistering your node...\x1b[0m');
1113
- try {
1114
- const tempClient = new PierClient({ apiUrl: autoAnswers.pierApiUrl });
1115
- const hostName = api?.getRuntimeInfo?.()?.hostname ?? 'Auto-Node';
1116
-
1117
- const { nodeId, secretKey, walletAddress } = await tempClient.autoRegister(autoAnswers.privateKey, hostName);
1118
-
1119
- finalWallet = finalWallet || walletAddress;
1120
- finalPrivateKey = autoAnswers.privateKey;
1121
- currentConfig.pierApiUrl = autoAnswers.pierApiUrl;
1122
- currentConfig.capabilities = autoAnswers.capabilities ? autoAnswers.capabilities.split(',').map(s => s.trim()) : currentConfig.capabilities;
1123
- finalNodeId = nodeId;
1124
- finalSecretKey = secretKey;
1125
-
1126
- console.log(`\x1b[32m✔ Node registered automatically! Node ID: ${nodeId}\x1b[0m`);
1127
- } catch (err) {
1128
- console.error(`\x1b[31m✖ Failed to auto-register: ${err.message}\x1b[0m`);
1129
- return; // Stop setup
1130
- }
1131
- }
1132
-
1133
- // ── Select Agent Binding ──
1134
- let finalAgentId = currentConfig.agentId;
1135
- try {
1136
- let agentsInfo = [];
1137
- if (api.runtime && typeof api.runtime.getAgents === 'function') {
1138
- const agentsMap = await api.runtime.getAgents();
1139
- if (agentsMap) {
1140
- agentsInfo = Object.entries(agentsMap).map(([id, info]) => ({
1141
- name: `${info.name || 'Unnamed Agent'} (${id})`,
1142
- value: id
1143
- }));
1144
- }
1145
- }
1146
-
1147
- if (agentsInfo.length > 0) {
1148
- agentsInfo.unshift({ name: 'None (Default Routing)', value: '' });
1149
- const agentAnswer = await inquirer.prompt([
1150
- {
1151
- type: 'list',
1152
- name: 'agentId',
1153
- message: 'Select an Agent to bind for processing these tasks:',
1154
- choices: agentsInfo,
1155
- default: finalAgentId || ''
1156
- }
1157
- ]);
1158
- finalAgentId = agentAnswer.agentId;
1159
- }
1160
- } catch (err) {
1161
- logger.warn(`Could not load agents list: ${err.message}`);
1162
- }
1163
-
1164
- // Determine how to save.
1165
- console.log('\n✅ \x1b[32mConfiguration Captured!\x1b[0m\n');
1166
-
1167
- const newConfigBlock = {
1168
- pierApiUrl: currentConfig.pierApiUrl,
1169
- nodeId: finalNodeId,
1170
- secretKey: finalSecretKey,
1171
- walletAddress: finalWallet,
1172
- agentId: finalAgentId,
1173
- capabilities: currentConfig.capabilities
1174
- };
1175
-
1176
- if (setupMethod === 'auto') {
1177
- newConfigBlock.privateKey = finalPrivateKey;
1178
- console.log('\x1b[33mNote: Private Key is included for future automatic reconnects.\x1b[0m');
1179
- }
1180
-
1181
- // Write to openclaw.json
1182
- try {
1183
- const cwd = process.cwd();
1184
- const configPath = path.join(cwd, 'openclaw.json');
1185
- let existingConfig = {};
1186
- if (fs.existsSync(configPath)) {
1187
- const raw = fs.readFileSync(configPath, 'utf8');
1188
- existingConfig = JSON.parse(raw);
1189
- }
1190
-
1191
- if (!existingConfig.plugins) existingConfig.plugins = {};
1192
- if (!existingConfig.plugins.entries) existingConfig.plugins.entries = {};
1193
- if (!existingConfig.plugins.entries['pier-connector']) {
1194
- existingConfig.plugins.entries['pier-connector'] = { enabled: true };
1195
- }
1196
-
1197
- existingConfig.plugins.entries['pier-connector'].config = {
1198
- ...(existingConfig.plugins.entries['pier-connector'].config || {}),
1199
- ...newConfigBlock
1200
- };
1201
-
1202
- fs.writeFileSync(configPath, JSON.stringify(existingConfig, null, 2), 'utf8');
1203
- console.log(`\x1b[32m✔ Successfully wrote configuration to ${configPath}\x1b[0m`);
1204
- } catch (err) {
1205
- console.error(`\x1b[31m✖ Failed to write to openclaw.json automatically: ${err.message}\x1b[0m`);
1206
- console.log('Please ensure your \x1b[1m\x1b[33mopenclaw.json\x1b[0m includes the following block in plugins.entries:\n');
1207
- console.log(JSON.stringify({
1208
- "pier-connector": {
1209
- enabled: true,
1210
- config: newConfigBlock
1211
- }
1212
- }, null, 2));
1213
- }
1214
-
1215
- console.log('\nRestart OpenClaw (or reload plugins) to apply changes.');
1216
- });
1217
- },
1218
- { commands: ['pier'] }
1219
- );
1220
-
1221
- logger.info('[pier-connector] Plugin registered');
1222
- }