@gholl-studio/pier-connector 0.7.2 → 0.7.3

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.ts CHANGED
@@ -100,8 +100,6 @@ const pierPlugin: ChannelPlugin<PierAccountConfig> = {
100
100
  idLabel: 'pierJobId',
101
101
  normalizeAllowEntry: (entry) => entry.replace(/^(pier|job):/i, ''),
102
102
  notifyApproval: async ({ cfg, id, accountId }) => {
103
- // Signal received when 'openclaw pairing approve pier <id>' is run.
104
- // Useful if we want to trigger specific onboarding or status updates.
105
103
  console.log(`[pier-connector] Pairing approved for Job ${id} on account ${accountId}`);
106
104
  }
107
105
  },
@@ -135,7 +133,6 @@ const pierPlugin: ChannelPlugin<PierAccountConfig> = {
135
133
  };
136
134
 
137
135
  if (isWorkspace) {
138
- // Use SDK-aligned workspace protocol (includes signing if configured)
139
136
  const ns = (ctx as any).metadata?.namespace || 'default';
140
137
  let swarmPayload: any = {
141
138
  ...payload,
@@ -149,7 +146,6 @@ const pierPlugin: ChannelPlugin<PierAccountConfig> = {
149
146
  }
150
147
  await robot.client.nats.publishGroupChat(jobId, swarmPayload, ns);
151
148
  } else {
152
- // Standard Marketplace Job Chat
153
149
  const subject = `chat.${jobId}`;
154
150
  await robot.js.publish(subject, new TextEncoder().encode(JSON.stringify(payload)));
155
151
  }
@@ -189,9 +185,7 @@ const pierPlugin: ChannelPlugin<PierAccountConfig> = {
189
185
  gateway: {
190
186
  startAccount: async (ctx) => {
191
187
  const config = ctx.account;
192
- // Use ctx as the runtime provider as it contains .channelRuntime, .log, etc.
193
188
  const robot = new PierRobot(config, ctx as any, async (inbound, jobId) => {
194
- // Pass the context containing channelRuntime
195
189
  await handleInbound(ctx, inbound, jobId, robot, pierPlugin);
196
190
  });
197
191
  instances.set(ctx.accountId, robot);
@@ -207,13 +201,10 @@ const pierPlugin: ChannelPlugin<PierAccountConfig> = {
207
201
  lastStartAt: (robot as any).lastStartAt
208
202
  } as any);
209
203
 
210
- // IMPORTANT: Keep startAccount promise active for the lifetime of the connection.
211
- // If this promise resolves, the Gateway will auto-restart the account.
212
204
  await new Promise<void>((resolve) => {
213
205
  ctx.abortSignal.addEventListener('abort', () => {
214
206
  resolve();
215
207
  }, { once: true });
216
-
217
208
  if (ctx.abortSignal.aborted) resolve();
218
209
  });
219
210
 
@@ -248,10 +239,9 @@ const register = (api: PierPluginApi) => {
248
239
  const logger = api.logger;
249
240
  const globalStats = { received: 0, completed: 0, failed: 0 };
250
241
 
251
- // Register our new ChannelPlugin
252
242
  api.registerChannel({ plugin: pierPlugin as any });
253
243
 
254
- // Tools and CLI registration
244
+ // --- Market Tools ---
255
245
  api.registerTool({
256
246
  name: 'pier_publish',
257
247
  label: 'Publish to Pier',
@@ -268,59 +258,59 @@ const register = (api: PierPluginApi) => {
268
258
  const accountId = params.accountId || ctx?.metadata?.accountId || ctx?.accountId || 'default';
269
259
  const robot = instances.get(accountId) || instances.values().next().value;
270
260
  if (!robot || robot.connectionStatus !== 'connected') {
271
- return {
272
- content: [{ type: 'text', text: 'Robot not connected' }],
273
- details: {}
274
- };
261
+ return { content: [{ type: 'text', text: 'Robot not connected' }], details: {} };
275
262
  }
276
-
277
263
  const taskPayload = createRequestPayload({ task: params.task });
278
264
  const reply = await robot.nc!.request(robot.config.publishSubject, new TextEncoder().encode(JSON.stringify(taskPayload)));
279
- return {
280
- content: [{ type: 'text', text: new TextDecoder().decode(reply.data) }],
281
- details: {}
282
- };
265
+ return { content: [{ type: 'text', text: new TextDecoder().decode(reply.data) }], details: {} };
283
266
  }
284
267
  }, { optional: true });
285
268
 
286
- api.registerTool(
287
- {
288
- name: 'pier_chat',
289
- label: 'Pier Chat',
290
- description: 'Send a message to the employer regarding a specific job.',
291
- parameters: {
292
- type: 'object',
293
- properties: {
294
- jobId: { type: 'string' },
295
- text: { type: 'string' },
296
- accountId: { type: 'string' }
297
- },
298
- required: ['jobId', 'text']
269
+ api.registerTool({
270
+ name: 'pier_chat',
271
+ label: 'Pier Chat',
272
+ description: 'Send a message in a specific job chat or workspace group.',
273
+ parameters: {
274
+ type: 'object',
275
+ properties: {
276
+ jobId: { type: 'string' },
277
+ text: { type: 'string' },
278
+ accountId: { type: 'string' }
299
279
  },
300
- async execute(_id, params, ctx: any) {
301
- const accountId = params.accountId || ctx?.metadata?.accountId || ctx?.accountId || 'default';
302
- const robot = instances.get(accountId) || instances.values().next().value;
303
- if (!robot || robot.connectionStatus !== 'connected') {
304
- return { content: [{ type: 'text', text: 'Error: Robot not connected' }], details: {} };
305
- }
280
+ required: ['jobId', 'text']
281
+ },
282
+ async execute(_id, params, ctx: any) {
283
+ const accountId = params.accountId || ctx?.metadata?.accountId || ctx?.accountId || 'default';
284
+ const robot = instances.get(accountId) || instances.values().next().value;
285
+ if (!robot || robot.connectionStatus !== 'connected') {
286
+ return { content: [{ type: 'text', text: 'Error: Robot not connected' }], details: {} };
287
+ }
306
288
 
307
- try {
308
- const subject = `chat.${params.jobId}`;
309
- let metadata = robot.activeNodeJobs.get(params.jobId);
310
-
311
- if (!metadata && ctx.to) {
312
- const toId = ctx.to.replace(/^pier:/, '');
313
- metadata = robot.activeNodeJobs.get(toId);
314
- }
315
-
316
- const jobId = metadata?.pierJobId || params.jobId;
317
- const isWorkspace = metadata?.workspace === true;
289
+ try {
290
+ let metadata = robot.activeNodeJobs.get(params.jobId);
291
+ const jobId = metadata?.pierJobId || params.jobId;
292
+ const isWorkspace = metadata?.workspace === true || jobId.startsWith('job-');
318
293
 
319
- if (!robot.js) {
320
- return { content: [{ type: 'text', text: 'Error: JetStream not available' }], details: {} };
321
- }
294
+ if (!robot.js) return { content: [{ type: 'text', text: 'Error: JetStream not available' }], details: {} };
322
295
 
296
+ if (isWorkspace) {
323
297
  const payload: any = {
298
+ id: (globalThis.crypto as any).randomUUID ? (globalThis.crypto as any).randomUUID() : (Math.random().toString(36).substring(2)),
299
+ groupId: jobId,
300
+ senderId: robot.accountId,
301
+ content: params.text,
302
+ timestamp: Date.now(),
303
+ mentions: [],
304
+ version: 'psp-1.0'
305
+ };
306
+ if (robot.config.privateKey) {
307
+ const signed = await PierCrypto.signSwarmMessage(payload, robot.config.privateKey);
308
+ await robot.client.nats.publishGroupChat(jobId, signed);
309
+ } else {
310
+ await robot.client.nats.publishGroupChat(jobId, payload);
311
+ }
312
+ } else {
313
+ const payload = {
324
314
  id: (globalThis.crypto as any).randomUUID ? (globalThis.crypto as any).randomUUID() : (Math.random().toString(36).substring(2)),
325
315
  job_id: jobId,
326
316
  sender_id: robot.config.nodeId,
@@ -331,429 +321,132 @@ const register = (api: PierPluginApi) => {
331
321
  auth_token: robot.config.secretKey,
332
322
  timestamp: Date.now()
333
323
  };
334
-
335
- if (isWorkspace) {
336
- let swarmPayload: any = {
337
- ...payload,
338
- version: 'psp-1.0',
339
- groupId: jobId,
340
- senderId: robot.accountId,
341
- };
342
- if (robot.config.privateKey) {
343
- swarmPayload = await PierCrypto.signSwarmMessage(swarmPayload, robot.config.privateKey);
344
- }
345
- await robot.client.nats.publishGroupChat(jobId, swarmPayload);
346
- } else {
347
- const subject = `chat.${jobId}`;
348
- await robot.js.publish(subject, new TextEncoder().encode(JSON.stringify(payload)));
349
- }
350
- return { content: [{ type: 'text', text: 'Message sent' }], details: {} };
351
- } catch (err: any) {
352
- return { content: [{ type: 'text', text: `Error: ${err.message}` }], details: {} };
324
+ await robot.js.publish(`chat.${jobId}`, new TextEncoder().encode(JSON.stringify(payload)));
353
325
  }
326
+ return { content: [{ type: 'text', text: 'Message sent' }], details: {} };
327
+ } catch (err: any) {
328
+ return { content: [{ type: 'text', text: `Error: ${err.message}` }], details: {} };
354
329
  }
355
- },
356
- { optional: true }
357
- );
330
+ }
331
+ }, { optional: true });
358
332
 
359
- // Helper to register marketplace action tools
360
- const registerSystemActionTool = (name: string, label: string, description: string, action: string, extraParams: any, userRole = 'node') => {
333
+ // Helper for marketplace system actions
334
+ const regSystemAction = (name: string, action: string, extraParams: any = {}) => {
361
335
  api.registerTool({
362
336
  name,
363
- label,
364
- description,
337
+ label: name,
338
+ description: `Execute ${action} on a Pier task.`,
365
339
  parameters: {
366
340
  type: 'object',
367
- properties: {
368
- jobId: { type: 'string', description: 'The ID of the job' },
369
- accountId: { type: 'string' },
370
- ...extraParams
371
- },
341
+ properties: { jobId: { type: 'string' }, ...extraParams },
372
342
  required: ['jobId', ...Object.keys(extraParams)]
373
343
  },
374
344
  async execute(_id, params, ctx: any) {
375
- const accountId = params.accountId || ctx?.metadata?.accountId || ctx?.accountId || 'default';
345
+ const accountId = ctx?.metadata?.accountId || ctx?.accountId || 'default';
376
346
  const robot = instances.get(accountId) || instances.values().next().value;
377
- if (!robot || robot.connectionStatus !== 'connected') {
378
- return { content: [{ type: 'text', text: 'Error: Robot not connected' }], details: {} };
379
- }
380
-
347
+ if (!robot?.js) return { content: [{ type: 'text', text: 'Error: Robot or JetStream unavailable' }], details: {} };
381
348
  try {
382
- const { jobId: j, accountId: _, ...p } = params;
383
-
384
- if (!robot.js) {
385
- return { content: [{ type: 'text', text: 'Error: JetStream not available' }], details: {} };
386
- }
387
-
388
349
  const msgData = {
389
350
  id: (globalThis.crypto as any).randomUUID ? (globalThis.crypto as any).randomUUID() : (Math.random().toString(36).substring(2)),
390
351
  job_id: params.jobId,
391
- sender_id: userRole === 'user' ? 'user_' + robot.config.nodeId : robot.config.nodeId,
392
- sender_type: userRole,
393
- content: JSON.stringify({ type: 'system_action', action, payload: p }),
352
+ sender_id: robot.config.nodeId,
353
+ content: JSON.stringify({ type: 'system_action', action, payload: params }),
394
354
  created_at: new Date().toISOString(),
395
- auth_token: robot.config.secretKey,
396
355
  type: 'system_action',
397
- action: action
356
+ action
398
357
  };
399
-
400
358
  await robot.js.publish(`chat.${params.jobId}`, new TextEncoder().encode(JSON.stringify(msgData)));
401
- return { content: [{ type: 'text', text: `${action} executed successfully` }], details: {} };
402
- } catch (err: any) {
403
- return { content: [{ type: 'text', text: `Error: ${err.message}` }], details: {} };
404
- }
359
+ return { content: [{ type: 'text', text: 'Executed' }], details: {} };
360
+ } catch (e: any) { return { content: [{ type: 'text', text: e.message }], details: {} }; }
405
361
  }
406
362
  }, { optional: true });
407
363
  };
408
364
 
409
- registerSystemActionTool('pier_bid_task', 'Bid on task', 'Bid on an marketplace task', 'task_bid', { message: { type: 'string', description: 'Your pitch' } });
410
- registerSystemActionTool('pier_accept_task', 'Accept task', 'Accept offered task', 'task_accept', {});
411
- registerSystemActionTool('pier_finish_task', 'Finish task', 'Submit final result', 'task_submit', { result: { type: 'string', description: 'Final result' } });
412
- registerSystemActionTool('pier_propose_task', 'Offer task', 'Offer task to a node', 'task_offer', {
413
- price: { type: 'number' },
414
- description: { type: 'string' }
415
- }, 'user');
416
- registerSystemActionTool('pier_rate_task', 'Rate task', 'Rate the node', 'task_rate', {
417
- score: { type: 'number' },
418
- comment: { type: 'string' }
419
- }, 'user');
420
- registerSystemActionTool('pier_reject_task', 'Reject task', 'Reject task', 'task_reject', { reason: { type: 'string' } });
421
- registerSystemActionTool('pier_fail_task', 'Report error', 'Report that the task has failed', 'task_error', { error: { type: 'string' } });
422
- registerSystemActionTool('pier_cancel_task', 'Cancel task', 'Cancel the task', 'task_cancel', { reason: { type: 'string' } }, 'user');
423
-
424
- api.registerTool({
425
- name: 'pier_get_profile',
426
- label: 'Pier Profile',
427
- description: 'Get current Pier profile and node stats.',
428
- parameters: {
429
- type: 'object',
430
- properties: { accountId: { type: 'string' } }
431
- },
432
- async execute(_id, params, ctx: any) {
433
- const accountId = params.accountId || ctx?.metadata?.accountId || ctx?.accountId || 'default';
434
- const robot = instances.get(accountId) || instances.values().next().value;
435
- if (!robot) return { content: [{ type: 'text', text: 'Error: Robot not found' }], details: {} };
436
- try {
437
- const profile = await robot.client.getUserProfile(robot.config.secretKey);
438
- return { content: [{ type: 'text', text: JSON.stringify(profile, null, 2) }], details: {} };
439
- } catch (err: any) {
440
- return { content: [{ type: 'text', text: `Error: ${err.message}` }], details: {} };
441
- }
442
- }
443
- }, { optional: true });
444
-
445
- // Multi-Agent Workspace Tools
446
- api.registerTool({
447
- name: 'pier_create_workspace',
448
- label: 'Create Workspace',
449
- description: 'Creates a decentralized multi-agent workspace (group chat) and initializes its whiteboard.',
450
- parameters: {
451
- type: 'object',
452
- properties: {
453
- namespace: { type: 'string', description: 'Federation namespace. Default to "default" if omitted.' }
454
- }
455
- },
456
- async execute(_id, params, ctx: any) {
457
- const accountId = params.accountId || ctx?.metadata?.accountId || ctx?.accountId || 'default';
458
- const robot = instances.get(accountId) || instances.values().next().value;
459
- if (!robot) return { content: [{ type: 'text', text: 'Error: Robot not found' }], details: {} };
460
-
461
- try {
462
- const groupId = (globalThis.crypto as any).randomUUID ? (globalThis.crypto as any).randomUUID() : (Math.random().toString(36).substring(2));
463
- const kv = await robot.client.nats.getWorkspaceKV(groupId);
464
- await kv.put('status', new TextEncoder().encode('Active Workspace Created'));
465
- await kv.put('budget', new TextEncoder().encode('100'));
466
- return { content: [{ type: 'text', text: `Workspace created successfully. Group ID: ${groupId}. Initial budget set to 100.` }], details: { groupId } };
467
- } catch (err: any) {
468
- return { content: [{ type: 'text', text: `Error: ${err.message}` }], details: {} };
469
- }
470
- }
471
- }, { optional: true });
365
+ regSystemAction('pier_bid_task', 'task_bid', { message: { type: 'string' } });
366
+ regSystemAction('pier_accept_task', 'task_accept');
367
+ regSystemAction('pier_finish_task', 'task_submit', { result: { type: 'string' } });
368
+ regSystemAction('pier_reject_task', 'task_reject', { reason: { type: 'string' } });
369
+ regSystemAction('pier_fail_task', 'task_error', { error: { type: 'string' } });
472
370
 
371
+ // --- Strategy Board (Whiteboard) Tools ---
473
372
  api.registerTool({
474
373
  name: STRATEGY_BOARD_TOOLS.LIST_KEYS,
475
374
  label: 'List Board Keys',
476
- description: 'List all available strategic keys in the workspace whiteboard.',
375
+ description: 'List all consensus keys in the workspace whiteboard.',
477
376
  parameters: {
478
377
  type: 'object',
479
- properties: {
480
- groupId: { type: 'string', description: 'The Workspace Group ID' }
481
- },
378
+ properties: { groupId: { type: 'string' } },
482
379
  required: ['groupId']
483
380
  },
484
381
  async execute(_id, params, ctx: any) {
485
- const robot = instances.get(params.accountId || ctx?.metadata?.accountId || ctx?.accountId || 'default') || instances.values().next().value;
486
- if (!robot) return { content: [{ type: 'text', text: 'Error: Robot not found' }], details: {} };
382
+ const robot = instances.get(ctx?.metadata?.accountId || ctx?.accountId || 'default') || instances.values().next().value;
383
+ if (!robot) return { content: [{ type: 'text', text: 'Error' }], details: {} };
487
384
  try {
488
385
  const kv = await robot.client.nats.getWorkspaceKV(params.groupId);
489
386
  const keys = await kv.keys();
490
387
  const list = [];
491
388
  for await (const k of keys) list.push(k);
492
- return { content: [{ type: 'text', text: `Available keys in board:\n${list.join('\n') || '[Board is empty]'}` }], details: { keys: list } };
493
- } catch (err: any) {
494
- return { content: [{ type: 'text', text: `Error or Board Empty: ${err.message}` }], details: {} };
495
- }
389
+ return { content: [{ type: 'text', text: `Board Keys:\n${list.join('\n') || '[Empty]'}` }], details: {} };
390
+ } catch (err: any) { return { content: [{ type: 'text', text: err.message }], details: {} }; }
496
391
  }
497
392
  }, { optional: true });
498
393
 
499
394
  api.registerTool({
500
395
  name: STRATEGY_BOARD_TOOLS.READ_ITEM,
501
396
  label: 'Read Board Item',
502
- description: 'Read a specific strategic item from the whiteboard.',
397
+ description: 'Read a specific strategic agreement from the whiteboard.',
503
398
  parameters: {
504
399
  type: 'object',
505
- properties: {
506
- groupId: { type: 'string', description: 'The Workspace Group ID' },
507
- key: { type: 'string', description: 'The specific key to read' }
508
- },
400
+ properties: { groupId: { type: 'string' }, key: { type: 'string' } },
509
401
  required: ['groupId', 'key']
510
402
  },
511
403
  async execute(_id, params, ctx: any) {
512
- const robot = instances.get(params.accountId || ctx?.metadata?.accountId || ctx?.accountId || 'default') || instances.values().next().value;
513
- if (!robot) return { content: [{ type: 'text', text: 'Error: Robot not found' }], details: {} };
404
+ const robot = instances.get(ctx?.metadata?.accountId || ctx?.accountId || 'default') || instances.values().next().value;
405
+ if (!robot) return { content: [{ type: 'text', text: 'Error' }], details: {} };
514
406
  try {
515
407
  const kv = await robot.client.nats.getWorkspaceKV(params.groupId);
516
408
  const entry = await kv.get(params.key);
517
- if (!entry) return { content: [{ type: 'text', text: 'Key not found.' }], details: {} };
518
- const val = new TextDecoder().decode(entry.value);
519
- return { content: [{ type: 'text', text: `--- KEY: ${params.key} (Rev: ${entry.revision}) ---\n${val}` }], details: { revision: entry.revision, value: val } };
520
- } catch (err: any) {
521
- return { content: [{ type: 'text', text: `Error: ${err.message}` }], details: {} };
522
- }
409
+ if (!entry) return { content: [{ type: 'text', text: 'Not found' }], details: {} };
410
+ return { content: [{ type: 'text', text: new TextDecoder().decode(entry.value) }], details: { revision: entry.revision } };
411
+ } catch (err: any) { return { content: [{ type: 'text', text: err.message }], details: {} }; }
523
412
  }
524
413
  }, { optional: true });
525
414
 
526
415
  api.registerTool({
527
416
  name: STRATEGY_BOARD_TOOLS.UPDATE_ITEM,
528
417
  label: 'Update Board Item',
529
- description: 'Update or create an item on the whiteboard. Use this to persist group consensus, API specs, or task progress.',
418
+ description: 'Update or create consensus on the whiteboard (Markdown supported).',
530
419
  parameters: {
531
420
  type: 'object',
532
- properties: {
533
- groupId: { type: 'string', description: 'The Workspace Group ID' },
534
- key: { type: 'string', description: 'The key to update' },
535
- value: { type: 'string', description: 'The new content (Markdown supported)' }
536
- },
421
+ properties: { groupId: { type: 'string' }, key: { type: 'string' }, value: { type: 'string' } },
537
422
  required: ['groupId', 'key', 'value']
538
423
  },
539
- async execute(_id, params, ctx: any) {
540
- const robot = instances.get(params.accountId || ctx?.metadata?.accountId || ctx?.accountId || 'default') || instances.values().next().value;
541
- if (!robot) return { content: [{ type: 'text', text: 'Error: Robot not found' }], details: {} };
542
- try {
543
- const kv = await robot.client.nats.getWorkspaceKV(params.groupId);
544
- await kv.put(params.key, new TextEncoder().encode(params.value));
545
- return { content: [{ type: 'text', text: `Successfully updated board item: ${params.key}` }], details: { key: params.key } };
546
- } catch (err: any) {
547
- return { content: [{ type: 'text', text: `Error: ${err.message}` }], details: {} };
548
- }
549
- }
550
- }, { optional: true });
551
-
552
- api.registerTool({
553
- name: 'pier_update_whiteboard',
554
- label: 'Update Whiteboard',
555
- description: 'Update the shared state on the workspace whiteboard using CAS (Compare-And-Swap). Provide expected_revision from read.',
556
- parameters: {
557
- type: 'object',
558
- properties: {
559
- groupId: { type: 'string' },
560
- key: { type: 'string' },
561
- value: { type: 'string' },
562
- expected_revision: { type: 'number', description: 'The revision number from the last read (use 0 if key not found)' },
563
- namespace: { type: 'string' }
564
- },
565
- required: ['groupId', 'key', 'value', 'expected_revision']
566
- },
567
- async execute(_id, params, ctx: any) {
568
- const robot = instances.get(ctx?.metadata?.accountId || ctx?.accountId || 'default') || instances.values().next().value;
569
- if (!robot) return { content: [{ type: 'text', text: 'Robot not found' }], details: {} };
570
-
571
- try {
572
- const kv = await robot.client.nats.getWorkspaceKV(params.groupId);
573
-
574
- try {
575
- // if expected_revision is 0 it means we expect the key does not exist yet (or we use create)
576
- if (params.expected_revision === 0) {
577
- try {
578
- await kv.create(params.key, new TextEncoder().encode(params.value));
579
- } catch (e: any) {
580
- if (e.message?.includes('wrong last sequence')) throw e; // Pass to conflict handler
581
- await kv.update(params.key, new TextEncoder().encode(params.value), 0); // fallback
582
- }
583
- } else {
584
- await kv.update(params.key, new TextEncoder().encode(params.value), params.expected_revision);
585
- }
586
- return { content: [{ type: 'text', text: `Whiteboard key '${params.key}' updated successfully.` }], details: {} };
587
- } catch (updateErr: any) {
588
- if (updateErr.message && (updateErr.message.includes('wrong last sequence') || updateErr.code === '400' || updateErr.api_error?.err_code === 10071)) {
589
- return { content: [{ type: 'text', text: `ERROR: CAS Conflict - Another agent modified this key while you were working. Please use pier_read_whiteboard to get the latest revision and try again.` }], details: {} };
590
- }
591
- throw updateErr;
592
- }
593
- } catch (err: any) {
594
- return { content: [{ type: 'text', text: `Error: ${err.message}` }], details: {} };
595
- }
596
- }
597
- }, { optional: true });
598
-
599
- api.registerTool({
600
- name: 'pier_group_mention',
601
- label: 'Mention in Workspace',
602
- description: 'Send a message in the workspace group chat explicitly mentioning an agent to wake them up',
603
- parameters: {
604
- type: 'object',
605
- properties: {
606
- groupId: { type: 'string' },
607
- mentions: { type: 'array', items: { type: 'string' }, description: 'Array of target Agent Node IDs' },
608
- content: { type: 'string', description: 'Message content' },
609
- namespace: { type: 'string' }
610
- },
611
- required: ['groupId', 'mentions', 'content']
612
- },
613
- async execute(_id, params, ctx: any) {
614
- const robot = instances.get(ctx?.metadata?.accountId || ctx?.accountId || 'default') || instances.values().next().value;
615
- if (!robot) return { content: [{ type: 'text', text: 'Robot not found' }], details: {} };
616
-
617
- try {
618
- const msgId = (globalThis.crypto as any).randomUUID ? (globalThis.crypto as any).randomUUID() : (Math.random().toString(36).substring(2));
619
- const ns = params.namespace || 'default';
620
-
621
- let payload: any = {
622
- version: 'psp-1.0',
623
- id: msgId,
624
- namespace: ns,
625
- timestamp: Date.now(),
626
- senderId: robot.accountId,
627
- groupId: params.groupId,
628
- content: params.content,
629
- mentions: params.mentions
630
- };
631
-
632
- if (robot.config.privateKey) {
633
- payload = await PierCrypto.signSwarmMessage(payload, robot.config.privateKey);
634
- }
635
-
636
- await robot.client.nats.publishGroupChat(params.groupId, payload, ns);
637
- return { content: [{ type: 'text', text: `Message sent to workspace ${params.groupId} successfully.` }], details: {} };
638
- } catch (err: any) {
639
- return { content: [{ type: 'text', text: `Error: ${err.message}` }], details: {} };
640
- }
641
- }
642
- }, { optional: true });
643
-
644
- api.registerTool({
645
- name: 'pier_search_workers',
646
- label: 'Search Workers',
647
- description: 'Find active agents on the Pier network. When \`capability\` is provided, uses fast Subject-based Discovery (PSP 1.0). Falls back to KV full-scan if omitted.',
648
- parameters: {
649
- type: 'object',
650
- properties: {
651
- capability: { type: 'string', description: '(Recommended) Filter by a specific capability e.g. "translation", "code-execution". Uses fast per-subject discovery when set.' }
652
- }
653
- },
654
424
  async execute(_id, params, ctx: any) {
655
425
  const robot = instances.get(ctx?.metadata?.accountId || ctx?.accountId || 'default') || instances.values().next().value;
656
426
  if (!robot) return { content: [{ type: 'text', text: 'Error' }], details: {} };
657
-
658
- // --- PSP 1.0 Subject-based Discovery (fast path, used when capability is specified) ---
659
- if (params.capability) {
660
- try {
661
- const nodes = await robot.client.nats.queryDiscovery(params.capability);
662
- return { content: [{ type: 'text', text: JSON.stringify(nodes, null, 2) }], details: {} };
663
- } catch (e: any) {
664
- // Fall through to KV scan
665
- }
666
- }
667
-
668
- // --- KV Full-scan (fallback when no capability filter, or subject discovery fails) ---
669
427
  try {
670
- const kv = await robot.client.nats.getWorkspaceKV('active_nodes');
671
- const activeNodes = [];
672
- const iter = await kv.keys();
673
- for await (const k of iter) {
674
- const entry = await kv.get(k);
675
- if (entry) {
676
- try {
677
- // S10: DoS protection
678
- if (entry.value.length > 1024 * 1024) continue;
679
- const nodeData = JSON.parse(new TextDecoder().decode(entry.value));
680
- if (Date.now() - nodeData.timestamp < 120000) {
681
- // --- Zero-Trust: Verify signed heartbeat (PSP 1.0 Hard-Block) ---
682
- if (nodeData.version === 'psp-1.0' && nodeData.signature && nodeData.publicKey) {
683
- const isValid = PierCrypto.verifySwarmMessage(nodeData);
684
- if (!isValid) {
685
- console.warn(`[pier_search_workers] šŸ”“ Dropping node ${k}: invalid signature (spoofed identity).`);
686
- continue;
687
- }
688
- }
689
- activeNodes.push(nodeData);
690
- }
691
- } catch (e) {}
692
- }
693
- }
694
- return { content: [{ type: 'text', text: JSON.stringify(activeNodes.slice(0, 10), null, 2) }], details: {} };
695
- } catch (e: any) {
696
- // Final fallback: central API
697
- try {
698
- const resp = await fetch(`${robot.client.apiUrl}/nodes`);
699
- const nodes = await resp.json();
700
- const active = nodes.filter((n: any) => n.is_online).slice(0, 10);
701
- return { content: [{ type: 'text', text: JSON.stringify(active, null, 2) }], details: {} };
702
- } catch (fallbackErr: any) {
703
- return { content: [{ type: 'text', text: `All discovery methods failed: ${e.message}` }], details: {} };
704
- }
705
- }
428
+ const kv = await robot.client.nats.getWorkspaceKV(params.groupId);
429
+ await kv.put(params.key, new TextEncoder().encode(params.value));
430
+ return { content: [{ type: 'text', text: 'Board updated' }], details: {} };
431
+ } catch (err: any) { return { content: [{ type: 'text', text: err.message }], details: {} }; }
706
432
  }
707
433
  }, { optional: true });
708
434
 
709
-
710
435
  api.registerTool({
711
- name: 'pier_invite_collaborator',
712
- label: 'Invite to Workspace',
713
- description: 'Send a targeted job request to a node to invite them.',
714
- parameters: {
715
- type: 'object',
716
- properties: {
717
- nodeId: { type: 'string' },
718
- groupId: { type: 'string' },
719
- roleDescription: { type: 'string' },
720
- namespace: { type: 'string' }
721
- },
722
- required: ['nodeId', 'groupId', 'roleDescription']
723
- },
436
+ name: 'pier_get_profile',
437
+ label: 'Pier Profile',
438
+ description: 'Get current Pier node stats.',
439
+ parameters: { type: 'object', properties: { accountId: { type: 'string' } } },
724
440
  async execute(_id, params, ctx: any) {
725
- const robot = instances.get(ctx?.metadata?.accountId || ctx?.accountId || 'default') || instances.values().next().value;
441
+ const robot = instances.get(params.accountId || ctx?.accountId || 'default') || instances.values().next().value;
726
442
  if (!robot) return { content: [{ type: 'text', text: 'Error' }], details: {} };
727
443
  try {
728
- let req: any = createRequestPayload({
729
- task: `INVITATION: Join my workspace ${params.groupId}.\nRole: ${params.roleDescription}\nPlease use 'pier_group_mention' tool to reply in group ${params.groupId}.\nYou can also use 'pier_read_whiteboard' with groupId=${params.groupId} to view the plan.`,
730
- targetNodeId: params.nodeId,
731
- namespace: params.namespace || 'default',
732
- senderId: robot.accountId,
733
- meta: { sender: robot.accountId, workspace: params.groupId }
734
- } as TaskRequestParams);
735
- if (robot.config.privateKey) {
736
- req = await PierCrypto.signSwarmMessage(req, robot.config.privateKey);
737
- }
738
- await robot.js!.publish(`jobs.node.${params.nodeId}`, new TextEncoder().encode(JSON.stringify(req)));
739
- return { content: [{ type: 'text', text: `Invitation sent to ${params.nodeId}` }], details: {} };
740
- } catch(e: any) { return { content: [{ type: 'text', text: e.message }], details: {} }; }
444
+ const profile = await robot.client.getUserProfile(robot.config.secretKey);
445
+ return { content: [{ type: 'text', text: JSON.stringify(profile, null, 2) }], details: {} };
446
+ } catch (err: any) { return { content: [{ type: 'text', text: err.message }], details: {} }; }
741
447
  }
742
448
  }, { optional: true });
743
449
 
744
- // Status Command
745
- api.registerCommand({
746
- name: 'pier',
747
- description: 'Show Pier status',
748
- handler: () => {
749
- const lines = ['**Pier Connector Status**'];
750
- instances.forEach((r, id) => {
751
- lines.push(`\n**Account: ${id}**\n• Status: ${r.connectionStatus}\n• Jobs: ${r.stats.received}/${r.stats.completed}/${r.stats.failed}`);
752
- });
753
- return { text: lines.join('\n') };
754
- }
755
- });
756
-
757
450
  registerCli(api, globalStats);
758
451
  logger.info('[pier-connector] Plugin registered');
759
452
  };
@@ -764,4 +457,3 @@ export default definePluginEntry({
764
457
  description: 'Connects OpenClaw to the Pier job marketplace.',
765
458
  register
766
459
  });
767
-