@gholl-studio/pier-connector 0.6.3 ā 0.6.5
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/dist/cli.d.ts +6 -0
- package/dist/cli.js +22 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +20 -0
- package/dist/config.js +17 -0
- package/dist/config.js.map +1 -0
- package/dist/inbound.d.ts +7 -0
- package/dist/inbound.js +154 -0
- package/dist/inbound.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +644 -0
- package/dist/index.js.map +1 -0
- package/dist/job-handler.d.ts +20 -0
- package/dist/job-handler.js +62 -0
- package/dist/job-handler.js.map +1 -0
- package/dist/robot.d.ts +54 -0
- package/dist/robot.js +599 -0
- package/dist/robot.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/dist/types.d.ts +36 -0
- package/dist/types.js +6 -0
- package/dist/types.js.map +1 -0
- package/package.json +5 -5
- package/src/index.ts +3 -1
- package/src/robot.ts +19 -1
package/dist/index.js
ADDED
|
@@ -0,0 +1,644 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file index.ts
|
|
3
|
+
* @description Main entry point for the Pier Connector plugin.
|
|
4
|
+
* Implements the standard OpenClaw ChannelPlugin interface for native multi-account support.
|
|
5
|
+
*/
|
|
6
|
+
import { definePluginEntry } from 'openclaw/plugin-sdk/plugin-entry';
|
|
7
|
+
import { protocol, PierCrypto } from '@gholl-studio/pier-sdk';
|
|
8
|
+
const { createRequestPayload } = protocol;
|
|
9
|
+
import { DEFAULTS } from './config.js';
|
|
10
|
+
import { PierRobot } from './robot.js';
|
|
11
|
+
import { handleInbound } from './inbound.js';
|
|
12
|
+
import { registerCli } from './cli.js';
|
|
13
|
+
// Global instances map to track active robots by account ID
|
|
14
|
+
const instances = new Map();
|
|
15
|
+
/**
|
|
16
|
+
* Merges global/legacy config with account-specific overrides.
|
|
17
|
+
*/
|
|
18
|
+
function mergedCfgFrom(legacy, account) {
|
|
19
|
+
const merged = { ...legacy, ...account };
|
|
20
|
+
return {
|
|
21
|
+
accountId: account.accountId || 'default',
|
|
22
|
+
pierApiUrl: merged.pierApiUrl || DEFAULTS.PIER_API_URL,
|
|
23
|
+
nodeId: merged.nodeId || DEFAULTS.NODE_ID,
|
|
24
|
+
secretKey: merged.secretKey || DEFAULTS.SECRET_KEY,
|
|
25
|
+
privateKey: merged.privateKey || DEFAULTS.PRIVATE_KEY,
|
|
26
|
+
natsUrl: merged.natsUrl || DEFAULTS.NATS_URL,
|
|
27
|
+
subject: merged.subject || DEFAULTS.SUBJECT,
|
|
28
|
+
publishSubject: merged.publishSubject || DEFAULTS.PUBLISH_SUBJECT,
|
|
29
|
+
queueGroup: merged.queueGroup || DEFAULTS.QUEUE_GROUP,
|
|
30
|
+
agentId: merged.agentId || DEFAULTS.AGENT_ID,
|
|
31
|
+
walletAddress: merged.walletAddress || DEFAULTS.WALLET_ADDRESS,
|
|
32
|
+
capabilities: merged.capabilities || ['translation', 'code-execution', 'reasoning', 'vision'],
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Resolves all configured accounts for the Pier channel.
|
|
37
|
+
*/
|
|
38
|
+
function getAccountConfigs(cfg, pluginConfig) {
|
|
39
|
+
const globalAccounts = cfg?.channels?.['pier']?.accounts || {};
|
|
40
|
+
const pluginAccounts = pluginConfig?.accounts || {};
|
|
41
|
+
const rawAccounts = { ...globalAccounts, ...pluginAccounts };
|
|
42
|
+
const legacyCfg = pluginConfig || {};
|
|
43
|
+
if (Object.keys(rawAccounts).length === 0) {
|
|
44
|
+
return [mergedCfgFrom(legacyCfg, { accountId: 'default' })];
|
|
45
|
+
}
|
|
46
|
+
return Object.entries(rawAccounts).map(([id, account]) => mergedCfgFrom(legacyCfg, { ...account, accountId: id }));
|
|
47
|
+
}
|
|
48
|
+
const pierPlugin = {
|
|
49
|
+
id: 'pier',
|
|
50
|
+
meta: {
|
|
51
|
+
id: 'pier',
|
|
52
|
+
label: 'Pier',
|
|
53
|
+
selectionLabel: 'Pier (NATS Job Marketplace)',
|
|
54
|
+
docsPath: '/channels/pier',
|
|
55
|
+
blurb: 'Connect to the Pier distributed job marketplace.',
|
|
56
|
+
},
|
|
57
|
+
capabilities: {
|
|
58
|
+
chatTypes: ['direct'],
|
|
59
|
+
media: false,
|
|
60
|
+
reactions: false,
|
|
61
|
+
threads: false,
|
|
62
|
+
nativeCommands: true,
|
|
63
|
+
blockStreaming: true,
|
|
64
|
+
},
|
|
65
|
+
// -------------------------------------------------------------------------
|
|
66
|
+
// Config Adapter
|
|
67
|
+
// -------------------------------------------------------------------------
|
|
68
|
+
config: {
|
|
69
|
+
listAccountIds: (cfg) => {
|
|
70
|
+
const accounts = cfg?.channels?.['pier']?.accounts || {};
|
|
71
|
+
const ids = Object.keys(accounts);
|
|
72
|
+
return ids.length > 0 ? ids : ['default'];
|
|
73
|
+
},
|
|
74
|
+
resolveAccount: (cfg, accountId) => {
|
|
75
|
+
const accounts = cfg?.channels?.['pier']?.accounts || {};
|
|
76
|
+
const account = accounts[accountId || 'default'] || {};
|
|
77
|
+
return mergedCfgFrom(cfg?.channels?.['pier'] || {}, { ...account, accountId: accountId || 'default' });
|
|
78
|
+
},
|
|
79
|
+
defaultAccountId: () => 'default'
|
|
80
|
+
},
|
|
81
|
+
// -------------------------------------------------------------------------
|
|
82
|
+
// Pairing Adapter
|
|
83
|
+
// -------------------------------------------------------------------------
|
|
84
|
+
pairing: {
|
|
85
|
+
idLabel: 'pierJobId',
|
|
86
|
+
normalizeAllowEntry: (entry) => entry.replace(/^(pier|job):/i, ''),
|
|
87
|
+
notifyApproval: async ({ cfg, id, accountId }) => {
|
|
88
|
+
// Signal received when 'openclaw pairing approve pier <id>' is run.
|
|
89
|
+
// Useful if we want to trigger specific onboarding or status updates.
|
|
90
|
+
console.log(`[pier-connector] Pairing approved for Job ${id} on account ${accountId}`);
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
// -------------------------------------------------------------------------
|
|
94
|
+
// Outbound Adapter
|
|
95
|
+
// -------------------------------------------------------------------------
|
|
96
|
+
outbound: {
|
|
97
|
+
deliveryMode: 'direct',
|
|
98
|
+
sendText: async (ctx) => {
|
|
99
|
+
const robot = instances.get(ctx.accountId || 'default') || instances.values().next().value;
|
|
100
|
+
if (!robot || !robot.js) {
|
|
101
|
+
throw new Error('Robot not connected');
|
|
102
|
+
}
|
|
103
|
+
const jobId = ctx.to.replace(/^pier:/, '');
|
|
104
|
+
const jobMeta = robot.activeNodeJobs.get(jobId);
|
|
105
|
+
const isWorkspace = jobMeta?.workspace === true || ctx.metadata?.workspace === true;
|
|
106
|
+
const subject = isWorkspace ? `workspace.${jobId}.chat` : `chat.${jobId}`;
|
|
107
|
+
const payload = {
|
|
108
|
+
id: globalThis.crypto.randomUUID ? globalThis.crypto.randomUUID() : (Math.random().toString(36).substring(2)),
|
|
109
|
+
job_id: jobId,
|
|
110
|
+
sender_id: robot.config.nodeId,
|
|
111
|
+
sender_name: robot.accountId,
|
|
112
|
+
sender_type: 'node',
|
|
113
|
+
content: ctx.text,
|
|
114
|
+
created_at: new Date().toISOString(),
|
|
115
|
+
auth_token: robot.config.secretKey
|
|
116
|
+
};
|
|
117
|
+
await robot.js.publish(subject, new TextEncoder().encode(JSON.stringify(payload)));
|
|
118
|
+
return {
|
|
119
|
+
channel: 'pier',
|
|
120
|
+
messageId: payload.id,
|
|
121
|
+
conversationId: jobId
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
// -------------------------------------------------------------------------
|
|
126
|
+
// Status Adapter
|
|
127
|
+
// -------------------------------------------------------------------------
|
|
128
|
+
status: {
|
|
129
|
+
buildAccountSnapshot: ({ account, cfg }) => {
|
|
130
|
+
const robot = instances.get(account.accountId);
|
|
131
|
+
return {
|
|
132
|
+
accountId: account.accountId,
|
|
133
|
+
running: !!robot,
|
|
134
|
+
connected: robot?.connectionStatus === 'connected',
|
|
135
|
+
lastStartAt: robot?.lastStartAt,
|
|
136
|
+
lastStopAt: robot?.lastStopAt,
|
|
137
|
+
lastError: robot?.lastError,
|
|
138
|
+
healthState: robot?.connectionStatus === 'connected' ? 'healthy' : 'degraded',
|
|
139
|
+
};
|
|
140
|
+
},
|
|
141
|
+
resolveAccountState: ({ configured, enabled }) => {
|
|
142
|
+
return configured && enabled ? 'enabled' : 'disabled';
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
// -------------------------------------------------------------------------
|
|
146
|
+
// Gateway Adapter
|
|
147
|
+
// -------------------------------------------------------------------------
|
|
148
|
+
gateway: {
|
|
149
|
+
startAccount: async (ctx) => {
|
|
150
|
+
const config = ctx.account;
|
|
151
|
+
// Use ctx as the runtime provider as it contains .channelRuntime, .log, etc.
|
|
152
|
+
const robot = new PierRobot(config, ctx, async (inbound, jobId) => {
|
|
153
|
+
// Pass the context containing channelRuntime
|
|
154
|
+
await handleInbound(ctx, inbound, jobId, robot, pierPlugin);
|
|
155
|
+
});
|
|
156
|
+
instances.set(ctx.accountId, robot);
|
|
157
|
+
try {
|
|
158
|
+
robot.lastStartAt = Date.now();
|
|
159
|
+
await robot.start();
|
|
160
|
+
ctx.setStatus({
|
|
161
|
+
...ctx.getStatus(),
|
|
162
|
+
running: true,
|
|
163
|
+
connected: true,
|
|
164
|
+
lastStartAt: robot.lastStartAt
|
|
165
|
+
});
|
|
166
|
+
// IMPORTANT: Keep startAccount promise active for the lifetime of the connection.
|
|
167
|
+
// If this promise resolves, the Gateway will auto-restart the account.
|
|
168
|
+
await new Promise((resolve) => {
|
|
169
|
+
ctx.abortSignal.addEventListener('abort', () => {
|
|
170
|
+
resolve();
|
|
171
|
+
}, { once: true });
|
|
172
|
+
if (ctx.abortSignal.aborted)
|
|
173
|
+
resolve();
|
|
174
|
+
});
|
|
175
|
+
console.log(`[pier-connector][${ctx.accountId}] Account signal aborted. Stopping...`);
|
|
176
|
+
}
|
|
177
|
+
catch (err) {
|
|
178
|
+
robot.lastError = err.message;
|
|
179
|
+
ctx.setStatus({
|
|
180
|
+
...ctx.getStatus(),
|
|
181
|
+
running: false,
|
|
182
|
+
lastError: err.message
|
|
183
|
+
});
|
|
184
|
+
throw err;
|
|
185
|
+
}
|
|
186
|
+
},
|
|
187
|
+
stopAccount: async (ctx) => {
|
|
188
|
+
const robot = instances.get(ctx.accountId);
|
|
189
|
+
if (robot) {
|
|
190
|
+
robot.lastStopAt = Date.now();
|
|
191
|
+
await robot.stop();
|
|
192
|
+
instances.delete(ctx.accountId);
|
|
193
|
+
}
|
|
194
|
+
ctx.setStatus({
|
|
195
|
+
...ctx.getStatus(),
|
|
196
|
+
running: false,
|
|
197
|
+
lastStopAt: robot.lastStopAt
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
const register = (api) => {
|
|
203
|
+
const logger = api.logger;
|
|
204
|
+
const globalStats = { received: 0, completed: 0, failed: 0 };
|
|
205
|
+
// Register our new ChannelPlugin
|
|
206
|
+
api.registerChannel({ plugin: pierPlugin });
|
|
207
|
+
// Tools and CLI registration
|
|
208
|
+
api.registerTool({
|
|
209
|
+
name: 'pier_publish',
|
|
210
|
+
label: 'Publish to Pier',
|
|
211
|
+
description: 'Publish a task to the Pier job marketplace.',
|
|
212
|
+
parameters: {
|
|
213
|
+
type: 'object',
|
|
214
|
+
properties: {
|
|
215
|
+
task: { type: 'string' },
|
|
216
|
+
accountId: { type: 'string' }
|
|
217
|
+
},
|
|
218
|
+
required: ['task']
|
|
219
|
+
},
|
|
220
|
+
async execute(_id, params, ctx) {
|
|
221
|
+
const accountId = params.accountId || ctx?.metadata?.accountId || ctx?.accountId || 'default';
|
|
222
|
+
const robot = instances.get(accountId) || instances.values().next().value;
|
|
223
|
+
if (!robot || robot.connectionStatus !== 'connected') {
|
|
224
|
+
return {
|
|
225
|
+
content: [{ type: 'text', text: 'Robot not connected' }],
|
|
226
|
+
details: {}
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
const taskPayload = createRequestPayload({ task: params.task });
|
|
230
|
+
const reply = await robot.nc.request(robot.config.publishSubject, new TextEncoder().encode(JSON.stringify(taskPayload)));
|
|
231
|
+
return {
|
|
232
|
+
content: [{ type: 'text', text: new TextDecoder().decode(reply.data) }],
|
|
233
|
+
details: {}
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
}, { optional: true });
|
|
237
|
+
api.registerTool({
|
|
238
|
+
name: 'pier_chat',
|
|
239
|
+
label: 'Pier Chat',
|
|
240
|
+
description: 'Send a message to the employer regarding a specific job.',
|
|
241
|
+
parameters: {
|
|
242
|
+
type: 'object',
|
|
243
|
+
properties: {
|
|
244
|
+
jobId: { type: 'string' },
|
|
245
|
+
text: { type: 'string' },
|
|
246
|
+
accountId: { type: 'string' }
|
|
247
|
+
},
|
|
248
|
+
required: ['jobId', 'text']
|
|
249
|
+
},
|
|
250
|
+
async execute(_id, params, ctx) {
|
|
251
|
+
const accountId = params.accountId || ctx?.metadata?.accountId || ctx?.accountId || 'default';
|
|
252
|
+
const robot = instances.get(accountId) || instances.values().next().value;
|
|
253
|
+
if (!robot || robot.connectionStatus !== 'connected') {
|
|
254
|
+
return { content: [{ type: 'text', text: 'Error: Robot not connected' }], details: {} };
|
|
255
|
+
}
|
|
256
|
+
try {
|
|
257
|
+
const subject = `chat.${params.jobId}`;
|
|
258
|
+
let metadata = robot.activeNodeJobs.get(params.jobId);
|
|
259
|
+
if (!metadata && ctx.to) {
|
|
260
|
+
const toId = ctx.to.replace(/^pier:/, '');
|
|
261
|
+
metadata = robot.activeNodeJobs.get(toId);
|
|
262
|
+
}
|
|
263
|
+
const jobId = metadata?.pierJobId || params.jobId;
|
|
264
|
+
if (!robot.js) {
|
|
265
|
+
return { content: [{ type: 'text', text: 'Error: JetStream not available' }], details: {} };
|
|
266
|
+
}
|
|
267
|
+
const payload = {
|
|
268
|
+
id: globalThis.crypto.randomUUID ? globalThis.crypto.randomUUID() : (Math.random().toString(36).substring(2)),
|
|
269
|
+
job_id: jobId,
|
|
270
|
+
sender_id: robot.config.nodeId,
|
|
271
|
+
sender_name: accountId,
|
|
272
|
+
sender_type: 'node',
|
|
273
|
+
content: params.text,
|
|
274
|
+
created_at: new Date().toISOString(),
|
|
275
|
+
auth_token: robot.config.secretKey
|
|
276
|
+
};
|
|
277
|
+
await robot.js.publish(subject, new TextEncoder().encode(JSON.stringify(payload)));
|
|
278
|
+
return { content: [{ type: 'text', text: 'Message sent' }], details: {} };
|
|
279
|
+
}
|
|
280
|
+
catch (err) {
|
|
281
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }], details: {} };
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}, { optional: true });
|
|
285
|
+
// Helper to register marketplace action tools
|
|
286
|
+
const registerSystemActionTool = (name, label, description, action, extraParams, userRole = 'node') => {
|
|
287
|
+
api.registerTool({
|
|
288
|
+
name,
|
|
289
|
+
label,
|
|
290
|
+
description,
|
|
291
|
+
parameters: {
|
|
292
|
+
type: 'object',
|
|
293
|
+
properties: {
|
|
294
|
+
jobId: { type: 'string', description: 'The ID of the job' },
|
|
295
|
+
accountId: { type: 'string' },
|
|
296
|
+
...extraParams
|
|
297
|
+
},
|
|
298
|
+
required: ['jobId', ...Object.keys(extraParams)]
|
|
299
|
+
},
|
|
300
|
+
async execute(_id, params, ctx) {
|
|
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
|
+
}
|
|
306
|
+
try {
|
|
307
|
+
const { jobId: j, accountId: _, ...p } = params;
|
|
308
|
+
if (!robot.js) {
|
|
309
|
+
return { content: [{ type: 'text', text: 'Error: JetStream not available' }], details: {} };
|
|
310
|
+
}
|
|
311
|
+
const msgData = {
|
|
312
|
+
id: globalThis.crypto.randomUUID ? globalThis.crypto.randomUUID() : (Math.random().toString(36).substring(2)),
|
|
313
|
+
job_id: params.jobId,
|
|
314
|
+
sender_id: userRole === 'user' ? 'user_' + robot.config.nodeId : robot.config.nodeId,
|
|
315
|
+
sender_type: userRole,
|
|
316
|
+
content: JSON.stringify({ type: 'system_action', action, payload: p }),
|
|
317
|
+
created_at: new Date().toISOString(),
|
|
318
|
+
auth_token: robot.config.secretKey,
|
|
319
|
+
type: 'system_action',
|
|
320
|
+
action: action
|
|
321
|
+
};
|
|
322
|
+
await robot.js.publish(`chat.${params.jobId}`, new TextEncoder().encode(JSON.stringify(msgData)));
|
|
323
|
+
return { content: [{ type: 'text', text: `${action} executed successfully` }], details: {} };
|
|
324
|
+
}
|
|
325
|
+
catch (err) {
|
|
326
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }], details: {} };
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}, { optional: true });
|
|
330
|
+
};
|
|
331
|
+
registerSystemActionTool('pier_bid_task', 'Bid on task', 'Bid on an marketplace task', 'task_bid', { message: { type: 'string', description: 'Your pitch' } });
|
|
332
|
+
registerSystemActionTool('pier_accept_task', 'Accept task', 'Accept offered task', 'task_accept', {});
|
|
333
|
+
registerSystemActionTool('pier_finish_task', 'Finish task', 'Submit final result', 'task_submit', { result: { type: 'string', description: 'Final result' } });
|
|
334
|
+
registerSystemActionTool('pier_propose_task', 'Offer task', 'Offer task to a node', 'task_offer', {
|
|
335
|
+
price: { type: 'number' },
|
|
336
|
+
description: { type: 'string' }
|
|
337
|
+
}, 'user');
|
|
338
|
+
registerSystemActionTool('pier_rate_task', 'Rate task', 'Rate the node', 'task_rate', {
|
|
339
|
+
score: { type: 'number' },
|
|
340
|
+
comment: { type: 'string' }
|
|
341
|
+
}, 'user');
|
|
342
|
+
registerSystemActionTool('pier_reject_task', 'Reject task', 'Reject task', 'task_reject', { reason: { type: 'string' } });
|
|
343
|
+
registerSystemActionTool('pier_fail_task', 'Report error', 'Report that the task has failed', 'task_error', { error: { type: 'string' } });
|
|
344
|
+
registerSystemActionTool('pier_cancel_task', 'Cancel task', 'Cancel the task', 'task_cancel', { reason: { type: 'string' } }, 'user');
|
|
345
|
+
api.registerTool({
|
|
346
|
+
name: 'pier_get_profile',
|
|
347
|
+
label: 'Pier Profile',
|
|
348
|
+
description: 'Get current Pier profile and node stats.',
|
|
349
|
+
parameters: {
|
|
350
|
+
type: 'object',
|
|
351
|
+
properties: { accountId: { type: 'string' } }
|
|
352
|
+
},
|
|
353
|
+
async execute(_id, params, ctx) {
|
|
354
|
+
const accountId = params.accountId || ctx?.metadata?.accountId || ctx?.accountId || 'default';
|
|
355
|
+
const robot = instances.get(accountId) || instances.values().next().value;
|
|
356
|
+
if (!robot)
|
|
357
|
+
return { content: [{ type: 'text', text: 'Error: Robot not found' }], details: {} };
|
|
358
|
+
try {
|
|
359
|
+
const profile = await robot.client.getUserProfile(robot.config.secretKey);
|
|
360
|
+
return { content: [{ type: 'text', text: JSON.stringify(profile, null, 2) }], details: {} };
|
|
361
|
+
}
|
|
362
|
+
catch (err) {
|
|
363
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }], details: {} };
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}, { optional: true });
|
|
367
|
+
// Multi-Agent Workspace Tools
|
|
368
|
+
api.registerTool({
|
|
369
|
+
name: 'pier_create_workspace',
|
|
370
|
+
label: 'Create Workspace',
|
|
371
|
+
description: 'Creates a decentralized multi-agent workspace (group chat) and initializes its whiteboard.',
|
|
372
|
+
parameters: {
|
|
373
|
+
type: 'object',
|
|
374
|
+
properties: {
|
|
375
|
+
namespace: { type: 'string', description: 'Federation namespace. Default to "default" if omitted.' }
|
|
376
|
+
}
|
|
377
|
+
},
|
|
378
|
+
async execute(_id, params, ctx) {
|
|
379
|
+
const accountId = params.accountId || ctx?.metadata?.accountId || ctx?.accountId || 'default';
|
|
380
|
+
const robot = instances.get(accountId) || instances.values().next().value;
|
|
381
|
+
if (!robot)
|
|
382
|
+
return { content: [{ type: 'text', text: 'Error: Robot not found' }], details: {} };
|
|
383
|
+
try {
|
|
384
|
+
const groupId = globalThis.crypto.randomUUID ? globalThis.crypto.randomUUID() : (Math.random().toString(36).substring(2));
|
|
385
|
+
const kv = await robot.client.nats.getWorkspaceKV(groupId);
|
|
386
|
+
await kv.put('status', new TextEncoder().encode('Active Workspace Created'));
|
|
387
|
+
await kv.put('budget', new TextEncoder().encode('100'));
|
|
388
|
+
return { content: [{ type: 'text', text: `Workspace created successfully. Group ID: ${groupId}. Initial budget set to 100.` }], details: { groupId } };
|
|
389
|
+
}
|
|
390
|
+
catch (err) {
|
|
391
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }], details: {} };
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}, { optional: true });
|
|
395
|
+
api.registerTool({
|
|
396
|
+
name: 'pier_read_whiteboard',
|
|
397
|
+
label: 'Read Whiteboard',
|
|
398
|
+
description: 'Read the current shared state from the workspace whiteboard (NATS KV)',
|
|
399
|
+
parameters: {
|
|
400
|
+
type: 'object',
|
|
401
|
+
properties: {
|
|
402
|
+
groupId: { type: 'string', description: 'The Workspace Group ID' },
|
|
403
|
+
key: { type: 'string', description: 'The key to read' },
|
|
404
|
+
namespace: { type: 'string', description: 'Federation namespace' }
|
|
405
|
+
},
|
|
406
|
+
required: ['groupId', 'key']
|
|
407
|
+
},
|
|
408
|
+
async execute(_id, params, ctx) {
|
|
409
|
+
const robot = instances.get(params.accountId || ctx?.metadata?.accountId || ctx?.accountId || 'default') || instances.values().next().value;
|
|
410
|
+
if (!robot)
|
|
411
|
+
return { content: [{ type: 'text', text: 'Error: Robot not found' }], details: {} };
|
|
412
|
+
try {
|
|
413
|
+
const kv = await robot.client.nats.getWorkspaceKV(params.groupId);
|
|
414
|
+
const entry = await kv.get(params.key);
|
|
415
|
+
if (!entry)
|
|
416
|
+
return { content: [{ type: 'text', text: '[Whiteboard: Key Not Found. Use revision 0 to update.]' }], details: { revision: 0 } };
|
|
417
|
+
const valueStr = new TextDecoder().decode(entry.value);
|
|
418
|
+
const result = JSON.stringify({ revision: entry.revision, value: valueStr });
|
|
419
|
+
return { content: [{ type: 'text', text: `Data retrieved:\n${result}\n(Supply this revision to pier_update_whiteboard)` }], details: { revision: entry.revision } };
|
|
420
|
+
}
|
|
421
|
+
catch (err) {
|
|
422
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }], details: {} };
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}, { optional: true });
|
|
426
|
+
api.registerTool({
|
|
427
|
+
name: 'pier_update_whiteboard',
|
|
428
|
+
label: 'Update Whiteboard',
|
|
429
|
+
description: 'Update the shared state on the workspace whiteboard using CAS (Compare-And-Swap). Provide expected_revision from read.',
|
|
430
|
+
parameters: {
|
|
431
|
+
type: 'object',
|
|
432
|
+
properties: {
|
|
433
|
+
groupId: { type: 'string' },
|
|
434
|
+
key: { type: 'string' },
|
|
435
|
+
value: { type: 'string' },
|
|
436
|
+
expected_revision: { type: 'number', description: 'The revision number from the last read (use 0 if key not found)' },
|
|
437
|
+
namespace: { type: 'string' }
|
|
438
|
+
},
|
|
439
|
+
required: ['groupId', 'key', 'value', 'expected_revision']
|
|
440
|
+
},
|
|
441
|
+
async execute(_id, params, ctx) {
|
|
442
|
+
const robot = instances.get(ctx?.metadata?.accountId || ctx?.accountId || 'default') || instances.values().next().value;
|
|
443
|
+
if (!robot)
|
|
444
|
+
return { content: [{ type: 'text', text: 'Robot not found' }], details: {} };
|
|
445
|
+
try {
|
|
446
|
+
const kv = await robot.client.nats.getWorkspaceKV(params.groupId);
|
|
447
|
+
try {
|
|
448
|
+
// if expected_revision is 0 it means we expect the key does not exist yet (or we use create)
|
|
449
|
+
if (params.expected_revision === 0) {
|
|
450
|
+
try {
|
|
451
|
+
await kv.create(params.key, new TextEncoder().encode(params.value));
|
|
452
|
+
}
|
|
453
|
+
catch (e) {
|
|
454
|
+
if (e.message?.includes('wrong last sequence'))
|
|
455
|
+
throw e; // Pass to conflict handler
|
|
456
|
+
await kv.update(params.key, new TextEncoder().encode(params.value), 0); // fallback
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
else {
|
|
460
|
+
await kv.update(params.key, new TextEncoder().encode(params.value), params.expected_revision);
|
|
461
|
+
}
|
|
462
|
+
return { content: [{ type: 'text', text: `Whiteboard key '${params.key}' updated successfully.` }], details: {} };
|
|
463
|
+
}
|
|
464
|
+
catch (updateErr) {
|
|
465
|
+
if (updateErr.message && (updateErr.message.includes('wrong last sequence') || updateErr.code === '400' || updateErr.api_error?.err_code === 10071)) {
|
|
466
|
+
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: {} };
|
|
467
|
+
}
|
|
468
|
+
throw updateErr;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
catch (err) {
|
|
472
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }], details: {} };
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}, { optional: true });
|
|
476
|
+
api.registerTool({
|
|
477
|
+
name: 'pier_group_mention',
|
|
478
|
+
label: 'Mention in Workspace',
|
|
479
|
+
description: 'Send a message in the workspace group chat explicitly mentioning an agent to wake them up',
|
|
480
|
+
parameters: {
|
|
481
|
+
type: 'object',
|
|
482
|
+
properties: {
|
|
483
|
+
groupId: { type: 'string' },
|
|
484
|
+
mentions: { type: 'array', items: { type: 'string' }, description: 'Array of target Agent Node IDs' },
|
|
485
|
+
content: { type: 'string', description: 'Message content' },
|
|
486
|
+
namespace: { type: 'string' }
|
|
487
|
+
},
|
|
488
|
+
required: ['groupId', 'mentions', 'content']
|
|
489
|
+
},
|
|
490
|
+
async execute(_id, params, ctx) {
|
|
491
|
+
const robot = instances.get(ctx?.metadata?.accountId || ctx?.accountId || 'default') || instances.values().next().value;
|
|
492
|
+
if (!robot)
|
|
493
|
+
return { content: [{ type: 'text', text: 'Robot not found' }], details: {} };
|
|
494
|
+
try {
|
|
495
|
+
const msgId = globalThis.crypto.randomUUID ? globalThis.crypto.randomUUID() : (Math.random().toString(36).substring(2));
|
|
496
|
+
const ns = params.namespace || 'default';
|
|
497
|
+
let payload = {
|
|
498
|
+
version: 'psp-1.0',
|
|
499
|
+
id: msgId,
|
|
500
|
+
namespace: ns,
|
|
501
|
+
timestamp: Date.now(),
|
|
502
|
+
senderId: robot.accountId,
|
|
503
|
+
groupId: params.groupId,
|
|
504
|
+
content: params.content,
|
|
505
|
+
mentions: params.mentions
|
|
506
|
+
};
|
|
507
|
+
if (robot.config.privateKey) {
|
|
508
|
+
payload = await PierCrypto.signSwarmMessage(payload, robot.config.privateKey);
|
|
509
|
+
}
|
|
510
|
+
await robot.client.nats.publishGroupChat(params.groupId, payload, ns);
|
|
511
|
+
return { content: [{ type: 'text', text: `Message sent to workspace ${params.groupId} successfully.` }], details: {} };
|
|
512
|
+
}
|
|
513
|
+
catch (err) {
|
|
514
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }], details: {} };
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
}, { optional: true });
|
|
518
|
+
api.registerTool({
|
|
519
|
+
name: 'pier_search_workers',
|
|
520
|
+
label: 'Search Workers',
|
|
521
|
+
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.',
|
|
522
|
+
parameters: {
|
|
523
|
+
type: 'object',
|
|
524
|
+
properties: {
|
|
525
|
+
capability: { type: 'string', description: '(Recommended) Filter by a specific capability e.g. "translation", "code-execution". Uses fast per-subject discovery when set.' }
|
|
526
|
+
}
|
|
527
|
+
},
|
|
528
|
+
async execute(_id, params, ctx) {
|
|
529
|
+
const robot = instances.get(ctx?.metadata?.accountId || ctx?.accountId || 'default') || instances.values().next().value;
|
|
530
|
+
if (!robot)
|
|
531
|
+
return { content: [{ type: 'text', text: 'Error' }], details: {} };
|
|
532
|
+
// --- PSP 1.0 Subject-based Discovery (fast path, used when capability is specified) ---
|
|
533
|
+
if (params.capability) {
|
|
534
|
+
try {
|
|
535
|
+
const nodes = await robot.client.nats.queryDiscovery(params.capability);
|
|
536
|
+
return { content: [{ type: 'text', text: JSON.stringify(nodes, null, 2) }], details: {} };
|
|
537
|
+
}
|
|
538
|
+
catch (e) {
|
|
539
|
+
// Fall through to KV scan
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
// --- KV Full-scan (fallback when no capability filter, or subject discovery fails) ---
|
|
543
|
+
try {
|
|
544
|
+
const kv = await robot.client.nats.getWorkspaceKV('active_nodes');
|
|
545
|
+
const activeNodes = [];
|
|
546
|
+
const iter = await kv.keys();
|
|
547
|
+
for await (const k of iter) {
|
|
548
|
+
const entry = await kv.get(k);
|
|
549
|
+
if (entry) {
|
|
550
|
+
try {
|
|
551
|
+
// S10: DoS protection
|
|
552
|
+
if (entry.value.length > 1024 * 1024)
|
|
553
|
+
continue;
|
|
554
|
+
const nodeData = JSON.parse(new TextDecoder().decode(entry.value));
|
|
555
|
+
if (Date.now() - nodeData.timestamp < 120000) {
|
|
556
|
+
// --- Zero-Trust: Verify signed heartbeat (PSP 1.0 Hard-Block) ---
|
|
557
|
+
if (nodeData.version === 'psp-1.0' && nodeData.signature && nodeData.publicKey) {
|
|
558
|
+
const isValid = PierCrypto.verifySwarmMessage(nodeData);
|
|
559
|
+
if (!isValid) {
|
|
560
|
+
console.warn(`[pier_search_workers] š“ Dropping node ${k}: invalid signature (spoofed identity).`);
|
|
561
|
+
continue;
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
activeNodes.push(nodeData);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
catch (e) { }
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
return { content: [{ type: 'text', text: JSON.stringify(activeNodes.slice(0, 10), null, 2) }], details: {} };
|
|
571
|
+
}
|
|
572
|
+
catch (e) {
|
|
573
|
+
// Final fallback: central API
|
|
574
|
+
try {
|
|
575
|
+
const resp = await fetch(`${robot.client.apiUrl}/nodes`);
|
|
576
|
+
const nodes = await resp.json();
|
|
577
|
+
const active = nodes.filter((n) => n.is_online).slice(0, 10);
|
|
578
|
+
return { content: [{ type: 'text', text: JSON.stringify(active, null, 2) }], details: {} };
|
|
579
|
+
}
|
|
580
|
+
catch (fallbackErr) {
|
|
581
|
+
return { content: [{ type: 'text', text: `All discovery methods failed: ${e.message}` }], details: {} };
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
}, { optional: true });
|
|
586
|
+
api.registerTool({
|
|
587
|
+
name: 'pier_invite_collaborator',
|
|
588
|
+
label: 'Invite to Workspace',
|
|
589
|
+
description: 'Send a targeted job request to a node to invite them.',
|
|
590
|
+
parameters: {
|
|
591
|
+
type: 'object',
|
|
592
|
+
properties: {
|
|
593
|
+
nodeId: { type: 'string' },
|
|
594
|
+
groupId: { type: 'string' },
|
|
595
|
+
roleDescription: { type: 'string' },
|
|
596
|
+
namespace: { type: 'string' }
|
|
597
|
+
},
|
|
598
|
+
required: ['nodeId', 'groupId', 'roleDescription']
|
|
599
|
+
},
|
|
600
|
+
async execute(_id, params, ctx) {
|
|
601
|
+
const robot = instances.get(ctx?.metadata?.accountId || ctx?.accountId || 'default') || instances.values().next().value;
|
|
602
|
+
if (!robot)
|
|
603
|
+
return { content: [{ type: 'text', text: 'Error' }], details: {} };
|
|
604
|
+
try {
|
|
605
|
+
let req = createRequestPayload({
|
|
606
|
+
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.`,
|
|
607
|
+
targetNodeId: params.nodeId,
|
|
608
|
+
namespace: params.namespace || 'default',
|
|
609
|
+
senderId: robot.accountId,
|
|
610
|
+
meta: { sender: robot.accountId, workspace: params.groupId }
|
|
611
|
+
});
|
|
612
|
+
if (robot.config.privateKey) {
|
|
613
|
+
req = await PierCrypto.signSwarmMessage(req, robot.config.privateKey);
|
|
614
|
+
}
|
|
615
|
+
await robot.js.publish(`jobs.node.${params.nodeId}`, new TextEncoder().encode(JSON.stringify(req)));
|
|
616
|
+
return { content: [{ type: 'text', text: `Invitation sent to ${params.nodeId}` }], details: {} };
|
|
617
|
+
}
|
|
618
|
+
catch (e) {
|
|
619
|
+
return { content: [{ type: 'text', text: e.message }], details: {} };
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
}, { optional: true });
|
|
623
|
+
// Status Command
|
|
624
|
+
api.registerCommand({
|
|
625
|
+
name: 'pier',
|
|
626
|
+
description: 'Show Pier status',
|
|
627
|
+
handler: () => {
|
|
628
|
+
const lines = ['**Pier Connector Status**'];
|
|
629
|
+
instances.forEach((r, id) => {
|
|
630
|
+
lines.push(`\n**Account: ${id}**\n⢠Status: ${r.connectionStatus}\n⢠Jobs: ${r.stats.received}/${r.stats.completed}/${r.stats.failed}`);
|
|
631
|
+
});
|
|
632
|
+
return { text: lines.join('\n') };
|
|
633
|
+
}
|
|
634
|
+
});
|
|
635
|
+
registerCli(api, globalStats);
|
|
636
|
+
logger.info('[pier-connector] Plugin registered');
|
|
637
|
+
};
|
|
638
|
+
export default definePluginEntry({
|
|
639
|
+
id: 'pier-connector',
|
|
640
|
+
name: 'Pier Connector',
|
|
641
|
+
description: 'Connects OpenClaw to the Pier job marketplace.',
|
|
642
|
+
register
|
|
643
|
+
});
|
|
644
|
+
//# sourceMappingURL=index.js.map
|