@gholl-studio/pier-connector 0.2.52 → 0.3.1
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/package.json +16 -9
- package/src/cli.ts +29 -0
- package/src/config.ts +29 -0
- package/src/inbound.ts +162 -0
- package/src/index.ts +297 -0
- package/src/job-handler.ts +51 -0
- package/src/robot.ts +300 -0
- package/src/types/pier-sdk.d.ts +20 -0
- package/src/types.ts +32 -0
- package/src/config.js +0 -37
- package/src/index.js +0 -1236
- package/src/job-handler.js +0 -81
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gholl-studio/pier-connector",
|
|
3
3
|
"author": "gholl",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.3.1",
|
|
5
5
|
"description": "OpenClaw plugin that connects to the Pier job marketplace. Automatically fetches, executes, and reports distributed tasks for rewards.",
|
|
6
6
|
"type": "module",
|
|
7
|
-
"main": "src/index.
|
|
7
|
+
"main": "src/index.ts",
|
|
8
8
|
"engines": {
|
|
9
9
|
"node": ">=22"
|
|
10
10
|
},
|
|
@@ -15,14 +15,20 @@
|
|
|
15
15
|
],
|
|
16
16
|
"openclaw": {
|
|
17
17
|
"extensions": [
|
|
18
|
-
"src/index.
|
|
19
|
-
]
|
|
18
|
+
"./src/index.ts"
|
|
19
|
+
],
|
|
20
|
+
"setupEntry": "./src/cli.ts",
|
|
21
|
+
"channel": {
|
|
22
|
+
"id": "pier",
|
|
23
|
+
"label": "Pier",
|
|
24
|
+
"blurb": "Connect OpenClaw to the Pier job marketplace."
|
|
25
|
+
}
|
|
20
26
|
},
|
|
21
27
|
"scripts": {
|
|
22
|
-
"
|
|
23
|
-
"
|
|
28
|
+
"build": "tsc",
|
|
29
|
+
"watch": "tsc -w",
|
|
30
|
+
"prepublishOnly": "npm run build",
|
|
24
31
|
"lint": "eslint .",
|
|
25
|
-
"preview": "vite preview",
|
|
26
32
|
"test:watch": "node --watch test-standalone.js"
|
|
27
33
|
},
|
|
28
34
|
"keywords": [
|
|
@@ -50,8 +56,9 @@
|
|
|
50
56
|
}
|
|
51
57
|
},
|
|
52
58
|
"devDependencies": {
|
|
59
|
+
"@types/node": "^25.5.0",
|
|
53
60
|
"dotenv": "^17.3.1",
|
|
54
61
|
"openclaw": ">=2.0.0",
|
|
55
|
-
"typescript": "^5.
|
|
62
|
+
"typescript": "^5.9.3"
|
|
56
63
|
}
|
|
57
|
-
}
|
|
64
|
+
}
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import type { PierPluginApi } from './types.js';
|
|
4
|
+
|
|
5
|
+
export function registerCli(api: PierPluginApi, stats: any) {
|
|
6
|
+
const logger = api.logger;
|
|
7
|
+
api.registerCli(
|
|
8
|
+
({ program }) => {
|
|
9
|
+
program
|
|
10
|
+
.command('pier')
|
|
11
|
+
.description('Check Pier connection status')
|
|
12
|
+
.action(() => {
|
|
13
|
+
console.log('\n\x1b[1m\x1b[35m🦞 Pier Connector Status\x1b[0m\n');
|
|
14
|
+
console.log(`Total Jobs Received: \x1b[36m${stats.received}\x1b[0m`);
|
|
15
|
+
console.log(`Total Jobs Completed: \x1b[32m${stats.completed}\x1b[0m`);
|
|
16
|
+
console.log(`Total Jobs Failed: \x1b[31m${stats.failed}\x1b[0m\n`);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
program
|
|
20
|
+
.command('pier-configure')
|
|
21
|
+
.description('Interactively configure Pier Node')
|
|
22
|
+
.action(async () => {
|
|
23
|
+
// Logic for interactive configuration
|
|
24
|
+
// (Omitted for brevity, but can be extracted from index.js)
|
|
25
|
+
});
|
|
26
|
+
},
|
|
27
|
+
{ descriptors: [{ name: 'pier', description: 'Pier status', hasSubcommands: false }] }
|
|
28
|
+
);
|
|
29
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default configuration constants for pier-connector.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export interface PierDefaults {
|
|
6
|
+
PIER_API_URL: string;
|
|
7
|
+
NATS_URL: string;
|
|
8
|
+
SUBJECT: string;
|
|
9
|
+
PUBLISH_SUBJECT: string;
|
|
10
|
+
QUEUE_GROUP: string;
|
|
11
|
+
NODE_ID: string;
|
|
12
|
+
SECRET_KEY: string;
|
|
13
|
+
PRIVATE_KEY: string;
|
|
14
|
+
WALLET_ADDRESS: string;
|
|
15
|
+
AGENT_ID: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const DEFAULTS: PierDefaults = Object.freeze({
|
|
19
|
+
PIER_API_URL: 'https://pier-connector.gholl.com/api/v1',
|
|
20
|
+
NATS_URL: 'wss://pier.gholl.com/nexus',
|
|
21
|
+
SUBJECT: 'jobs.worker',
|
|
22
|
+
PUBLISH_SUBJECT: 'jobs.submit',
|
|
23
|
+
QUEUE_GROUP: 'openclaw-workers',
|
|
24
|
+
NODE_ID: '',
|
|
25
|
+
SECRET_KEY: '',
|
|
26
|
+
PRIVATE_KEY: '',
|
|
27
|
+
WALLET_ADDRESS: '',
|
|
28
|
+
AGENT_ID: '',
|
|
29
|
+
});
|
package/src/inbound.ts
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import type { PierPluginApi, InboundMessage } from './types.js';
|
|
2
|
+
import { truncate } from './job-handler.js';
|
|
3
|
+
|
|
4
|
+
export async function handleInbound(
|
|
5
|
+
api: PierPluginApi,
|
|
6
|
+
inbound: InboundMessage,
|
|
7
|
+
jobId: string,
|
|
8
|
+
robot: any, // Avoiding circular dependency with type PierRobot
|
|
9
|
+
pierChannel: any
|
|
10
|
+
) {
|
|
11
|
+
const logger = api.logger;
|
|
12
|
+
if (!api.runtime?.channel?.reply) {
|
|
13
|
+
logger.error(`[pier-connector][${robot.accountId}] SDK Error: api.runtime.channel.reply is not available.`);
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// 1. Resolve Global Configuration
|
|
18
|
+
const rootConfig = api.config || {};
|
|
19
|
+
|
|
20
|
+
// 2. Resolve Agent Route via SDK
|
|
21
|
+
const route = api.runtime.channel.routing.resolveAgentRoute({
|
|
22
|
+
cfg: rootConfig,
|
|
23
|
+
channel: 'pier',
|
|
24
|
+
accountId: inbound.accountId,
|
|
25
|
+
peer: { kind: 'direct', id: jobId }
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// 3. Robust Routing Decision Tree
|
|
29
|
+
let finalAgentId = null;
|
|
30
|
+
let routingSource = 'unresolved';
|
|
31
|
+
|
|
32
|
+
// A. Check explicit account-level binding in plugin local config
|
|
33
|
+
if (robot.config.agentId && robot.config.agentId !== 'main' && robot.config.agentId !== 'default') {
|
|
34
|
+
finalAgentId = robot.config.agentId;
|
|
35
|
+
routingSource = 'plugin-local-config';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// B. Check Global Bindings
|
|
39
|
+
if (!finalAgentId) {
|
|
40
|
+
const bindings = Array.isArray(rootConfig.bindings) ? rootConfig.bindings : [];
|
|
41
|
+
const binding = bindings.find((bi: any) =>
|
|
42
|
+
bi.match?.channel === 'pier' &&
|
|
43
|
+
(bi.match?.accountId === inbound.accountId || bi.match?.account === inbound.accountId)
|
|
44
|
+
);
|
|
45
|
+
if (binding?.agentId && binding.agentId !== 'main') {
|
|
46
|
+
finalAgentId = binding.agentId;
|
|
47
|
+
routingSource = 'manual-global-bindings-match';
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// C. SDK Routing Result
|
|
52
|
+
if (!finalAgentId && route.agentId && route.agentId !== 'main' && route.agentId !== 'default') {
|
|
53
|
+
finalAgentId = route.agentId;
|
|
54
|
+
routingSource = 'sdk-global-bindings';
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// D. Name-Matching Fallback
|
|
58
|
+
if (!finalAgentId && inbound.accountId && inbound.accountId !== 'default' && inbound.accountId !== 'main') {
|
|
59
|
+
const agents = (rootConfig as any).agents?.list || {};
|
|
60
|
+
const agentExists = Array.isArray(agents) ? agents.some((a: any) => a.id === inbound.accountId) : !!agents[inbound.accountId];
|
|
61
|
+
|
|
62
|
+
if (agentExists) {
|
|
63
|
+
finalAgentId = inbound.accountId;
|
|
64
|
+
routingSource = 'account-agent-name-match';
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// E. Ultimate Fallback
|
|
69
|
+
if (!finalAgentId) {
|
|
70
|
+
finalAgentId = route.agentId || 'main';
|
|
71
|
+
routingSource = route.agentId ? 'sdk-default-fallback' : 'hardcoded-main-fallback';
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
logger.info(`[pier-connector] Routing account '${inbound.accountId}' -> agent '${finalAgentId}' (Source: ${routingSource})`);
|
|
75
|
+
|
|
76
|
+
const dynamicSessionKey = `pier-job-${jobId}`;
|
|
77
|
+
const metadata = robot.activeNodeJobs.get(jobId);
|
|
78
|
+
let injectedPrompt = "";
|
|
79
|
+
|
|
80
|
+
if (metadata && metadata.isTargeted === false) {
|
|
81
|
+
injectedPrompt = [
|
|
82
|
+
"=========================================",
|
|
83
|
+
"CRITICAL DIRECTIVE - OPEN BIDDING MARKETPLACE",
|
|
84
|
+
"=========================================",
|
|
85
|
+
"You are an autonomous AI worker on Pier. This job currently has NO assigned worker.",
|
|
86
|
+
"1. DO NOT directly solve the user's task or output the final answer right now.",
|
|
87
|
+
"2. You MUST USE the \`pier_bid_task\` tool to send a pitch/bid explaining why you are best equipped.",
|
|
88
|
+
"3. Stop executing after submitting the bid.",
|
|
89
|
+
"4. You MUST use the Job ID provided below when calling the tool.",
|
|
90
|
+
`Job ID: ${jobId}`
|
|
91
|
+
].join('\n');
|
|
92
|
+
} else {
|
|
93
|
+
injectedPrompt = [
|
|
94
|
+
"=========================================",
|
|
95
|
+
"CRITICAL DIRECTIVE - PIER TRADING NETWORK",
|
|
96
|
+
"=========================================",
|
|
97
|
+
"You are an autonomous AI worker on Pier.",
|
|
98
|
+
"1. You MUST USE the \`pier_finish_task\` tool to submit the final result when work is complete.",
|
|
99
|
+
"2. If you need to negotiate or clarify requirements, use \`pier_chat\`.",
|
|
100
|
+
"3. Do not just wait in silence. Always officially finish the task if the goal is met."
|
|
101
|
+
].join('\n');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const ctxPayload = api.runtime.channel.reply.finalizeInboundContext({
|
|
105
|
+
AgentId: finalAgentId,
|
|
106
|
+
Body: inbound.body,
|
|
107
|
+
BodyForAgent: inbound.body,
|
|
108
|
+
RawBody: inbound.body,
|
|
109
|
+
From: inbound.senderId,
|
|
110
|
+
To: `pier:${jobId}`,
|
|
111
|
+
SessionKey: dynamicSessionKey,
|
|
112
|
+
AccountId: inbound.accountId,
|
|
113
|
+
ChatType: 'direct',
|
|
114
|
+
SenderId: inbound.senderId,
|
|
115
|
+
Provider: 'pier',
|
|
116
|
+
Surface: 'pier',
|
|
117
|
+
OriginatingChannel: 'pier',
|
|
118
|
+
OriginatingTo: `pier:${jobId}`,
|
|
119
|
+
WasMentioned: true,
|
|
120
|
+
CommandAuthorized: true,
|
|
121
|
+
SystemPrompt: injectedPrompt,
|
|
122
|
+
MessageId: jobId,
|
|
123
|
+
Metadata: {
|
|
124
|
+
...metadata,
|
|
125
|
+
accountId: robot.accountId,
|
|
126
|
+
pierJobId: jobId,
|
|
127
|
+
routingSource: routingSource
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
const { dispatcher, markDispatchIdle } = api.runtime.channel.reply.createReplyDispatcherWithTyping({
|
|
132
|
+
deliver: async (payload: any) => {
|
|
133
|
+
const currentMeta = robot.activeNodeJobs.get(jobId);
|
|
134
|
+
await pierChannel.outbound.sendText({
|
|
135
|
+
text: payload.text,
|
|
136
|
+
to: `pier:${jobId}`,
|
|
137
|
+
metadata: {
|
|
138
|
+
...currentMeta,
|
|
139
|
+
accountId: robot.accountId,
|
|
140
|
+
pierJobId: jobId
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
if (api.runtime.channel.session?.recordSessionMetaFromInbound) {
|
|
147
|
+
try {
|
|
148
|
+
const storePath = api.runtime.channel.session.resolveStorePath(dynamicSessionKey);
|
|
149
|
+
await api.runtime.channel.session.recordSessionMetaFromInbound({
|
|
150
|
+
storePath, sessionKey: dynamicSessionKey, ctx: ctxPayload
|
|
151
|
+
});
|
|
152
|
+
} catch (err) {}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
try {
|
|
156
|
+
await api.runtime.channel.reply.dispatchReplyFromConfig({
|
|
157
|
+
ctx: ctxPayload, cfg: api.config, dispatcher
|
|
158
|
+
});
|
|
159
|
+
} finally {
|
|
160
|
+
markDispatchIdle();
|
|
161
|
+
}
|
|
162
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
import { definePluginEntry } from 'openclaw/plugin-sdk/plugin-entry';
|
|
2
|
+
import { protocol } from '@gholl-studio/pier-sdk';
|
|
3
|
+
const { createRequestPayload } = protocol;
|
|
4
|
+
import { DEFAULTS } from './config.js';
|
|
5
|
+
import { PierRobot } from './robot.js';
|
|
6
|
+
import { handleInbound } from './inbound.js';
|
|
7
|
+
import { registerCli } from './cli.js';
|
|
8
|
+
import type { PierAccountConfig, PierPluginApi } from './types.js';
|
|
9
|
+
|
|
10
|
+
const register = (api: PierPluginApi) => {
|
|
11
|
+
const logger = api.logger;
|
|
12
|
+
const instances = new Map<string, PierRobot>();
|
|
13
|
+
const globalStats = { received: 0, completed: 0, failed: 0 };
|
|
14
|
+
|
|
15
|
+
function mergedCfgFrom(legacy: any, account: any): PierAccountConfig {
|
|
16
|
+
const merged = { ...legacy, ...account };
|
|
17
|
+
return {
|
|
18
|
+
accountId: account.accountId || 'default',
|
|
19
|
+
pierApiUrl: merged.pierApiUrl || DEFAULTS.PIER_API_URL,
|
|
20
|
+
nodeId: merged.nodeId || DEFAULTS.NODE_ID,
|
|
21
|
+
secretKey: merged.secretKey || DEFAULTS.SECRET_KEY,
|
|
22
|
+
privateKey: merged.privateKey || process.env.PIER_PRIVATE_KEY || DEFAULTS.PRIVATE_KEY,
|
|
23
|
+
natsUrl: merged.natsUrl || DEFAULTS.NATS_URL,
|
|
24
|
+
subject: merged.subject || DEFAULTS.SUBJECT,
|
|
25
|
+
publishSubject: merged.publishSubject || DEFAULTS.PUBLISH_SUBJECT,
|
|
26
|
+
queueGroup: merged.queueGroup || DEFAULTS.QUEUE_GROUP,
|
|
27
|
+
agentId: merged.agentId || DEFAULTS.AGENT_ID,
|
|
28
|
+
walletAddress: merged.walletAddress || DEFAULTS.WALLET_ADDRESS,
|
|
29
|
+
capabilities: merged.capabilities || ['translation', 'code-execution', 'reasoning', 'vision'],
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function resolveConfigs(): PierAccountConfig[] {
|
|
34
|
+
const globalAccounts = (api.config as any)?.channels?.['pier']?.accounts || {};
|
|
35
|
+
const pluginAccounts = (api.pluginConfig as any)?.accounts || {};
|
|
36
|
+
const rawAccounts = { ...globalAccounts, ...pluginAccounts };
|
|
37
|
+
const legacyCfg = api.pluginConfig || {};
|
|
38
|
+
|
|
39
|
+
if (Object.keys(rawAccounts).length === 0) {
|
|
40
|
+
return [mergedCfgFrom(legacyCfg, { accountId: 'default' })];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return Object.entries(rawAccounts).map(([id, account]: [string, any]) =>
|
|
44
|
+
mergedCfgFrom(legacyCfg, { ...account, accountId: id })
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const pierChannel = {
|
|
49
|
+
id: 'pier',
|
|
50
|
+
meta: {
|
|
51
|
+
id: 'pier',
|
|
52
|
+
label: 'Pier',
|
|
53
|
+
selectionLabel: 'Pier (NATS Job Marketplace)',
|
|
54
|
+
blurb: 'Connect to the Pier distributed job marketplace.'
|
|
55
|
+
},
|
|
56
|
+
outbound: {
|
|
57
|
+
sendText: async (params: any) => {
|
|
58
|
+
const robot = instances.get(params.metadata?.accountId || 'default') || instances.values().next().value;
|
|
59
|
+
if (!robot || !robot.js) return;
|
|
60
|
+
|
|
61
|
+
const jobId = params.metadata?.pierJobId || params.to.replace(/^pier:/, '');
|
|
62
|
+
const subject = `chat.${jobId}`;
|
|
63
|
+
|
|
64
|
+
const payload = {
|
|
65
|
+
id: crypto.randomUUID ? crypto.randomUUID() : (Math.random().toString(36).substring(2)),
|
|
66
|
+
job_id: jobId,
|
|
67
|
+
sender_id: robot.config.nodeId,
|
|
68
|
+
sender_name: robot.accountId,
|
|
69
|
+
sender_type: 'node',
|
|
70
|
+
content: params.text,
|
|
71
|
+
created_at: new Date().toISOString(),
|
|
72
|
+
auth_token: robot.config.secretKey
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
await robot.js.publish(subject, new TextEncoder().encode(JSON.stringify(payload)));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
api.registerChannel(pierChannel as any);
|
|
81
|
+
|
|
82
|
+
api.registerService({
|
|
83
|
+
id: 'pier-connector',
|
|
84
|
+
start: async () => {
|
|
85
|
+
const configs = resolveConfigs();
|
|
86
|
+
for (const config of configs) {
|
|
87
|
+
const robot = new PierRobot(config, api, async (inbound, jobId) => {
|
|
88
|
+
await handleInbound(api, inbound, jobId, robot, pierChannel);
|
|
89
|
+
globalStats.received++;
|
|
90
|
+
});
|
|
91
|
+
instances.set(config.accountId, robot);
|
|
92
|
+
await robot.start();
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
stop: async () => {
|
|
96
|
+
for (const robot of instances.values()) {
|
|
97
|
+
await robot.stop();
|
|
98
|
+
}
|
|
99
|
+
instances.clear();
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
api.registerTool({
|
|
104
|
+
name: 'pier_publish',
|
|
105
|
+
label: 'Publish to Pier',
|
|
106
|
+
description: 'Publish a task to the Pier job marketplace.',
|
|
107
|
+
parameters: {
|
|
108
|
+
type: 'object',
|
|
109
|
+
properties: {
|
|
110
|
+
task: { type: 'string' },
|
|
111
|
+
accountId: { type: 'string' }
|
|
112
|
+
},
|
|
113
|
+
required: ['task']
|
|
114
|
+
},
|
|
115
|
+
async execute(_id, params) {
|
|
116
|
+
const robot = instances.get(params.accountId || 'default') || instances.values().next().value;
|
|
117
|
+
if (!robot || robot.connectionStatus !== 'connected') {
|
|
118
|
+
return {
|
|
119
|
+
content: [{ type: 'text', text: 'Robot not connected' }],
|
|
120
|
+
details: {}
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const taskPayload = createRequestPayload({ task: params.task });
|
|
125
|
+
const reply = await robot.nc!.request(robot.config.publishSubject, new TextEncoder().encode(JSON.stringify(taskPayload)));
|
|
126
|
+
return {
|
|
127
|
+
content: [{ type: 'text', text: new TextDecoder().decode(reply.data) }],
|
|
128
|
+
details: {}
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
}, { optional: true });
|
|
132
|
+
|
|
133
|
+
api.registerTool(
|
|
134
|
+
{
|
|
135
|
+
name: 'pier_chat',
|
|
136
|
+
label: 'Pier Chat',
|
|
137
|
+
description: 'Send a message to the employer regarding a specific job.',
|
|
138
|
+
parameters: {
|
|
139
|
+
type: 'object',
|
|
140
|
+
properties: {
|
|
141
|
+
jobId: { type: 'string' },
|
|
142
|
+
text: { type: 'string' },
|
|
143
|
+
accountId: { type: 'string' }
|
|
144
|
+
},
|
|
145
|
+
required: ['jobId', 'text']
|
|
146
|
+
},
|
|
147
|
+
async execute(_id, params, ctx: any) {
|
|
148
|
+
const accountId = params.accountId || 'default';
|
|
149
|
+
const robot = instances.get(accountId) || instances.values().next().value;
|
|
150
|
+
if (!robot || robot.connectionStatus !== 'connected') {
|
|
151
|
+
return { content: [{ type: 'text', text: 'Error: Robot not connected' }], details: {} };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
try {
|
|
155
|
+
const subject = `chat.${params.jobId}`;
|
|
156
|
+
let metadata = robot.activeNodeJobs.get(params.jobId);
|
|
157
|
+
|
|
158
|
+
if (!metadata && ctx.to) {
|
|
159
|
+
const toId = ctx.to.replace(/^pier:/, '');
|
|
160
|
+
metadata = robot.activeNodeJobs.get(toId);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const jobId = metadata?.pierJobId || params.jobId;
|
|
164
|
+
|
|
165
|
+
if (!robot.js) {
|
|
166
|
+
return { content: [{ type: 'text', text: 'Error: JetStream not available' }], details: {} };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const payload = {
|
|
170
|
+
id: (globalThis.crypto as any).randomUUID ? (globalThis.crypto as any).randomUUID() : (Math.random().toString(36).substring(2)),
|
|
171
|
+
job_id: jobId,
|
|
172
|
+
sender_id: robot.config.nodeId,
|
|
173
|
+
sender_name: accountId,
|
|
174
|
+
sender_type: 'node',
|
|
175
|
+
content: params.text,
|
|
176
|
+
created_at: new Date().toISOString(),
|
|
177
|
+
auth_token: robot.config.secretKey
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
await robot.js.publish(subject, new TextEncoder().encode(JSON.stringify(payload)));
|
|
181
|
+
return { content: [{ type: 'text', text: 'Message sent' }], details: {} };
|
|
182
|
+
} catch (err: any) {
|
|
183
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }], details: {} };
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
},
|
|
187
|
+
{ optional: true }
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
const registerSystemActionTool = (name: string, label: string, description: string, action: string, extraParams: any, userRole = 'node') => {
|
|
191
|
+
api.registerTool({
|
|
192
|
+
name,
|
|
193
|
+
label,
|
|
194
|
+
description,
|
|
195
|
+
parameters: {
|
|
196
|
+
type: 'object',
|
|
197
|
+
properties: {
|
|
198
|
+
jobId: { type: 'string', description: 'The ID of the job' },
|
|
199
|
+
accountId: { type: 'string' },
|
|
200
|
+
...extraParams
|
|
201
|
+
},
|
|
202
|
+
required: ['jobId', ...Object.keys(extraParams)]
|
|
203
|
+
},
|
|
204
|
+
async execute(_id, params) {
|
|
205
|
+
const accountId = params.accountId || 'default';
|
|
206
|
+
const robot = instances.get(accountId) || instances.values().next().value;
|
|
207
|
+
if (!robot || robot.connectionStatus !== 'connected') {
|
|
208
|
+
return { content: [{ type: 'text', text: 'Error: Robot not connected' }], details: {} };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
try {
|
|
212
|
+
const subject = `chat.${params.jobId}`;
|
|
213
|
+
const { jobId: j, accountId: _, ...p } = params;
|
|
214
|
+
|
|
215
|
+
if (!robot.js) {
|
|
216
|
+
return { content: [{ type: 'text', text: 'Error: JetStream not available' }], details: {} };
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const msgData = {
|
|
220
|
+
id: (globalThis.crypto as any).randomUUID ? (globalThis.crypto as any).randomUUID() : (Math.random().toString(36).substring(2)),
|
|
221
|
+
job_id: params.jobId,
|
|
222
|
+
sender_id: userRole === 'user' ? 'user_' + robot.config.nodeId : robot.config.nodeId,
|
|
223
|
+
sender_type: userRole,
|
|
224
|
+
content: JSON.stringify({ type: 'system_action', action, payload: p }),
|
|
225
|
+
created_at: new Date().toISOString(),
|
|
226
|
+
auth_token: robot.config.secretKey,
|
|
227
|
+
type: 'system_action',
|
|
228
|
+
action: action
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
await robot.js.publish(subject, new TextEncoder().encode(JSON.stringify(msgData)));
|
|
232
|
+
return { content: [{ type: 'text', text: `${action} executed successfully` }], details: {} };
|
|
233
|
+
} catch (err: any) {
|
|
234
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }], details: {} };
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}, { optional: true });
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
registerSystemActionTool('pier_bid_task', 'Bid on task', 'Bid on an marketplace task', 'task_bid', { message: { type: 'string', description: 'Your pitch' } });
|
|
241
|
+
registerSystemActionTool('pier_accept_task', 'Accept task', 'Accept offered task', 'task_accept', {});
|
|
242
|
+
registerSystemActionTool('pier_finish_task', 'Finish task', 'Submit final result', 'task_submit', { result: { type: 'string', description: 'Final result' } });
|
|
243
|
+
registerSystemActionTool('pier_propose_task', 'Offer task', 'Offer task to a node', 'task_offer', {
|
|
244
|
+
price: { type: 'number' },
|
|
245
|
+
description: { type: 'string' }
|
|
246
|
+
}, 'user');
|
|
247
|
+
registerSystemActionTool('pier_rate_task', 'Rate task', 'Rate the node', 'task_rate', {
|
|
248
|
+
score: { type: 'number' },
|
|
249
|
+
comment: { type: 'string' }
|
|
250
|
+
}, 'user');
|
|
251
|
+
registerSystemActionTool('pier_reject_task', 'Reject task', 'Reject task', 'task_reject', { reason: { type: 'string' } });
|
|
252
|
+
registerSystemActionTool('pier_fail_task', 'Report error', 'Report that the task has failed', 'task_error', { error: { type: 'string' } });
|
|
253
|
+
registerSystemActionTool('pier_cancel_task', 'Cancel task', 'Cancel the task', 'task_cancel', { reason: { type: 'string' } }, 'user');
|
|
254
|
+
|
|
255
|
+
api.registerTool({
|
|
256
|
+
name: 'pier_get_profile',
|
|
257
|
+
label: 'Pier Profile',
|
|
258
|
+
description: 'Get current Pier profile and node stats.',
|
|
259
|
+
parameters: {
|
|
260
|
+
type: 'object',
|
|
261
|
+
properties: { accountId: { type: 'string' } }
|
|
262
|
+
},
|
|
263
|
+
async execute(_id, params) {
|
|
264
|
+
const robot = instances.get(params.accountId || 'default') || instances.values().next().value;
|
|
265
|
+
if (!robot) return { content: [{ type: 'text', text: 'Error: Robot not found' }], details: {} };
|
|
266
|
+
try {
|
|
267
|
+
const profile = await robot.client.getUserProfile(robot.config.secretKey);
|
|
268
|
+
return { content: [{ type: 'text', text: JSON.stringify(profile, null, 2) }], details: {} };
|
|
269
|
+
} catch (err: any) {
|
|
270
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }], details: {} };
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}, { optional: true });
|
|
274
|
+
|
|
275
|
+
// Register simple status command
|
|
276
|
+
api.registerCommand({
|
|
277
|
+
name: 'pier',
|
|
278
|
+
description: 'Show Pier status',
|
|
279
|
+
handler: () => {
|
|
280
|
+
const lines = ['**Pier Connector Status**'];
|
|
281
|
+
instances.forEach((r, id) => {
|
|
282
|
+
lines.push(`\n**Account: ${id}**\n• Status: ${r.connectionStatus}\n• Jobs: ${r.stats.received}/${r.stats.completed}/${r.stats.failed}`);
|
|
283
|
+
});
|
|
284
|
+
return { text: lines.join('\n') };
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
registerCli(api, globalStats);
|
|
289
|
+
logger.info('[pier-connector] Plugin registered');
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
export default definePluginEntry({
|
|
293
|
+
id: 'pier-connector',
|
|
294
|
+
name: 'Pier Connector',
|
|
295
|
+
description: 'Connects OpenClaw to the Pier job marketplace.',
|
|
296
|
+
register
|
|
297
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { protocol } from '@gholl-studio/pier-sdk';
|
|
2
|
+
const { normalizeInboundPayload } = protocol;
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Parse a raw NATS message into a structured job object.
|
|
6
|
+
*/
|
|
7
|
+
export function parseJob(msg: any, logger?: any) {
|
|
8
|
+
let raw: string;
|
|
9
|
+
try {
|
|
10
|
+
raw = msg.string();
|
|
11
|
+
} catch (err: any) {
|
|
12
|
+
if (logger) logger.error(`[pier-connector] Failed to read message bytes: ${err.message}`);
|
|
13
|
+
return { ok: false, error: 'Unreadable message payload' };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
let payload: any;
|
|
17
|
+
try {
|
|
18
|
+
payload = JSON.parse(raw);
|
|
19
|
+
} catch (err: any) {
|
|
20
|
+
if (logger) logger.error(`[pier-connector] Failed to parse JSON: ${err.message}`);
|
|
21
|
+
return { ok: false, error: `Invalid JSON: ${err.message}` };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return normalizeInboundPayload(payload);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Build a JSON response buffer to send back via msg.respond().
|
|
29
|
+
*/
|
|
30
|
+
export function encodeResponse(data: any): Uint8Array {
|
|
31
|
+
return new TextEncoder().encode(JSON.stringify(data));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Safely respond to a NATS message with a JSON object.
|
|
36
|
+
*/
|
|
37
|
+
export function safeRespond(msg: any, data: any) {
|
|
38
|
+
try {
|
|
39
|
+
msg.respond(encodeResponse(data));
|
|
40
|
+
} catch {
|
|
41
|
+
// Silently ignore
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Truncate a string for log readability.
|
|
47
|
+
*/
|
|
48
|
+
export function truncate(str: string, max: number = 100): string {
|
|
49
|
+
if (typeof str !== 'string') return String(str);
|
|
50
|
+
return str.length > max ? str.slice(0, max) + '…' : str;
|
|
51
|
+
}
|