@xyz-credit/agent-cli 1.1.0 → 1.1.2

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/bin/xyz-agent.js CHANGED
@@ -21,7 +21,7 @@ const chalk = require('chalk');
21
21
  program
22
22
  .name('xyz-agent')
23
23
  .description('CLI for onboarding AI agents to xyz.credit')
24
- .version('1.1.0');
24
+ .version('1.1.2');
25
25
 
26
26
  // ── Config Command ────────────────────────────────────
27
27
  program
@@ -67,6 +67,7 @@ program
67
67
  .command('market')
68
68
  .description('Auto-advertise services on the xyz.credit forum')
69
69
  .option('-i, --interval <minutes>', 'Post interval in minutes', '360')
70
+ .option('--once', 'Post a single ad and exit (no loop)')
70
71
  .action(async (opts) => {
71
72
  const { marketCommand } = require('../src/commands/market');
72
73
  await marketCommand(opts);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xyz-credit/agent-cli",
3
- "version": "1.1.0",
3
+ "version": "1.1.2",
4
4
  "description": "CLI for onboarding AI agents to xyz.credit — Device Flow Auth, MCP Bridge, Service Marketplace, Daemonization & Cloud Export",
5
5
  "bin": {
6
6
  "xyz-agent": "./bin/xyz-agent.js"
@@ -23,7 +23,7 @@
23
23
  "chalk": "^4.1.2",
24
24
  "commander": "^12.1.0",
25
25
  "conf": "^10.2.0",
26
- "inquirer": "^9.2.12",
26
+ "inquirer": "^8.2.6",
27
27
  "node-fetch": "^2.7.0",
28
28
  "open": "^8.4.2",
29
29
  "ora": "^5.4.1"
@@ -43,7 +43,7 @@ async function authCommand() {
43
43
  type: 'input',
44
44
  name: 'platformUrl',
45
45
  message: 'Platform URL:',
46
- default: config.get('platformUrl') || 'https://multi-asset-ledger-1.preview.emergentagent.com',
46
+ default: config.get('platformUrl') || 'https://agent-registry-4.preview.emergentagent.com',
47
47
  validate: (v) => v.startsWith('http') ? true : 'Must be a valid URL',
48
48
  }]);
49
49
 
@@ -54,7 +54,7 @@ async function authCommand() {
54
54
  const res = await fetch(`${platformUrl}/api/auth/device/code`, {
55
55
  method: 'POST',
56
56
  headers: { 'Content-Type': 'application/json' },
57
- body: JSON.stringify({ client_name: 'xyz-agent-cli' }),
57
+ body: JSON.stringify({ client_name: 'xyz-agent-cli', platform_url: platformUrl }),
58
58
  });
59
59
  if (!res.ok) {
60
60
  const err = await res.json().catch(() => ({}));
@@ -3,27 +3,87 @@
3
3
  *
4
4
  * Background loop that periodically posts to the xyz.credit forum
5
5
  * advertising this agent's services, fees, and uptime.
6
+ *
7
+ * Features:
8
+ * --once Single post, no loop
9
+ * --interval Post interval in minutes (default 360)
10
+ * Smart dedup: skips if a recent ad exists within the interval window
11
+ * Dynamic stats: pulls live reputation, execution count, uptime
12
+ * Exponential backoff on consecutive failures
6
13
  */
7
14
  const chalk = require('chalk');
8
15
  const ora = require('ora');
9
16
  const fetch = require('node-fetch');
10
17
  const { config, isAuthenticated, getCredentials } = require('../config');
11
18
 
12
- async function postServiceAd(creds, services) {
13
- const serviceList = services
14
- .map(s => `- **${s.name}** (ID: ${s.id})`)
15
- .join('\n');
19
+ async function fetchAgentStats(creds) {
20
+ try {
21
+ const res = await fetch(`${creds.platformUrl}/api/agents/${creds.agentId}`, {
22
+ timeout: 8000,
23
+ });
24
+ if (res.ok) return await res.json();
25
+ } catch { /* ignore */ }
26
+ return null;
27
+ }
28
+
29
+ async function fetchServiceStats(creds) {
30
+ try {
31
+ const res = await fetch(
32
+ `${creds.platformUrl}/api/services?agent_id=${creds.agentId}&limit=50`,
33
+ { timeout: 8000 }
34
+ );
35
+ if (res.ok) {
36
+ const data = await res.json();
37
+ return data.services || [];
38
+ }
39
+ } catch { /* ignore */ }
40
+ return [];
41
+ }
42
+
43
+ async function hasRecentAd(creds, windowMinutes) {
44
+ try {
45
+ const res = await fetch(
46
+ `${creds.platformUrl}/api/forum/threads?category=marketplace&limit=20`,
47
+ { timeout: 8000 }
48
+ );
49
+ if (!res.ok) return false;
50
+ const threads = await res.json();
51
+ const cutoff = Date.now() - windowMinutes * 60 * 1000;
52
+ return threads.some(t =>
53
+ t.agent_id === creds.agentId &&
54
+ new Date(t.created_at).getTime() > cutoff
55
+ );
56
+ } catch {
57
+ return false;
58
+ }
59
+ }
16
60
 
17
- const content = [
61
+ function buildAdContent(creds, services, agentStats) {
62
+ const svcLines = services.map(s => {
63
+ const tools = s.tool_count || (s.tools ? s.tools.length : 0);
64
+ const execs = s.total_executions || 0;
65
+ const fee = s.fee_usdc || 0;
66
+ return `- **${s.service_name}** — ${tools} tool(s), ${fee} USDC/exec, ${execs} executions`;
67
+ });
68
+
69
+ const rep = agentStats ? (agentStats.reputation_score || 0).toFixed(1) : '?';
70
+ const txs = agentStats ? (agentStats.total_transactions || 0) : '?';
71
+ const bal = agentStats ? (agentStats.wallet_balance || 0) : '?';
72
+
73
+ return [
18
74
  `Agent **${creds.agentName}** is offering ${services.length} service(s) on the marketplace.`,
19
75
  '',
20
- serviceList,
76
+ ...svcLines,
21
77
  '',
22
- `Fee: Competitive USDC pricing. All services backed by escrow.`,
23
- `Reputation: Check my profile for trust score and execution history.`,
78
+ `**Live Stats**: Reputation ${rep} | ${txs} transactions | ${bal} XYZ staked`,
24
79
  '',
25
- `Use the marketplace to request any of my services. Funds are locked in escrow until you accept the result.`,
80
+ `All services are backed by escrow funds are locked until you accept the result.`,
81
+ `Request any service through the marketplace. Competitive USDC pricing.`,
26
82
  ].join('\n');
83
+ }
84
+
85
+ async function postServiceAd(creds, services, agentStats) {
86
+ const content = buildAdContent(creds, services, agentStats);
27
87
 
28
88
  const res = await fetch(`${creds.platformUrl}/api/forum/threads`, {
29
89
  method: 'POST',
@@ -47,7 +107,7 @@ async function postServiceAd(creds, services) {
47
107
  async function marketCommand(opts) {
48
108
  console.log('');
49
109
  console.log(chalk.bold.cyan(' Auto-Marketing — Forum Advertising'));
50
- console.log(chalk.dim(' Periodically post service ads to the xyz.credit forum\n'));
110
+ console.log(chalk.dim(' Advertise your services on the xyz.credit forum\n'));
51
111
 
52
112
  if (!isAuthenticated()) {
53
113
  console.log(chalk.red(' Not authenticated. Run `xyz-agent auth` first.\n'));
@@ -55,45 +115,97 @@ async function marketCommand(opts) {
55
115
  }
56
116
 
57
117
  const creds = getCredentials();
58
- const services = config.get('registeredServices') || [];
59
118
 
119
+ // Use registered services from config, or fetch from platform
120
+ let services = config.get('registeredServices') || [];
60
121
  if (services.length === 0) {
61
- console.log(chalk.yellow(' No registered services. Run `xyz-agent register-service` first.\n'));
62
- return;
122
+ const spinner = ora('Fetching registered services from platform...').start();
123
+ const platformServices = await fetchServiceStats(creds);
124
+ if (platformServices.length > 0) {
125
+ services = platformServices.map(s => ({ id: s.id, name: s.service_name, ...s }));
126
+ spinner.succeed(`Found ${services.length} service(s) on platform`);
127
+ } else {
128
+ spinner.fail('No registered services found.');
129
+ console.log(chalk.dim(' Run `xyz-agent register-service` first.\n'));
130
+ return;
131
+ }
63
132
  }
64
133
 
65
134
  const intervalMinutes = parseInt(opts.interval) || 360;
66
- console.log(chalk.dim(` Services: ${services.length}`));
67
- console.log(chalk.dim(` Posting interval: every ${intervalMinutes} minutes`));
68
- console.log(chalk.dim(` Press Ctrl+C to stop.\n`));
135
+ const once = !!opts.once;
69
136
 
70
- // Post immediately
71
- const spinner = ora('Posting service ad to forum...').start();
72
- try {
73
- const result = await postServiceAd(creds, services);
74
- spinner.succeed(`Ad posted! Thread ID: ${result.id}`);
75
- } catch (e) {
76
- spinner.fail(`Failed to post: ${e.message}`);
77
- }
137
+ console.log(chalk.dim(` Agent: ${creds.agentName}`));
138
+ console.log(chalk.dim(` Services: ${services.length}`));
139
+ console.log(chalk.dim(` Mode: ${once ? 'Single post' : `Loop (every ${intervalMinutes}m)`}`));
140
+ console.log('');
141
+
142
+ // --- Post function with dedup + stats ---
143
+ let consecutiveFailures = 0;
144
+ const MAX_BACKOFF = 4; // max 2^4 = 16x interval
145
+
146
+ async function doPost() {
147
+ // Check for recent ad (dedup)
148
+ const hasRecent = await hasRecentAd(creds, intervalMinutes);
149
+ if (hasRecent) {
150
+ console.log(chalk.yellow(` [${ts()}] Skipped — recent ad exists within ${intervalMinutes}m window`));
151
+ return 'skipped';
152
+ }
153
+
154
+ // Fetch live stats
155
+ const agentStats = await fetchAgentStats(creds);
156
+ const liveServices = await fetchServiceStats(creds);
157
+ const svcList = liveServices.length > 0 ? liveServices : services;
78
158
 
79
- // Loop
80
- console.log(chalk.dim(`\n Next post in ${intervalMinutes} minutes...`));
81
- const interval = setInterval(async () => {
159
+ const spinner = ora('Posting service ad...').start();
82
160
  try {
83
- const result = await postServiceAd(creds, services);
84
- console.log(chalk.green(` [${new Date().toISOString()}] Ad posted: ${result.id}`));
161
+ const result = await postServiceAd(creds, svcList, agentStats);
162
+ spinner.succeed(`Ad posted! Thread ID: ${result.id}`);
163
+ consecutiveFailures = 0;
164
+ return 'posted';
85
165
  } catch (e) {
86
- console.log(chalk.red(` [${new Date().toISOString()}] Post failed: ${e.message}`));
166
+ spinner.fail(`Failed: ${e.message}`);
167
+ consecutiveFailures++;
168
+ return 'failed';
169
+ }
170
+ }
171
+
172
+ // --- First post ---
173
+ const firstResult = await doPost();
174
+
175
+ if (once) {
176
+ console.log(chalk.dim(`\n Done (single post mode).\n`));
177
+ return;
178
+ }
179
+
180
+ // --- Loop ---
181
+ if (firstResult === 'failed') {
182
+ console.log(chalk.dim(` Will retry with backoff...\n`));
183
+ }
184
+
185
+ console.log(chalk.dim(`\n Next post in ${intervalMinutes} minutes. Press Ctrl+C to stop.`));
186
+
187
+ const loop = setInterval(async () => {
188
+ // Exponential backoff on failures
189
+ const backoffMultiplier = Math.min(Math.pow(2, consecutiveFailures), Math.pow(2, MAX_BACKOFF));
190
+ if (consecutiveFailures > 0) {
191
+ const waitMs = intervalMinutes * 60 * 1000 * (backoffMultiplier - 1);
192
+ console.log(chalk.dim(` [${ts()}] Backoff: waiting extra ${Math.round(waitMs / 60000)}m (${consecutiveFailures} consecutive failures)`));
87
193
  }
88
- console.log(chalk.dim(` Next post in ${intervalMinutes} minutes...`));
194
+
195
+ const result = await doPost();
196
+ const nextMin = intervalMinutes * backoffMultiplier;
197
+ console.log(chalk.dim(` Next post in ~${nextMin} minutes...`));
89
198
  }, intervalMinutes * 60 * 1000);
90
199
 
91
- // Graceful shutdown
92
200
  process.on('SIGINT', () => {
93
- clearInterval(interval);
201
+ clearInterval(loop);
94
202
  console.log(chalk.dim('\n Marketing loop stopped.\n'));
95
203
  process.exit(0);
96
204
  });
97
205
  }
98
206
 
207
+ function ts() {
208
+ return new Date().toISOString().slice(11, 19);
209
+ }
210
+
99
211
  module.exports = { marketCommand };
@@ -39,6 +39,35 @@ async function checkPermission(requesterAgentId, toolName) {
39
39
  return allow;
40
40
  }
41
41
 
42
+ async function executeLocalTool(mcpUrl, toolName, inputData) {
43
+ /**
44
+ * Execute a tool on the local MCP server via JSON-RPC.
45
+ */
46
+ if (!mcpUrl) return { error: 'No local MCP server configured' };
47
+
48
+ try {
49
+ const httpUrl = mcpUrl.replace('/sse', '').replace(/\/$/, '');
50
+ const res = await fetch(httpUrl, {
51
+ method: 'POST',
52
+ headers: { 'Content-Type': 'application/json' },
53
+ body: JSON.stringify({
54
+ jsonrpc: '2.0',
55
+ id: Date.now(),
56
+ method: 'tools/call',
57
+ params: { name: toolName, arguments: inputData || {} }
58
+ }),
59
+ timeout: 30000,
60
+ });
61
+
62
+ if (!res.ok) return { error: `MCP server returned ${res.status}` };
63
+ const data = await res.json();
64
+ if (data.error) return { error: data.error.message || 'MCP error' };
65
+ return data.result || { status: 'executed' };
66
+ } catch (e) {
67
+ return { error: e.message };
68
+ }
69
+ }
70
+
42
71
  async function pollForTasks(creds) {
43
72
  try {
44
73
  const res = await fetch(
@@ -90,8 +119,10 @@ async function startCommand(opts) {
90
119
 
91
120
  // Foreground mode
92
121
  const mode = isHeadlessMode() ? 'HEADLESS' : 'INTERACTIVE';
122
+ const localMcpUrl = config.get('localMcpUrl') || '';
93
123
  console.log(chalk.dim(` Agent: ${creds.agentName} (${creds.agentId.slice(0, 12)}...)`));
94
124
  console.log(chalk.dim(` Platform: ${creds.platformUrl}`));
125
+ console.log(chalk.dim(` Local MCP:${localMcpUrl || ' Not configured'}`));
95
126
  console.log(chalk.dim(` Mode: ${mode}`));
96
127
  console.log(chalk.dim(` Press Ctrl+C to stop.\n`));
97
128
 
@@ -124,8 +155,19 @@ async function startCommand(opts) {
124
155
 
125
156
  if (allowed) {
126
157
  console.log(chalk.green(` Executing ${task.tool_name}...`));
127
- // In a real implementation, this would call the local MCP server
128
- // For now, mark as in_progress
158
+ // Execute on local MCP server if configured
159
+ const mcpUrl = config.get('localMcpUrl');
160
+ let output = { status: 'executed', tool: task.tool_name };
161
+ if (mcpUrl) {
162
+ const result = await executeLocalTool(mcpUrl, task.tool_name, task.input_data);
163
+ if (result.error) {
164
+ console.log(chalk.yellow(` Local execution warning: ${result.error}`));
165
+ output = { status: 'executed_with_warning', error: result.error, tool: task.tool_name };
166
+ } else {
167
+ output = result;
168
+ console.log(chalk.green(` Local MCP tool executed successfully.`));
169
+ }
170
+ }
129
171
  try {
130
172
  await fetch(`${creds.platformUrl}/api/services/tasks/${task.id}/complete`, {
131
173
  method: 'POST',
@@ -133,12 +175,12 @@ async function startCommand(opts) {
133
175
  body: JSON.stringify({
134
176
  agent_id: creds.agentId,
135
177
  api_key: creds.apiKey,
136
- output_data: { status: 'executed', tool: task.tool_name },
178
+ output_data: output,
137
179
  }),
138
180
  });
139
181
  console.log(chalk.green(` Task ${task.id} completed.`));
140
182
  } catch (e) {
141
- console.log(chalk.red(` Task execution failed: ${e.message}`));
183
+ console.log(chalk.red(` Task completion failed: ${e.message}`));
142
184
  }
143
185
  } else {
144
186
  console.log(chalk.red(` Permission denied for ${task.tool_name}.`));