@gholl-studio/pier-connector 0.2.52 → 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/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@gholl-studio/pier-connector",
3
3
  "author": "gholl",
4
- "version": "0.2.52",
4
+ "version": "0.3.0",
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.js",
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.js"
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
- "dev": "vite",
23
- "build": "tsc -b && vite build",
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.0.0"
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,155 @@
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
+ // Register simple status command
134
+ api.registerCommand({
135
+ name: 'pier',
136
+ description: 'Show Pier status',
137
+ handler: () => {
138
+ const lines = ['**Pier Connector Status**'];
139
+ instances.forEach((r, id) => {
140
+ lines.push(`\n**Account: ${id}**\n• Status: ${r.connectionStatus}\n• Jobs: ${r.stats.received}/${r.stats.completed}/${r.stats.failed}`);
141
+ });
142
+ return { text: lines.join('\n') };
143
+ }
144
+ });
145
+
146
+ registerCli(api, globalStats);
147
+ logger.info('[pier-connector] Plugin registered');
148
+ };
149
+
150
+ export default definePluginEntry({
151
+ id: 'pier-connector',
152
+ name: 'Pier Connector',
153
+ description: 'Connects OpenClaw to the Pier job marketplace.',
154
+ register
155
+ });
@@ -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
+ }