@obolos_tech/cli 0.3.3 → 0.5.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.
Files changed (72) hide show
  1. package/README.md +35 -1
  2. package/dist/commands/anp.d.ts +277 -0
  3. package/dist/commands/anp.js +440 -0
  4. package/dist/commands/anp.js.map +1 -0
  5. package/dist/commands/index.d.ts +3 -0
  6. package/dist/commands/index.js +34 -0
  7. package/dist/commands/index.js.map +1 -0
  8. package/dist/commands/jobs.d.ts +111 -0
  9. package/dist/commands/jobs.js +294 -0
  10. package/dist/commands/jobs.js.map +1 -0
  11. package/dist/commands/listings.d.ts +128 -0
  12. package/dist/commands/listings.js +246 -0
  13. package/dist/commands/listings.js.map +1 -0
  14. package/dist/commands/marketplace.d.ts +87 -0
  15. package/dist/commands/marketplace.js +133 -0
  16. package/dist/commands/marketplace.js.map +1 -0
  17. package/dist/commands/reputation.d.ts +27 -0
  18. package/dist/commands/reputation.js +114 -0
  19. package/dist/commands/reputation.js.map +1 -0
  20. package/dist/commands/setup.d.ts +78 -0
  21. package/dist/commands/setup.js +133 -0
  22. package/dist/commands/setup.js.map +1 -0
  23. package/dist/commands/wallet.d.ts +30 -0
  24. package/dist/commands/wallet.js +66 -0
  25. package/dist/commands/wallet.js.map +1 -0
  26. package/dist/index.d.ts +4 -24
  27. package/dist/index.js +61 -2516
  28. package/dist/index.js.map +1 -1
  29. package/dist/registry.d.ts +53 -0
  30. package/dist/registry.js +39 -0
  31. package/dist/registry.js.map +1 -0
  32. package/dist/runtime/acp.d.ts +162 -0
  33. package/dist/runtime/acp.js +132 -0
  34. package/dist/runtime/acp.js.map +1 -0
  35. package/dist/runtime/anp.d.ts +214 -0
  36. package/dist/runtime/anp.js +255 -0
  37. package/dist/runtime/anp.js.map +1 -0
  38. package/dist/runtime/argv.d.ts +18 -0
  39. package/dist/runtime/argv.js +114 -0
  40. package/dist/runtime/argv.js.map +1 -0
  41. package/dist/runtime/config.d.ts +14 -0
  42. package/dist/runtime/config.js +34 -0
  43. package/dist/runtime/config.js.map +1 -0
  44. package/dist/runtime/dispatch.d.ts +13 -0
  45. package/dist/runtime/dispatch.js +123 -0
  46. package/dist/runtime/dispatch.js.map +1 -0
  47. package/dist/runtime/display.d.ts +21 -0
  48. package/dist/runtime/display.js +68 -0
  49. package/dist/runtime/display.js.map +1 -0
  50. package/dist/runtime/errors.d.ts +19 -0
  51. package/dist/runtime/errors.js +23 -0
  52. package/dist/runtime/errors.js.map +1 -0
  53. package/dist/runtime/http.d.ts +9 -0
  54. package/dist/runtime/http.js +36 -0
  55. package/dist/runtime/http.js.map +1 -0
  56. package/dist/runtime/output.d.ts +19 -0
  57. package/dist/runtime/output.js +12 -0
  58. package/dist/runtime/output.js.map +1 -0
  59. package/dist/runtime/payment.d.ts +21 -0
  60. package/dist/runtime/payment.js +91 -0
  61. package/dist/runtime/payment.js.map +1 -0
  62. package/dist/runtime/wallet.d.ts +32 -0
  63. package/dist/runtime/wallet.js +44 -0
  64. package/dist/runtime/wallet.js.map +1 -0
  65. package/dist/schema/json-schema.d.ts +10 -0
  66. package/dist/schema/json-schema.js +34 -0
  67. package/dist/schema/json-schema.js.map +1 -0
  68. package/dist/schema/zod-shape.d.ts +9 -0
  69. package/dist/schema/zod-shape.js +36 -0
  70. package/dist/schema/zod-shape.js.map +1 -0
  71. package/package.json +46 -4
  72. package/scripts/lint-stdout-purity.mjs +62 -0
package/dist/index.js CHANGED
@@ -1,2546 +1,91 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * Obolos CLI
3
+ * Obolos CLI entrypoint.
4
4
  *
5
- * Search, browse, and call x402 APIs from the terminal.
6
- *
7
- * npx @obolos_tech/cli search "token price"
8
- * npx @obolos_tech/cli info ext-abc123
9
- * npx @obolos_tech/cli call ext-abc123 --body '{"symbol":"ETH"}'
10
- * npx @obolos_tech/cli categories
11
- * npx @obolos_tech/cli balance
12
- * npx @obolos_tech/cli setup-mcp
13
- * npx @obolos_tech/cli job list --status=open
14
- * npx @obolos_tech/cli job create --title "..." --evaluator 0x...
15
- * npx @obolos_tech/cli job info <id>
16
- * npx @obolos_tech/cli listing list --status=open
17
- * npx @obolos_tech/cli listing create --title "..." --max-budget 10.00
18
- * npx @obolos_tech/cli listing bid <id> --price 5.00
19
- * npx @obolos_tech/cli anp list --status=open
20
- * npx @obolos_tech/cli anp create --title "..." --min-budget 5 --max-budget 50
21
- * npx @obolos_tech/cli anp bid <cid> --price 25 --delivery 48h
22
- * npx @obolos_tech/cli anp accept <cid> --bid <bid_cid>
23
- * npx @obolos_tech/cli anp verify <cid>
24
- * npx @obolos_tech/cli reputation check 16907
25
- * npx @obolos_tech/cli reputation check 16907 --chain ethereum
26
- * npx @obolos_tech/cli reputation compare 123 456 789
27
- * npx @obolos_tech/cli rep compare base:123 ethereum:456
5
+ * All commands live in `cli/src/commands/` and are registered in
6
+ * `cli/src/commands/index.ts`. This file only dispatches argv and renders
7
+ * top-level help. See `cli/src/registry.ts` for the Command contract.
28
8
  */
29
- import { homedir } from 'os';
30
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
31
- import { join } from 'path';
32
- import { createInterface } from 'readline';
33
- const CONFIG_DIR = join(homedir(), '.obolos');
34
- const CONFIG_FILE = join(CONFIG_DIR, 'config.json');
35
- function loadConfig() {
36
- try {
37
- if (existsSync(CONFIG_FILE)) {
38
- return JSON.parse(readFileSync(CONFIG_FILE, 'utf-8'));
39
- }
40
- }
41
- catch { }
42
- return {};
43
- }
44
- function saveConfig(config) {
45
- if (!existsSync(CONFIG_DIR)) {
46
- mkdirSync(CONFIG_DIR, { mode: 0o700 });
47
- }
48
- writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2) + '\n', { mode: 0o600 });
49
- }
50
- const config = loadConfig();
51
- const OBOLOS_API_URL = process.env.OBOLOS_API_URL || config.api_url || 'https://obolos.tech';
52
- const OBOLOS_PRIVATE_KEY = process.env.OBOLOS_PRIVATE_KEY || config.private_key || '';
53
- // ─── Colors (no deps) ──────────────────────────────────────────────────────
54
- // ─── ACP Contract ABIs (ERC-8183) ─────────────────────────────────────────
55
- const ACP_ADDRESS = '0xaF3148696242F7Fb74893DC47690e37950807362';
56
- const USDC_CONTRACT = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913';
57
- const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000';
58
- const ZERO_BYTES32 = '0x0000000000000000000000000000000000000000000000000000000000000000';
59
- const ACP_ABI = [
60
- {
61
- type: 'function',
62
- name: 'createJob',
63
- inputs: [
64
- { name: 'provider', type: 'address' },
65
- { name: 'evaluator', type: 'address' },
66
- { name: 'expiredAt', type: 'uint256' },
67
- { name: 'description', type: 'string' },
68
- { name: 'hook', type: 'address' },
69
- ],
70
- outputs: [{ name: 'jobId', type: 'uint256' }],
71
- stateMutability: 'nonpayable',
72
- },
73
- {
74
- type: 'function',
75
- name: 'fund',
76
- inputs: [
77
- { name: 'jobId', type: 'uint256' },
78
- { name: 'expectedBudget', type: 'uint256' },
79
- { name: 'optParams', type: 'bytes' },
80
- ],
81
- outputs: [],
82
- stateMutability: 'nonpayable',
83
- },
84
- {
85
- type: 'function',
86
- name: 'submit',
87
- inputs: [
88
- { name: 'jobId', type: 'uint256' },
89
- { name: 'deliverable', type: 'bytes32' },
90
- { name: 'optParams', type: 'bytes' },
91
- ],
92
- outputs: [],
93
- stateMutability: 'nonpayable',
94
- },
95
- {
96
- type: 'function',
97
- name: 'complete',
98
- inputs: [
99
- { name: 'jobId', type: 'uint256' },
100
- { name: 'reason', type: 'bytes32' },
101
- { name: 'optParams', type: 'bytes' },
102
- ],
103
- outputs: [],
104
- stateMutability: 'nonpayable',
105
- },
106
- {
107
- type: 'function',
108
- name: 'reject',
109
- inputs: [
110
- { name: 'jobId', type: 'uint256' },
111
- { name: 'reason', type: 'bytes32' },
112
- { name: 'optParams', type: 'bytes' },
113
- ],
114
- outputs: [],
115
- stateMutability: 'nonpayable',
116
- },
117
- {
118
- type: 'event',
119
- name: 'JobCreated',
120
- inputs: [
121
- { name: 'jobId', type: 'uint256', indexed: true },
122
- { name: 'client', type: 'address', indexed: true },
123
- { name: 'provider', type: 'address', indexed: false },
124
- { name: 'evaluator', type: 'address', indexed: false },
125
- { name: 'expiredAt', type: 'uint256', indexed: false },
126
- ],
127
- },
128
- ];
129
- const ERC20_ABI = [
130
- {
131
- type: 'function',
132
- name: 'approve',
133
- inputs: [
134
- { name: 'spender', type: 'address' },
135
- { name: 'amount', type: 'uint256' },
136
- ],
137
- outputs: [{ name: '', type: 'bool' }],
138
- stateMutability: 'nonpayable',
139
- },
140
- {
141
- type: 'function',
142
- name: 'allowance',
143
- inputs: [
144
- { name: 'owner', type: 'address' },
145
- { name: 'spender', type: 'address' },
146
- ],
147
- outputs: [{ name: '', type: 'uint256' }],
148
- stateMutability: 'view',
149
- },
150
- ];
9
+ import { dispatch } from './runtime/dispatch.js';
10
+ import { registry } from './commands/index.js';
11
+ const args = process.argv.slice(2);
12
+ const command = args[0];
13
+ const commandArgs = args.slice(1);
151
14
  const c = {
152
- reset: '\x1b[0m',
153
- bold: '\x1b[1m',
154
- dim: '\x1b[2m',
155
- green: '\x1b[32m',
156
- yellow: '\x1b[33m',
157
- blue: '\x1b[34m',
158
- magenta: '\x1b[35m',
159
- cyan: '\x1b[36m',
160
- red: '\x1b[31m',
161
- white: '\x1b[37m',
162
- gray: '\x1b[90m',
15
+ reset: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m',
16
+ cyan: '\x1b[36m', red: '\x1b[31m', yellow: '\x1b[33m',
163
17
  };
164
- function formatPrice(price) {
165
- return `${c.green}$${price.toFixed(4)}${c.reset} USDC`;
166
- }
167
- // ─── API Client ─────────────────────────────────────────────────────────────
168
- async function apiGet(path) {
169
- const res = await fetch(`${OBOLOS_API_URL}${path}`);
170
- if (!res.ok)
171
- throw new Error(`${res.status} ${res.statusText}`);
172
- return res.json();
173
- }
174
- async function apiPost(path, body, headers) {
175
- const res = await fetch(`${OBOLOS_API_URL}${path}`, {
176
- method: 'POST',
177
- headers: { 'Content-Type': 'application/json', ...headers },
178
- body: body !== undefined ? JSON.stringify(body) : undefined,
179
- });
180
- if (!res.ok) {
181
- let msg = `${res.status} ${res.statusText}`;
182
- try {
183
- const err = await res.json();
184
- if (err.error)
185
- msg = err.error;
186
- else if (err.message)
187
- msg = err.message;
188
- }
189
- catch { }
190
- throw new Error(msg);
191
- }
192
- return res.json();
193
- }
194
- // ─── Helpers ────────────────────────────────────────────────────────────────
195
- function getFlag(args, name) {
196
- // Supports --name=value and --name value
197
- for (let i = 0; i < args.length; i++) {
198
- if (args[i] === `--${name}` && args[i + 1] && !args[i + 1].startsWith('--')) {
199
- return args[i + 1];
200
- }
201
- if (args[i].startsWith(`--${name}=`)) {
202
- return args[i].slice(`--${name}=`.length);
203
- }
204
- }
205
- return undefined;
206
- }
207
- function getPositional(args, index) {
208
- let pos = 0;
209
- for (const arg of args) {
210
- if (!arg.startsWith('--')) {
211
- if (pos === index)
212
- return arg;
213
- pos++;
214
- }
215
- }
216
- return undefined;
217
- }
218
- function shortenAddr(addr) {
219
- if (!addr)
220
- return `${c.dim}—${c.reset}`;
221
- if (addr.length <= 12)
222
- return addr;
223
- return `${addr.slice(0, 6)}...${addr.slice(-4)}`;
224
- }
225
- function shortenId(id) {
226
- if (id.length <= 12)
227
- return id;
228
- return `${id.slice(0, 8)}...`;
229
- }
230
- function formatDate(iso) {
231
- if (!iso)
232
- return `${c.dim}—${c.reset}`;
233
- const d = new Date(iso);
234
- return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
235
- }
236
- function statusColor(status) {
237
- switch (status) {
238
- case 'open': return `${c.yellow}${status}${c.reset}`;
239
- case 'funded': return `${c.blue}${status}${c.reset}`;
240
- case 'submitted': return `${c.cyan}${status}${c.reset}`;
241
- case 'completed': return `${c.green}${status}${c.reset}`;
242
- case 'rejected': return `${c.red}${status}${c.reset}`;
243
- case 'expired': return `${c.gray}${status}${c.reset}`;
244
- case 'negotiating': return `${c.magenta}${status}${c.reset}`;
245
- case 'accepted': return `${c.green}${status}${c.reset}`;
246
- case 'cancelled': return `${c.gray}${status}${c.reset}`;
247
- default: return status;
248
- }
249
- }
250
- function parseRelativeTime(input) {
251
- const match = input.match(/^(\d+)\s*(h|hr|hrs|hour|hours|d|day|days|m|min|mins|minute|minutes)$/i);
252
- if (!match) {
253
- // Try parsing as ISO date directly
254
- const d = new Date(input);
255
- if (!isNaN(d.getTime()))
256
- return d.toISOString();
257
- throw new Error(`Cannot parse expiry: "${input}". Use formats like "24h", "7d", "1h", or an ISO date.`);
258
- }
259
- const num = parseInt(match[1], 10);
260
- const unit = match[2].toLowerCase();
261
- const now = Date.now();
262
- let ms = 0;
263
- if (unit.startsWith('h'))
264
- ms = num * 60 * 60 * 1000;
265
- else if (unit.startsWith('d'))
266
- ms = num * 24 * 60 * 60 * 1000;
267
- else if (unit.startsWith('m'))
268
- ms = num * 60 * 1000;
269
- return new Date(now + ms).toISOString();
270
- }
271
- function resolveWalletAddress() {
272
- const cfg = loadConfig();
273
- const addr = cfg.wallet_address;
274
- if (addr)
275
- return addr;
276
- // Derive from private key if available
277
- if (!OBOLOS_PRIVATE_KEY) {
278
- console.error(`${c.red}No wallet configured.${c.reset} Run ${c.cyan}obolos setup${c.reset} first.`);
279
- process.exit(1);
280
- }
281
- // We'll derive it lazily — for now return empty and let callers handle async derivation
282
- return '';
283
- }
284
- async function getWalletAddress() {
285
- const cfg = loadConfig();
286
- if (cfg.wallet_address)
287
- return cfg.wallet_address;
288
- if (!OBOLOS_PRIVATE_KEY) {
289
- console.error(`${c.red}No wallet configured.${c.reset} Run ${c.cyan}obolos setup${c.reset} first.`);
290
- process.exit(1);
291
- }
292
- const { privateKeyToAccount } = await import('viem/accounts');
293
- const key = OBOLOS_PRIVATE_KEY.startsWith('0x') ? OBOLOS_PRIVATE_KEY : `0x${OBOLOS_PRIVATE_KEY}`;
294
- const account = privateKeyToAccount(key);
295
- return account.address;
296
- }
297
- async function getACPClient() {
298
- const key = OBOLOS_PRIVATE_KEY;
299
- if (!key) {
300
- console.error(`${c.red}No wallet configured.${c.reset} Run ${c.cyan}obolos setup${c.reset} first.`);
301
- process.exit(1);
302
- }
303
- const { createPublicClient, createWalletClient, http: viemHttp, parseUnits, keccak256, toHex, decodeEventLog } = await import('viem');
304
- const { privateKeyToAccount } = await import('viem/accounts');
305
- const { base } = await import('viem/chains');
306
- const normalizedKey = key.startsWith('0x') ? key : `0x${key}`;
307
- const account = privateKeyToAccount(normalizedKey);
308
- const publicClient = createPublicClient({ chain: base, transport: viemHttp() });
309
- const walletClient = createWalletClient({ account, chain: base, transport: viemHttp() });
310
- return { account, publicClient, walletClient, parseUnits, keccak256, toHex, decodeEventLog };
311
- }
312
- function stateVisualization(status) {
313
- const states = ['open', 'funded', 'submitted', 'completed'];
314
- const parts = states.map(s => {
315
- if (s === status)
316
- return `${c.bold}[${s.charAt(0).toUpperCase() + s.slice(1)}]${c.reset}`;
317
- return `${c.dim}${s.charAt(0).toUpperCase() + s.slice(1)}${c.reset}`;
318
- });
319
- // Handle terminal states that branch off
320
- if (status === 'rejected') {
321
- const base = states.slice(0, 3).map(s => `${c.dim}${s.charAt(0).toUpperCase() + s.slice(1)}${c.reset}`);
322
- return ` ${base.join(` ${c.dim}->${c.reset} `)} ${c.dim}->${c.reset} ${c.bold}${c.red}[Rejected]${c.reset}`;
323
- }
324
- if (status === 'expired') {
325
- return ` ${c.bold}${c.gray}[Expired]${c.reset}`;
326
- }
327
- return ` ${parts.join(` ${c.dim}->${c.reset} `)}`;
328
- }
329
- // ─── ANP Helpers ─────────────────────────────────────────────────────────────
330
- import { computeContentHash, ANP_TYPES, getANPDomain, hashListingIntent, hashBidIntent, hashAcceptIntent } from '@obolos_tech/anp-sdk';
331
- function parseTimeToSeconds(input) {
332
- const match = input.match(/^(\d+)\s*(s|sec|secs|second|seconds|h|hr|hrs|hour|hours|d|day|days|m|min|mins|minute|minutes)$/i);
333
- if (!match) {
334
- throw new Error(`Cannot parse time: "${input}". Use formats like "48h", "7d", "3d".`);
335
- }
336
- const num = parseInt(match[1], 10);
337
- const unit = match[2].toLowerCase();
338
- if (unit.startsWith('s'))
339
- return num;
340
- if (unit.startsWith('m'))
341
- return num * 60;
342
- if (unit.startsWith('h'))
343
- return num * 3600;
344
- if (unit.startsWith('d'))
345
- return num * 86400;
346
- return num;
347
- }
348
- const ANP_DOMAIN = getANPDomain(8453, '0xfEa362Bf569e97B20681289fB4D4a64CEBDFa792');
349
- async function getANPSigningClient() {
350
- if (!OBOLOS_PRIVATE_KEY) {
351
- console.error(`${c.red}No wallet configured.${c.reset} Run ${c.cyan}obolos setup${c.reset} first.`);
352
- process.exit(1);
353
- }
354
- const { createWalletClient, http: viemHttp } = await import('viem');
355
- const { privateKeyToAccount } = await import('viem/accounts');
356
- const { base } = await import('viem/chains');
357
- const key = OBOLOS_PRIVATE_KEY.startsWith('0x') ? OBOLOS_PRIVATE_KEY : `0x${OBOLOS_PRIVATE_KEY}`;
358
- const account = privateKeyToAccount(key);
359
- const walletClient = createWalletClient({ account, chain: base, transport: viemHttp() });
360
- return { account, walletClient, hashListingStruct: hashListingIntent, hashBidStruct: hashBidIntent, hashAcceptStruct: hashAcceptIntent };
361
- }
362
- function generateNonce() {
363
- return BigInt(Math.floor(Math.random() * 2 ** 32));
364
- }
365
- // ─── Commands ───────────────────────────────────────────────────────────────
366
- async function cmdSearch(args) {
367
- const query = args.join(' ');
368
- const params = new URLSearchParams();
369
- if (query)
370
- params.set('q', query);
371
- params.set('limit', '25');
372
- params.set('type', 'native');
373
- const data = await apiGet(`/api/marketplace/apis/search?${params}`);
374
- const apis = data.apis;
375
- if (apis.length === 0) {
376
- console.log(`${c.yellow}No APIs found${query ? ` for "${query}"` : ''}.${c.reset}`);
377
- return;
378
- }
379
- console.log(`\n${c.bold}${c.cyan}Obolos Marketplace${c.reset} ${c.dim}— ${data.pagination.total} APIs found${c.reset}\n`);
380
- for (const api of apis) {
381
- const name = (api.name || 'Unnamed').slice(0, 50);
382
- const price = `$${api.price_per_call.toFixed(4)}`;
383
- const cat = api.category;
384
- const id = api.id;
385
- console.log(` ${c.bold}${name}${c.reset}`);
386
- console.log(` ${c.green}${price}${c.reset} ${c.dim}${cat}${c.reset} ${c.cyan}${id}${c.reset}\n`);
387
- }
388
- console.log(`${c.dim}Use: obolos info <id> for details, or copy the full ID to call it${c.reset}\n`);
389
- }
390
- async function cmdCategories() {
391
- const data = await apiGet('/api/marketplace/categories');
392
- console.log(`\n${c.bold}${c.cyan}API Categories${c.reset}\n`);
393
- for (const cat of data.categories) {
394
- const bar = '█'.repeat(Math.min(50, Math.ceil(cat.count / 5)));
395
- console.log(` ${cat.name.padEnd(25)} ${c.green}${String(cat.count).padStart(4)}${c.reset} ${c.dim}${bar}${c.reset}`);
396
- }
397
- console.log(`\n ${c.bold}Total:${c.reset} ${data.nativeCount} native + ${data.externalCount} external\n`);
398
- }
399
- async function cmdInfo(args) {
400
- const id = args[0];
401
- if (!id) {
402
- console.error(`${c.red}Usage: obolos info <api-id>${c.reset}`);
403
- process.exit(1);
404
- }
405
- const api = await apiGet(`/api/marketplace/apis/${encodeURIComponent(id)}`);
406
- console.log(`\n${c.bold}${c.cyan}${api.name}${c.reset}`);
407
- console.log(`${c.dim}${'─'.repeat(60)}${c.reset}`);
408
- console.log(` ${c.bold}ID:${c.reset} ${api.id}`);
409
- console.log(` ${c.bold}Type:${c.reset} ${api.api_type}`);
410
- console.log(` ${c.bold}Price:${c.reset} ${formatPrice(api.price_per_call)}`);
411
- console.log(` ${c.bold}Method:${c.reset} ${api.http_method}`);
412
- console.log(` ${c.bold}Category:${c.reset} ${api.category}`);
413
- console.log(` ${c.bold}Seller:${c.reset} ${api.seller_name}`);
414
- console.log(` ${c.bold}Calls:${c.reset} ${api.total_calls}`);
415
- if (api.average_rating) {
416
- console.log(` ${c.bold}Rating:${c.reset} ${api.average_rating.toFixed(1)}/5 (${api.review_count} reviews)`);
417
- }
418
- if (api.description) {
419
- console.log(`\n ${c.bold}Description:${c.reset}`);
420
- console.log(` ${api.description}`);
421
- }
422
- if (api.input_schema?.fields && Object.keys(api.input_schema.fields).length > 0) {
423
- console.log(`\n ${c.bold}Input Fields:${c.reset}`);
424
- for (const [name, field] of Object.entries(api.input_schema.fields)) {
425
- const req = field.required ? `${c.red}*${c.reset}` : ' ';
426
- const ex = field.example ? `${c.dim}(e.g. ${JSON.stringify(field.example)})${c.reset}` : '';
427
- console.log(` ${req} ${c.cyan}${name}${c.reset}: ${field.type} ${ex}`);
428
- }
429
- }
430
- if (api.example_request) {
431
- console.log(`\n ${c.bold}Example Request:${c.reset}`);
432
- try {
433
- console.log(` ${c.dim}${JSON.stringify(JSON.parse(api.example_request), null, 2).replace(/\n/g, '\n ')}${c.reset}`);
434
- }
435
- catch {
436
- console.log(` ${c.dim}${api.example_request}${c.reset}`);
437
- }
438
- }
439
- if (api.example_response) {
440
- console.log(`\n ${c.bold}Example Response:${c.reset}`);
441
- try {
442
- const parsed = JSON.parse(api.example_response);
443
- const formatted = JSON.stringify(parsed, null, 2);
444
- // Truncate long responses
445
- const lines = formatted.split('\n');
446
- if (lines.length > 20) {
447
- console.log(` ${c.dim}${lines.slice(0, 20).join('\n ')}\n ... (${lines.length - 20} more lines)${c.reset}`);
448
- }
449
- else {
450
- console.log(` ${c.dim}${formatted.replace(/\n/g, '\n ')}${c.reset}`);
451
- }
452
- }
453
- catch {
454
- console.log(` ${c.dim}${api.example_response.slice(0, 500)}${c.reset}`);
455
- }
456
- }
457
- console.log(`\n ${c.bold}Call:${c.reset} obolos call ${api.id}${api.http_method === 'POST' ? " --body '{...}'" : ''}`);
458
- console.log(` ${c.bold}Proxy:${c.reset} ${OBOLOS_API_URL}/api/proxy/${api.id}`);
459
- if (api.slug) {
460
- console.log(` ${c.bold}Slug:${c.reset} ${OBOLOS_API_URL}/api/${api.slug}`);
461
- }
462
- console.log(`\n ${c.dim}Note: Always use the full URL above. Do NOT call /${api.slug || api.id} directly — the /api/proxy/ or /api/ prefix is required.${c.reset}\n`);
463
- }
464
- async function cmdCall(args) {
465
- const id = args[0];
466
- if (!id) {
467
- console.error(`${c.red}Usage: obolos call <api-id> [--body '{"key":"value"}'] [--method POST]${c.reset}`);
468
- process.exit(1);
469
- }
470
- // Parse flags
471
- let method = 'GET';
472
- let body = undefined;
473
- for (let i = 1; i < args.length; i++) {
474
- if (args[i] === '--method' && args[i + 1]) {
475
- method = args[++i].toUpperCase();
476
- }
477
- else if (args[i] === '--body' && args[i + 1]) {
478
- try {
479
- body = JSON.parse(args[++i]);
480
- }
481
- catch {
482
- console.error(`${c.red}Invalid JSON body${c.reset}`);
483
- process.exit(1);
484
- }
485
- }
486
- }
487
- if (!OBOLOS_PRIVATE_KEY) {
488
- // Free call attempt (for free APIs or to see the 402 response)
489
- console.log(`${c.yellow}No wallet configured — attempting without payment${c.reset}`);
490
- console.log(`${c.dim}Run "obolos setup" to configure a wallet for paid APIs${c.reset}`);
491
- }
492
- const url = `${OBOLOS_API_URL}/api/proxy/${encodeURIComponent(id)}`;
493
- const fetchOpts = { method };
494
- if (body && method !== 'GET') {
495
- fetchOpts.headers = { 'Content-Type': 'application/json' };
496
- fetchOpts.body = JSON.stringify(body);
497
- }
498
- console.log(`\n${c.dim}${method} ${url}${c.reset}`);
499
- let res = await fetch(url, fetchOpts);
500
- if (res.status === 402 && OBOLOS_PRIVATE_KEY) {
501
- console.log(`${c.yellow}402 Payment Required — signing payment...${c.reset}`);
502
- let paymentInfo;
503
- try {
504
- paymentInfo = await res.json();
505
- }
506
- catch {
507
- console.error(`${c.red}Could not parse 402 response${c.reset}`);
508
- process.exit(1);
509
- }
510
- // Dynamic import viem for signing
511
- const { createWalletClient, http: viemHttp, keccak256, encodePacked } = await import('viem');
512
- const { privateKeyToAccount } = await import('viem/accounts');
513
- const { base } = await import('viem/chains');
514
- const key = OBOLOS_PRIVATE_KEY.startsWith('0x')
515
- ? OBOLOS_PRIVATE_KEY
516
- : `0x${OBOLOS_PRIVATE_KEY}`;
517
- const account = privateKeyToAccount(key);
518
- const client = createWalletClient({ account, chain: base, transport: viemHttp() });
519
- const accepts = paymentInfo.accepts?.[0];
520
- if (!accepts) {
521
- console.error(`${c.red}No payment options in 402 response${c.reset}`);
522
- process.exit(1);
523
- }
524
- const amount = BigInt(accepts.maxAmountRequired || accepts.amount || '0');
525
- const payTo = accepts.payTo;
526
- const asset = accepts.asset || '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913';
527
- const scheme = accepts.scheme || 'exact';
528
- const rawNetwork = accepts.network || 'base';
529
- const network = rawNetwork.startsWith('eip155:') ? rawNetwork : 'eip155:8453';
530
- const deadline = BigInt(Math.floor(Date.now() / 1000) + 300);
531
- // EIP-712 domain must match the USDC contract's domain (not "x402")
532
- const domain = {
533
- name: accepts.extra?.name || 'USD Coin',
534
- version: accepts.extra?.version || '2',
535
- chainId: 8453n,
536
- verifyingContract: asset,
537
- };
538
- const types = {
539
- TransferWithAuthorization: [
540
- { name: 'from', type: 'address' },
541
- { name: 'to', type: 'address' },
542
- { name: 'value', type: 'uint256' },
543
- { name: 'validAfter', type: 'uint256' },
544
- { name: 'validBefore', type: 'uint256' },
545
- { name: 'nonce', type: 'bytes32' },
546
- ],
547
- };
548
- // Check for v2 router settlement extension
549
- const settlementKey = 'x402x-router-settlement';
550
- const settlementExt = accepts.extra?.[settlementKey];
551
- const settlementInfo = settlementExt?.info;
552
- // Determine nonce: commitment hash for router settlement, random otherwise
553
- let nonce;
554
- if (settlementInfo?.settlementRouter && settlementInfo?.salt) {
555
- nonce = keccak256(encodePacked(['string', 'uint256', 'address', 'address', 'address', 'uint256', 'uint256', 'uint256', 'bytes32', 'address', 'uint256', 'address', 'bytes32'], [
556
- 'X402/settle/v1',
557
- 8453n,
558
- settlementInfo.settlementRouter,
559
- asset,
560
- account.address,
561
- BigInt(amount),
562
- 0n,
563
- deadline,
564
- settlementInfo.salt,
565
- (settlementInfo.finalPayTo || payTo),
566
- BigInt(settlementInfo.facilitatorFee || '0'),
567
- settlementInfo.hook,
568
- keccak256(settlementInfo.hookData),
569
- ]));
570
- }
571
- else {
572
- const nonceBytes = new Uint8Array(32);
573
- crypto.getRandomValues(nonceBytes);
574
- nonce = `0x${Array.from(nonceBytes).map(b => b.toString(16).padStart(2, '0')).join('')}`;
575
- }
576
- const signature = await client.signTypedData({
577
- account,
578
- domain,
579
- types,
580
- primaryType: 'TransferWithAuthorization',
581
- message: {
582
- from: account.address,
583
- to: payTo,
584
- value: BigInt(amount),
585
- validAfter: 0n,
586
- validBefore: deadline,
587
- nonce,
588
- },
589
- });
590
- const authorization = {
591
- from: account.address,
592
- to: payTo,
593
- value: amount.toString(),
594
- validAfter: '0',
595
- validBefore: deadline.toString(),
596
- nonce,
597
- };
598
- let encoded;
599
- let headerName;
600
- if (paymentInfo.x402Version === 2) {
601
- const paymentPayload = {
602
- x402Version: 2,
603
- scheme,
604
- network,
605
- payload: { signature, authorization },
606
- accepted: { ...accepts, network },
607
- };
608
- if (settlementExt) {
609
- paymentPayload.extensions = { [settlementKey]: settlementExt };
610
- }
611
- encoded = Buffer.from(JSON.stringify(paymentPayload)).toString('base64');
612
- headerName = 'payment-signature';
613
- }
614
- else {
615
- const paymentPayload = {
616
- x402Version: 1,
617
- scheme,
618
- network,
619
- payload: { signature, authorization },
620
- };
621
- encoded = Buffer.from(JSON.stringify(paymentPayload)).toString('base64');
622
- headerName = 'x-payment';
623
- }
624
- console.log(`${c.green}Payment signed. Retrying...${c.reset}`);
625
- res = await fetch(url, {
626
- ...fetchOpts,
627
- headers: {
628
- ...(fetchOpts.headers || {}),
629
- [headerName]: encoded,
630
- },
631
- });
632
- }
633
- // Display response
634
- const status = res.status;
635
- const statusColor = status < 300 ? c.green : status < 400 ? c.yellow : c.red;
636
- console.log(`${statusColor}${status} ${res.statusText}${c.reset}\n`);
637
- const contentType = res.headers.get('content-type') || '';
638
- if (contentType.includes('json')) {
639
- const data = await res.json();
640
- console.log(JSON.stringify(data, null, 2));
641
- }
642
- else {
643
- const text = await res.text();
644
- console.log(text.slice(0, 2000));
645
- }
646
- console.log();
647
- }
648
- async function cmdBalance() {
649
- if (!OBOLOS_PRIVATE_KEY) {
650
- console.error(`${c.red}No wallet configured.${c.reset} Run ${c.cyan}obolos setup${c.reset} first.`);
651
- process.exit(1);
652
- }
653
- const { createPublicClient, http: viemHttp, formatUnits } = await import('viem');
654
- const { privateKeyToAccount } = await import('viem/accounts');
655
- const { base } = await import('viem/chains');
656
- const key = OBOLOS_PRIVATE_KEY.startsWith('0x') ? OBOLOS_PRIVATE_KEY : `0x${OBOLOS_PRIVATE_KEY}`;
657
- const account = privateKeyToAccount(key);
658
- const client = createPublicClient({ chain: base, transport: viemHttp() });
659
- const balance = await client.readContract({
660
- address: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913',
661
- abi: [{ inputs: [{ name: 'account', type: 'address' }], name: 'balanceOf', outputs: [{ name: '', type: 'uint256' }], stateMutability: 'view', type: 'function' }],
662
- functionName: 'balanceOf',
663
- args: [account.address],
664
- });
665
- console.log(`\n${c.bold}Wallet:${c.reset} ${account.address}`);
666
- console.log(`${c.bold}Balance:${c.reset} ${c.green}${formatUnits(balance, 6)} USDC${c.reset}`);
667
- console.log(`${c.bold}Network:${c.reset} Base (Chain ID: 8453)\n`);
668
- }
669
- function prompt(question) {
670
- const rl = createInterface({ input: process.stdin, output: process.stdout });
671
- return new Promise(resolve => {
672
- rl.question(question, answer => {
673
- rl.close();
674
- resolve(answer.trim());
675
- });
676
- });
677
- }
678
- async function cmdSetup(args) {
679
- console.log(`\n${c.bold}${c.cyan}Obolos Wallet Setup${c.reset}\n`);
680
- const existing = loadConfig();
681
- if (args.includes('--generate')) {
682
- // Generate a new wallet
683
- const { privateKeyToAccount, generatePrivateKey } = await import('viem/accounts');
684
- const key = generatePrivateKey();
685
- const account = privateKeyToAccount(key);
686
- existing.private_key = key;
687
- saveConfig(existing);
688
- console.log(`${c.green}New wallet generated and saved!${c.reset}\n`);
689
- console.log(` ${c.bold}Address:${c.reset} ${account.address}`);
690
- console.log(` ${c.bold}Config:${c.reset} ${CONFIG_FILE}\n`);
691
- console.log(`${c.yellow}Next steps:${c.reset}`);
692
- console.log(` 1. Fund this address with USDC on Base`);
693
- console.log(` Send USDC to: ${c.cyan}${account.address}${c.reset}`);
694
- console.log(` 2. Check your balance: ${c.dim}obolos balance${c.reset}`);
695
- console.log(` 3. Call an API: ${c.dim}obolos call <api-id> --body '{...}'${c.reset}\n`);
696
- return;
697
- }
698
- if (args.includes('--show')) {
699
- if (existing.private_key) {
700
- const { privateKeyToAccount } = await import('viem/accounts');
701
- const key = existing.private_key.startsWith('0x') ? existing.private_key : `0x${existing.private_key}`;
702
- const account = privateKeyToAccount(key);
703
- console.log(` ${c.bold}Address:${c.reset} ${account.address}`);
704
- console.log(` ${c.bold}Config:${c.reset} ${CONFIG_FILE}`);
705
- console.log(` ${c.bold}API URL:${c.reset} ${OBOLOS_API_URL}\n`);
706
- }
707
- else {
708
- console.log(` ${c.yellow}No wallet configured.${c.reset}`);
709
- console.log(` Run ${c.cyan}obolos setup --generate${c.reset} to create one,`);
710
- console.log(` or ${c.cyan}obolos setup${c.reset} to import an existing key.\n`);
711
- }
712
- return;
713
- }
714
- // Interactive setup
715
- console.log(` Config is saved to ${c.dim}${CONFIG_FILE}${c.reset} (permissions: 600)\n`);
716
- if (existing.private_key) {
717
- const { privateKeyToAccount } = await import('viem/accounts');
718
- const key = existing.private_key.startsWith('0x') ? existing.private_key : `0x${existing.private_key}`;
719
- const account = privateKeyToAccount(key);
720
- console.log(` ${c.dim}Current wallet: ${account.address}${c.reset}\n`);
721
- }
722
- const keyInput = await prompt(` Private key (0x...) or "generate" for a new wallet: `);
723
- if (!keyInput) {
724
- console.log(`\n${c.yellow}No changes made.${c.reset}\n`);
725
- return;
726
- }
727
- if (keyInput === 'generate') {
728
- return cmdSetup(['--generate']);
729
- }
730
- // Validate the key
731
- const normalizedKey = keyInput.startsWith('0x') ? keyInput : `0x${keyInput}`;
732
- try {
733
- const { privateKeyToAccount } = await import('viem/accounts');
734
- const account = privateKeyToAccount(normalizedKey);
735
- existing.private_key = normalizedKey;
736
- saveConfig(existing);
737
- console.log(`\n${c.green}Wallet saved!${c.reset}\n`);
738
- console.log(` ${c.bold}Address:${c.reset} ${account.address}`);
739
- console.log(` ${c.bold}Config:${c.reset} ${CONFIG_FILE}\n`);
740
- console.log(` Check your balance: ${c.dim}obolos balance${c.reset}\n`);
741
- }
742
- catch (err) {
743
- console.error(`\n${c.red}Invalid private key: ${err.message}${c.reset}\n`);
744
- process.exit(1);
745
- }
746
- }
747
- async function cmdSetupMcp() {
748
- console.log(`\n${c.bold}${c.cyan}Obolos MCP Server Setup${c.reset}\n`);
749
- console.log(`${c.bold}Install:${c.reset}`);
750
- console.log(` npm install -g @obolos_tech/mcp-server\n`);
751
- console.log(`${c.bold}For Claude Code (global — all projects):${c.reset}`);
752
- console.log(` claude mcp add obolos ${c.yellow}--scope user${c.reset} -e OBOLOS_PRIVATE_KEY=0xyour_key -- obolos-mcp\n`);
753
- console.log(`${c.bold}For Claude Code (current project only):${c.reset}`);
754
- console.log(` claude mcp add obolos -e OBOLOS_PRIVATE_KEY=0xyour_key -- obolos-mcp\n`);
755
- console.log(`${c.bold}Or use npx (no install):${c.reset}`);
756
- console.log(` claude mcp add obolos ${c.yellow}--scope user${c.reset} -e OBOLOS_PRIVATE_KEY=0xyour_key -- npx @obolos_tech/mcp-server\n`);
757
- console.log(` ${c.dim}Scope reference:${c.reset}`);
758
- console.log(` ${c.dim} (default) Current project only${c.reset}`);
759
- console.log(` ${c.dim} --scope user All projects on your machine${c.reset}`);
760
- console.log(` ${c.dim} --scope project Shared via .mcp.json (checked into git)${c.reset}\n`);
761
- console.log(`${c.bold}For Claude Desktop / Cursor / Windsurf:${c.reset}`);
762
- console.log(` Add to your MCP config:\n`);
763
- console.log(` ${c.dim}{`);
764
- console.log(` "mcpServers": {`);
765
- console.log(` "obolos": {`);
766
- console.log(` "command": "npx",`);
767
- console.log(` "args": ["@obolos_tech/mcp-server"],`);
768
- console.log(` "env": {`);
769
- console.log(` "OBOLOS_PRIVATE_KEY": "0xyour_private_key"`);
770
- console.log(` }`);
771
- console.log(` }`);
772
- console.log(` }`);
773
- console.log(` }${c.reset}\n`);
774
- }
775
- // ─── Job Commands (ERC-8183 ACP) ────────────────────────────────────────────
776
- async function cmdJobList(args) {
777
- const params = new URLSearchParams();
778
- const status = getFlag(args, 'status');
779
- const client = getFlag(args, 'client');
780
- const provider = getFlag(args, 'provider');
781
- const limit = getFlag(args, 'limit') || '20';
782
- if (status)
783
- params.set('status', status);
784
- if (client)
785
- params.set('client', client);
786
- if (provider)
787
- params.set('provider', provider);
788
- params.set('limit', limit);
789
- const data = await apiGet(`/api/jobs?${params}`);
790
- const jobs = data.jobs || data.data || [];
791
- if (jobs.length === 0) {
792
- console.log(`${c.yellow}No jobs found.${c.reset}`);
793
- return;
794
- }
795
- const total = data.pagination?.total || data.total || jobs.length;
796
- console.log(`\n${c.bold}${c.cyan}ACP Jobs${c.reset} ${c.dim}— ${total} jobs${c.reset}\n`);
797
- // Table header
798
- console.log(` ${c.bold}${'ID'.padEnd(12)} ${'Title'.padEnd(30)} ${'Status'.padEnd(12)} ${'Budget'.padEnd(12)} ${'Client'.padEnd(14)} ${'Provider'.padEnd(14)} Created${c.reset}`);
799
- console.log(` ${c.dim}${'─'.repeat(110)}${c.reset}`);
800
- for (const job of jobs) {
801
- const id = shortenId(job.id || '');
802
- const title = (job.title || 'Untitled').slice(0, 28).padEnd(30);
803
- const st = statusColor((job.status || 'open').padEnd(10));
804
- const budget = job.budget != null ? `$${Number(job.budget).toFixed(2)}`.padEnd(12) : `${c.dim}—${c.reset}`.padEnd(12);
805
- const cl = shortenAddr(job.client).padEnd(14);
806
- const prov = job.provider ? shortenAddr(job.provider).padEnd(14) : `${c.dim}Open${c.reset}`.padEnd(14);
807
- const created = formatDate(job.created_at || job.createdAt);
808
- console.log(` ${id.padEnd(12)} ${title} ${st} ${budget} ${cl} ${prov} ${created}`);
809
- }
810
- console.log(`\n${c.dim}Use: obolos job info <id> for full details${c.reset}\n`);
811
- }
812
- async function cmdJobCreate(args) {
813
- const title = getFlag(args, 'title');
814
- const description = getFlag(args, 'description');
815
- const evaluator = getFlag(args, 'evaluator');
816
- const provider = getFlag(args, 'provider');
817
- const budget = getFlag(args, 'budget');
818
- const expires = getFlag(args, 'expires');
819
- if (!title) {
820
- console.error(`${c.red}Usage: obolos job create --title "..." --description "..." --evaluator 0x... [--provider 0x...] [--budget 1.00] [--expires 24h]${c.reset}`);
821
- process.exit(1);
822
- }
823
- if (!evaluator) {
824
- console.error(`${c.red}--evaluator is required. Provide the evaluator address (0x...).${c.reset}`);
825
- process.exit(1);
826
- }
827
- const walletAddress = await getWalletAddress();
828
- // Create job on-chain first
829
- let chainJobId = null;
830
- let chainTxHash = null;
831
- try {
832
- const acp = await getACPClient();
833
- // Parse expiry to unix timestamp (default: 7 days)
834
- let expiredAt;
835
- if (expires) {
836
- const parsed = parseRelativeTime(expires);
837
- expiredAt = Math.floor(new Date(parsed).getTime() / 1000);
838
- }
839
- else {
840
- expiredAt = Math.floor((Date.now() + 7 * 86400000) / 1000);
841
- }
842
- console.log(`\n ${c.dim}Creating job on-chain...${c.reset}`);
843
- const txHash = await acp.walletClient.writeContract({
844
- address: ACP_ADDRESS,
845
- abi: ACP_ABI,
846
- functionName: 'createJob',
847
- args: [
848
- (provider || ZERO_ADDRESS),
849
- evaluator,
850
- BigInt(expiredAt),
851
- description || title,
852
- ZERO_ADDRESS,
853
- ],
854
- account: acp.account,
855
- chain: (await import('viem/chains')).base,
856
- });
857
- console.log(` ${c.dim}Waiting for confirmation...${c.reset}`);
858
- const receipt = await acp.publicClient.waitForTransactionReceipt({ hash: txHash });
859
- // Extract jobId from JobCreated event
860
- for (const log of receipt.logs) {
861
- try {
862
- const decoded = acp.decodeEventLog({
863
- abi: ACP_ABI,
864
- data: log.data,
865
- topics: log.topics,
866
- });
867
- if (decoded.eventName === 'JobCreated') {
868
- chainJobId = (decoded.args.jobId).toString();
869
- break;
870
- }
871
- }
872
- catch { }
873
- }
874
- chainTxHash = txHash;
875
- console.log(` ${c.green}Transaction confirmed: ${txHash}${c.reset}`);
876
- if (chainJobId) {
877
- console.log(` ${c.green}Chain job ID: ${chainJobId}${c.reset}`);
878
- }
879
- }
880
- catch (err) {
881
- console.error(` ${c.yellow}On-chain creation failed: ${err.message}${c.reset}`);
882
- console.error(` ${c.dim}Falling back to backend-only...${c.reset}`);
883
- }
884
- const payload = {
885
- title,
886
- evaluator,
887
- };
888
- if (description)
889
- payload.description = description;
890
- if (provider)
891
- payload.provider = provider;
892
- if (budget)
893
- payload.budget = parseFloat(budget);
894
- if (expires)
895
- payload.expires_at = parseRelativeTime(expires);
896
- if (chainJobId)
897
- payload.chain_job_id = chainJobId;
898
- if (chainTxHash)
899
- payload.chain_tx_hash = chainTxHash;
900
- const data = await apiPost('/api/jobs', payload, {
901
- 'x-wallet-address': walletAddress,
902
- });
903
- const job = data.job || data;
904
- console.log(`\n${c.green}Job created successfully!${c.reset}\n`);
905
- console.log(`${c.dim}${'─'.repeat(60)}${c.reset}`);
906
- console.log(` ${c.bold}ID:${c.reset} ${job.id}`);
907
- if (chainJobId) {
908
- console.log(` ${c.bold}Chain ID:${c.reset} ${chainJobId}`);
909
- }
910
- console.log(` ${c.bold}Title:${c.reset} ${job.title}`);
911
- if (job.description) {
912
- console.log(` ${c.bold}Description:${c.reset} ${job.description}`);
913
- }
914
- console.log(` ${c.bold}Status:${c.reset} ${statusColor(job.status || 'open')}`);
915
- console.log(` ${c.bold}Client:${c.reset} ${job.client || walletAddress}`);
916
- console.log(` ${c.bold}Evaluator:${c.reset} ${job.evaluator}`);
917
- if (job.provider) {
918
- console.log(` ${c.bold}Provider:${c.reset} ${job.provider}`);
919
- }
920
- if (job.budget != null) {
921
- console.log(` ${c.bold}Budget:${c.reset} ${c.green}$${Number(job.budget).toFixed(2)} USDC${c.reset}`);
922
- }
923
- if (job.expires_at) {
924
- console.log(` ${c.bold}Expires:${c.reset} ${formatDate(job.expires_at)}`);
925
- }
926
- if (chainTxHash) {
927
- console.log(` ${c.bold}Tx:${c.reset} ${c.cyan}${chainTxHash}${c.reset}`);
928
- }
929
- console.log(`\n${c.dim}Next: obolos job fund ${job.id}${c.reset}\n`);
930
- }
931
- async function cmdJobInfo(args) {
932
- const id = getPositional(args, 0);
933
- if (!id) {
934
- console.error(`${c.red}Usage: obolos job info <id>${c.reset}`);
935
- process.exit(1);
936
- }
937
- const data = await apiGet(`/api/jobs/${encodeURIComponent(id)}`);
938
- const job = data.job || data;
939
- console.log(`\n${c.bold}${c.cyan}${job.title || 'Untitled Job'}${c.reset}`);
940
- console.log(`${c.dim}${'─'.repeat(60)}${c.reset}`);
941
- console.log(` ${c.bold}ID:${c.reset} ${job.id}`);
942
- console.log(` ${c.bold}Status:${c.reset} ${statusColor(job.status || 'open')}`);
943
- // State machine visualization
944
- console.log(` ${c.bold}Progress:${c.reset}`);
945
- console.log(stateVisualization(job.status || 'open'));
946
- console.log(` ${c.bold}Client:${c.reset} ${job.client || `${c.dim}—${c.reset}`}`);
947
- console.log(` ${c.bold}Evaluator:${c.reset} ${job.evaluator || `${c.dim}—${c.reset}`}`);
948
- console.log(` ${c.bold}Provider:${c.reset} ${job.provider || `${c.dim}Open (anyone can claim)${c.reset}`}`);
949
- if (job.budget != null) {
950
- console.log(` ${c.bold}Budget:${c.reset} ${c.green}$${Number(job.budget).toFixed(2)} USDC${c.reset}`);
951
- }
952
- if (job.description) {
953
- console.log(`\n ${c.bold}Description:${c.reset}`);
954
- const descLines = job.description.split('\n');
955
- for (const line of descLines) {
956
- console.log(` ${line}`);
957
- }
958
- }
959
- if (job.deliverable) {
960
- console.log(`\n ${c.bold}Deliverable:${c.reset} ${c.cyan}${job.deliverable}${c.reset}`);
961
- }
962
- if (job.reason) {
963
- console.log(` ${c.bold}Reason:${c.reset} ${job.reason}`);
964
- }
965
- if (job.expires_at) {
966
- const expiryDate = new Date(job.expires_at);
967
- const now = new Date();
968
- const expired = expiryDate < now;
969
- console.log(` ${c.bold}Expires:${c.reset} ${expired ? c.red : c.dim}${formatDate(job.expires_at)}${expired ? ' (expired)' : ''}${c.reset}`);
970
- }
971
- console.log(` ${c.bold}Created:${c.reset} ${formatDate(job.created_at || job.createdAt)}`);
972
- if (job.updated_at || job.updatedAt) {
973
- console.log(` ${c.bold}Updated:${c.reset} ${formatDate(job.updated_at || job.updatedAt)}`);
974
- }
975
- // Actions hint based on status
976
- console.log();
977
- const s = job.status || 'open';
978
- if (s === 'open') {
979
- console.log(` ${c.bold}Actions:${c.reset}`);
980
- console.log(` obolos job fund ${job.id} ${c.dim}Fund the escrow${c.reset}`);
981
- }
982
- else if (s === 'funded') {
983
- console.log(` ${c.bold}Actions:${c.reset}`);
984
- console.log(` obolos job submit ${job.id} --deliverable <hash> ${c.dim}Submit work${c.reset}`);
985
- }
986
- else if (s === 'submitted') {
987
- console.log(` ${c.bold}Actions:${c.reset}`);
988
- console.log(` obolos job complete ${job.id} ${c.dim}Approve and release funds${c.reset}`);
989
- console.log(` obolos job reject ${job.id} ${c.dim}Reject the submission${c.reset}`);
990
- }
991
- console.log();
992
- }
993
- async function cmdJobFund(args) {
994
- const id = getPositional(args, 0);
995
- if (!id) {
996
- console.error(`${c.red}Usage: obolos job fund <id>${c.reset}`);
997
- process.exit(1);
998
- }
999
- const walletAddress = await getWalletAddress();
1000
- // First fetch the job to show budget info
1001
- const jobData = await apiGet(`/api/jobs/${encodeURIComponent(id)}`);
1002
- const job = jobData.job || jobData;
1003
- console.log(`\n${c.bold}${c.cyan}Fund Job${c.reset}\n`);
1004
- console.log(`${c.dim}${'─'.repeat(60)}${c.reset}`);
1005
- console.log(` ${c.bold}Job:${c.reset} ${job.title || id}`);
1006
- if (job.budget != null) {
1007
- console.log(` ${c.bold}Budget:${c.reset} ${c.green}$${Number(job.budget).toFixed(2)} USDC${c.reset}`);
1008
- }
1009
- console.log(` ${c.bold}Status:${c.reset} ${statusColor(job.status || 'open')}`);
1010
- console.log();
1011
- const chainJobId = job.chain_job_id;
1012
- let txHash = null;
1013
- if (chainJobId && job.budget != null) {
1014
- try {
1015
- const acp = await getACPClient();
1016
- const budgetStr = String(job.budget);
1017
- // Check USDC allowance and approve if needed
1018
- console.log(` ${c.dim}Checking USDC allowance...${c.reset}`);
1019
- const amount = acp.parseUnits(budgetStr, 6);
1020
- const allowance = await acp.publicClient.readContract({
1021
- address: USDC_CONTRACT,
1022
- abi: ERC20_ABI,
1023
- functionName: 'allowance',
1024
- args: [acp.account.address, ACP_ADDRESS],
1025
- });
1026
- if (allowance < amount) {
1027
- console.log(` ${c.dim}Approving USDC spend...${c.reset}`);
1028
- const approveTx = await acp.walletClient.writeContract({
1029
- address: USDC_CONTRACT,
1030
- abi: ERC20_ABI,
1031
- functionName: 'approve',
1032
- args: [ACP_ADDRESS, amount],
1033
- account: acp.account,
1034
- chain: (await import('viem/chains')).base,
1035
- });
1036
- await acp.publicClient.waitForTransactionReceipt({ hash: approveTx });
1037
- console.log(` ${c.green}USDC approved${c.reset}`);
1038
- }
1039
- console.log(` ${c.dim}Funding escrow on-chain...${c.reset}`);
1040
- const fundTx = await acp.walletClient.writeContract({
1041
- address: ACP_ADDRESS,
1042
- abi: ACP_ABI,
1043
- functionName: 'fund',
1044
- args: [BigInt(chainJobId), amount, '0x'],
1045
- account: acp.account,
1046
- chain: (await import('viem/chains')).base,
1047
- });
1048
- console.log(` ${c.dim}Waiting for confirmation...${c.reset}`);
1049
- await acp.publicClient.waitForTransactionReceipt({ hash: fundTx });
1050
- txHash = fundTx;
1051
- console.log(` ${c.green}Transaction confirmed: ${txHash}${c.reset}\n`);
1052
- }
1053
- catch (err) {
1054
- console.error(` ${c.yellow}On-chain funding failed: ${err.message}${c.reset}`);
1055
- console.error(` ${c.dim}Recording funding intent in backend...${c.reset}\n`);
1056
- }
1057
- }
1058
- // Update backend
1059
- const fundPayload = {};
1060
- if (txHash)
1061
- fundPayload.tx_hash = txHash;
1062
- if (chainJobId)
1063
- fundPayload.chain_job_id = chainJobId;
1064
- const data = await apiPost(`/api/jobs/${encodeURIComponent(id)}/fund`, fundPayload, {
1065
- 'x-wallet-address': walletAddress,
1066
- });
1067
- const updated = data.job || data;
1068
- console.log(`${c.green}Job funded successfully!${c.reset}`);
1069
- console.log(` ${c.bold}Status:${c.reset} ${statusColor(updated.status || 'funded')}`);
1070
- if (txHash) {
1071
- console.log(` ${c.bold}Tx:${c.reset} ${c.cyan}${txHash}${c.reset}`);
1072
- }
1073
- console.log(`${c.dim}Next: Provider submits work with: obolos job submit ${id} --deliverable <hash>${c.reset}\n`);
1074
- }
1075
- async function cmdJobSubmit(args) {
1076
- const id = getPositional(args, 0);
1077
- if (!id) {
1078
- console.error(`${c.red}Usage: obolos job submit <id> --deliverable <hash/CID/URL>${c.reset}`);
1079
- process.exit(1);
1080
- }
1081
- const deliverable = getFlag(args, 'deliverable');
1082
- if (!deliverable) {
1083
- console.error(`${c.red}--deliverable is required. Provide a hash, CID, or URL for the work product.${c.reset}`);
1084
- process.exit(1);
1085
- }
1086
- const walletAddress = await getWalletAddress();
1087
- // Fetch job to get chain_job_id
1088
- const jobData = await apiGet(`/api/jobs/${encodeURIComponent(id)}`);
1089
- const existingJob = jobData.job || jobData;
1090
- const chainJobId = existingJob.chain_job_id;
1091
- let txHash = null;
1092
- if (chainJobId) {
1093
- try {
1094
- const acp = await getACPClient();
1095
- const deliverableHash = acp.keccak256(acp.toHex(deliverable));
1096
- console.log(`\n ${c.dim}Submitting work on-chain...${c.reset}`);
1097
- const submitTx = await acp.walletClient.writeContract({
1098
- address: ACP_ADDRESS,
1099
- abi: ACP_ABI,
1100
- functionName: 'submit',
1101
- args: [BigInt(chainJobId), deliverableHash, '0x'],
1102
- account: acp.account,
1103
- chain: (await import('viem/chains')).base,
1104
- });
1105
- console.log(` ${c.dim}Waiting for confirmation...${c.reset}`);
1106
- await acp.publicClient.waitForTransactionReceipt({ hash: submitTx });
1107
- txHash = submitTx;
1108
- console.log(` ${c.green}Transaction confirmed: ${txHash}${c.reset}`);
1109
- }
1110
- catch (err) {
1111
- console.error(` ${c.yellow}On-chain submission failed: ${err.message}${c.reset}`);
1112
- }
1113
- }
1114
- const submitPayload = { deliverable };
1115
- if (txHash)
1116
- submitPayload.tx_hash = txHash;
1117
- const data = await apiPost(`/api/jobs/${encodeURIComponent(id)}/submit`, submitPayload, {
1118
- 'x-wallet-address': walletAddress,
1119
- });
1120
- const job = data.job || data;
1121
- console.log(`\n${c.green}Work submitted successfully!${c.reset}\n`);
1122
- console.log(`${c.dim}${'─'.repeat(60)}${c.reset}`);
1123
- console.log(` ${c.bold}Job:${c.reset} ${job.title || id}`);
1124
- console.log(` ${c.bold}Status:${c.reset} ${statusColor(job.status || 'submitted')}`);
1125
- console.log(` ${c.bold}Deliverable:${c.reset} ${c.cyan}${deliverable}${c.reset}`);
1126
- if (txHash) {
1127
- console.log(` ${c.bold}Tx:${c.reset} ${c.cyan}${txHash}${c.reset}`);
1128
- }
1129
- console.log(`\n${c.dim}The evaluator will now review and approve or reject the submission.${c.reset}\n`);
1130
- }
1131
- async function cmdJobComplete(args) {
1132
- const id = getPositional(args, 0);
1133
- if (!id) {
1134
- console.error(`${c.red}Usage: obolos job complete <id> [--reason "..."]${c.reset}`);
1135
- process.exit(1);
1136
- }
1137
- const reason = getFlag(args, 'reason');
1138
- const walletAddress = await getWalletAddress();
1139
- // Fetch job to get chain_job_id
1140
- const jobData = await apiGet(`/api/jobs/${encodeURIComponent(id)}`);
1141
- const existingJob = jobData.job || jobData;
1142
- const chainJobId = existingJob.chain_job_id;
1143
- let txHash = null;
1144
- if (chainJobId) {
1145
- try {
1146
- const acp = await getACPClient();
1147
- const reasonHash = reason
1148
- ? acp.keccak256(acp.toHex(reason))
1149
- : ZERO_BYTES32;
1150
- console.log(`\n ${c.dim}Completing job on-chain...${c.reset}`);
1151
- const completeTx = await acp.walletClient.writeContract({
1152
- address: ACP_ADDRESS,
1153
- abi: ACP_ABI,
1154
- functionName: 'complete',
1155
- args: [BigInt(chainJobId), reasonHash, '0x'],
1156
- account: acp.account,
1157
- chain: (await import('viem/chains')).base,
1158
- });
1159
- console.log(` ${c.dim}Waiting for confirmation...${c.reset}`);
1160
- await acp.publicClient.waitForTransactionReceipt({ hash: completeTx });
1161
- txHash = completeTx;
1162
- console.log(` ${c.green}Transaction confirmed: ${txHash}${c.reset}`);
1163
- }
1164
- catch (err) {
1165
- console.error(` ${c.yellow}On-chain completion failed: ${err.message}${c.reset}`);
1166
- }
1167
- }
1168
- const payload = {};
1169
- if (reason)
1170
- payload.reason = reason;
1171
- if (txHash)
1172
- payload.tx_hash = txHash;
1173
- const data = await apiPost(`/api/jobs/${encodeURIComponent(id)}/complete`, payload, {
1174
- 'x-wallet-address': walletAddress,
1175
- });
1176
- const job = data.job || data;
1177
- console.log(`\n${c.green}Job completed and approved!${c.reset}\n`);
1178
- console.log(`${c.dim}${'─'.repeat(60)}${c.reset}`);
1179
- console.log(` ${c.bold}Job:${c.reset} ${job.title || id}`);
1180
- console.log(` ${c.bold}Status:${c.reset} ${statusColor(job.status || 'completed')}`);
1181
- if (reason) {
1182
- console.log(` ${c.bold}Reason:${c.reset} ${reason}`);
1183
- }
1184
- if (txHash) {
1185
- console.log(` ${c.bold}Tx:${c.reset} ${c.cyan}${txHash}${c.reset}`);
1186
- }
1187
- if (job.budget != null) {
1188
- console.log(`\n ${c.dim}Escrow of $${Number(job.budget).toFixed(2)} USDC released to provider.${c.reset}`);
1189
- }
1190
- console.log();
1191
- }
1192
- async function cmdJobReject(args) {
1193
- const id = getPositional(args, 0);
1194
- if (!id) {
1195
- console.error(`${c.red}Usage: obolos job reject <id> [--reason "..."]${c.reset}`);
1196
- process.exit(1);
1197
- }
1198
- const reason = getFlag(args, 'reason');
1199
- const walletAddress = await getWalletAddress();
1200
- // Fetch job to get chain_job_id
1201
- const jobData = await apiGet(`/api/jobs/${encodeURIComponent(id)}`);
1202
- const existingJob = jobData.job || jobData;
1203
- const chainJobId = existingJob.chain_job_id;
1204
- let txHash = null;
1205
- if (chainJobId) {
1206
- try {
1207
- const acp = await getACPClient();
1208
- const reasonHash = reason
1209
- ? acp.keccak256(acp.toHex(reason))
1210
- : ZERO_BYTES32;
1211
- console.log(`\n ${c.dim}Rejecting job on-chain...${c.reset}`);
1212
- const rejectTx = await acp.walletClient.writeContract({
1213
- address: ACP_ADDRESS,
1214
- abi: ACP_ABI,
1215
- functionName: 'reject',
1216
- args: [BigInt(chainJobId), reasonHash, '0x'],
1217
- account: acp.account,
1218
- chain: (await import('viem/chains')).base,
1219
- });
1220
- console.log(` ${c.dim}Waiting for confirmation...${c.reset}`);
1221
- await acp.publicClient.waitForTransactionReceipt({ hash: rejectTx });
1222
- txHash = rejectTx;
1223
- console.log(` ${c.green}Transaction confirmed: ${txHash}${c.reset}`);
1224
- }
1225
- catch (err) {
1226
- console.error(` ${c.yellow}On-chain rejection failed: ${err.message}${c.reset}`);
1227
- }
1228
- }
1229
- const payload = {};
1230
- if (reason)
1231
- payload.reason = reason;
1232
- if (txHash)
1233
- payload.tx_hash = txHash;
1234
- const data = await apiPost(`/api/jobs/${encodeURIComponent(id)}/reject`, payload, {
1235
- 'x-wallet-address': walletAddress,
1236
- });
1237
- const job = data.job || data;
1238
- console.log(`\n${c.red}Job rejected.${c.reset}\n`);
1239
- console.log(`${c.dim}${'─'.repeat(60)}${c.reset}`);
1240
- console.log(` ${c.bold}Job:${c.reset} ${job.title || id}`);
1241
- console.log(` ${c.bold}Status:${c.reset} ${statusColor(job.status || 'rejected')}`);
1242
- if (reason) {
1243
- console.log(` ${c.bold}Reason:${c.reset} ${reason}`);
1244
- }
1245
- if (txHash) {
1246
- console.log(` ${c.bold}Tx:${c.reset} ${c.cyan}${txHash}${c.reset}`);
18
+ function showHelp() {
19
+ const groups = {};
20
+ for (const cmd of registry.all()) {
21
+ const [group] = cmd.name.split('.');
22
+ (groups[group] ||= []).push(cmd.name);
1247
23
  }
1248
- console.log();
1249
- }
1250
- function showJobHelp() {
1251
- console.log(`
1252
- ${c.bold}${c.cyan}obolos job${c.reset} — ERC-8183 Agentic Commerce Protocol (ACP) job management
24
+ process.stdout.write(`
25
+ ${c.bold}${c.cyan}obolos${c.reset} — Commerce infrastructure for autonomous work on Base
1253
26
 
1254
27
  ${c.bold}Usage:${c.reset}
1255
- obolos job list [options] List jobs with optional filters
1256
- obolos job create [options] Create a new job
1257
- obolos job info <id> Get full job details
1258
- obolos job fund <id> Fund a job's escrow
1259
- obolos job submit <id> [options] Submit work for a job
1260
- obolos job complete <id> [options] Approve a job (evaluator)
1261
- obolos job reject <id> [options] Reject a job submission
1262
-
1263
- ${c.bold}List Options:${c.reset}
1264
- --status=open|funded|submitted|completed|rejected|expired
1265
- --client=0x... Filter by client address
1266
- --provider=0x... Filter by provider address
1267
- --limit=20 Max results (default: 20)
1268
-
1269
- ${c.bold}Create Options:${c.reset}
1270
- --title "..." Job title (required)
1271
- --description "..." Job description
1272
- --evaluator 0x... Evaluator address (required)
1273
- --provider 0x... Specific provider (optional, open if omitted)
1274
- --budget 1.00 Budget in USDC
1275
- --expires 24h Expiry (e.g., "24h", "7d", "1h")
1276
-
1277
- ${c.bold}Submit Options:${c.reset}
1278
- --deliverable <hash/CID/URL> Work product reference (required)
1279
-
1280
- ${c.bold}Complete/Reject Options:${c.reset}
1281
- --reason "..." Optional reason text
1282
-
1283
- ${c.bold}Examples:${c.reset}
1284
- obolos job list --status=open
1285
- obolos job create --title "Analyze dataset" --evaluator 0xABC... --budget 5.00 --expires 7d
1286
- obolos job info abc123
1287
- obolos job fund abc123
1288
- obolos job submit abc123 --deliverable ipfs://Qm...
1289
- obolos job complete abc123 --reason "Looks great"
1290
- obolos job reject abc123 --reason "Missing section 3"
1291
- `);
1292
- }
1293
- async function cmdJob(args) {
1294
- const subcommand = args[0];
1295
- const subArgs = args.slice(1);
1296
- switch (subcommand) {
1297
- case 'list':
1298
- case 'ls':
1299
- await cmdJobList(subArgs);
1300
- break;
1301
- case 'create':
1302
- case 'new':
1303
- await cmdJobCreate(subArgs);
1304
- break;
1305
- case 'info':
1306
- case 'show':
1307
- await cmdJobInfo(subArgs);
1308
- break;
1309
- case 'fund':
1310
- await cmdJobFund(subArgs);
1311
- break;
1312
- case 'submit':
1313
- await cmdJobSubmit(subArgs);
1314
- break;
1315
- case 'complete':
1316
- case 'approve':
1317
- await cmdJobComplete(subArgs);
1318
- break;
1319
- case 'reject':
1320
- await cmdJobReject(subArgs);
1321
- break;
1322
- case 'help':
1323
- case '--help':
1324
- case '-h':
1325
- case undefined:
1326
- showJobHelp();
1327
- break;
1328
- default:
1329
- console.error(`${c.red}Unknown job subcommand: ${subcommand}${c.reset}`);
1330
- showJobHelp();
1331
- process.exit(1);
1332
- }
1333
- }
1334
- // ─── Listing Commands (Negotiation Layer) ────────────────────────────────────
1335
- async function cmdListingList(args) {
1336
- const params = new URLSearchParams();
1337
- const status = getFlag(args, 'status');
1338
- const client = getFlag(args, 'client');
1339
- const limit = getFlag(args, 'limit') || '20';
1340
- if (status)
1341
- params.set('status', status);
1342
- if (client)
1343
- params.set('client', client);
1344
- params.set('limit', limit);
1345
- const data = await apiGet(`/api/listings?${params}`);
1346
- const listings = data.listings || data.data || [];
1347
- if (listings.length === 0) {
1348
- console.log(`${c.yellow}No listings found.${c.reset}`);
1349
- return;
1350
- }
1351
- const total = data.pagination?.total || data.total || listings.length;
1352
- console.log(`\n${c.bold}${c.cyan}Job Listings${c.reset} ${c.dim}— ${total} listings${c.reset}\n`);
1353
- // Table header
1354
- console.log(` ${c.bold}${'ID'.padEnd(12)} ${'Title'.padEnd(28)} ${'Status'.padEnd(14)} ${'Budget Range'.padEnd(20)} ${'Bids'.padEnd(6)} ${'Client'.padEnd(14)} Deadline${c.reset}`);
1355
- console.log(` ${c.dim}${'─'.repeat(110)}${c.reset}`);
1356
- for (const l of listings) {
1357
- const id = shortenId(l.id || '');
1358
- const title = (l.title || 'Untitled').slice(0, 26).padEnd(28);
1359
- const st = statusColor((l.status || 'open').padEnd(12));
1360
- const budgetMin = l.min_budget != null ? `$${Number(l.min_budget).toFixed(2)}` : '?';
1361
- const budgetMax = l.max_budget != null ? `$${Number(l.max_budget).toFixed(2)}` : '?';
1362
- const budget = `${budgetMin}-${budgetMax}`.padEnd(20);
1363
- const bids = String(l.bid_count ?? l.bids?.length ?? 0).padEnd(6);
1364
- const cl = shortenAddr(l.client_address || l.client).padEnd(14);
1365
- const deadline = l.deadline ? formatDate(l.deadline) : `${c.dim}—${c.reset}`;
1366
- console.log(` ${id.padEnd(12)} ${title} ${st} ${budget} ${bids} ${cl} ${deadline}`);
1367
- }
1368
- console.log(`\n${c.dim}Use: obolos listing info <id> for full details${c.reset}\n`);
1369
- }
1370
- async function cmdListingCreate(args) {
1371
- const title = getFlag(args, 'title');
1372
- const description = getFlag(args, 'description');
1373
- const minBudget = getFlag(args, 'min-budget');
1374
- const maxBudget = getFlag(args, 'max-budget');
1375
- const deadline = getFlag(args, 'deadline');
1376
- const duration = getFlag(args, 'duration');
1377
- const evaluator = getFlag(args, 'evaluator');
1378
- const hook = getFlag(args, 'hook');
1379
- if (!title) {
1380
- console.error(`${c.red}Usage: obolos listing create --title "..." --description "..." [--min-budget 1.00] [--max-budget 10.00] [--deadline 7d] [--duration 24]${c.reset}`);
1381
- process.exit(1);
1382
- }
1383
- const walletAddress = await getWalletAddress();
1384
- const payload = { title };
1385
- if (description)
1386
- payload.description = description;
1387
- if (minBudget)
1388
- payload.min_budget = minBudget;
1389
- if (maxBudget)
1390
- payload.max_budget = maxBudget;
1391
- if (deadline)
1392
- payload.deadline = deadline;
1393
- if (duration)
1394
- payload.job_duration = parseInt(duration, 10);
1395
- if (evaluator)
1396
- payload.preferred_evaluator = evaluator;
1397
- if (hook)
1398
- payload.hook_address = hook;
1399
- const data = await apiPost('/api/listings', payload, {
1400
- 'x-wallet-address': walletAddress,
1401
- });
1402
- const listing = data.listing || data;
1403
- console.log(`\n${c.green}Listing created successfully!${c.reset}\n`);
1404
- console.log(`${c.dim}${'─'.repeat(60)}${c.reset}`);
1405
- console.log(` ${c.bold}ID:${c.reset} ${listing.id}`);
1406
- console.log(` ${c.bold}Title:${c.reset} ${listing.title}`);
1407
- if (listing.description) {
1408
- console.log(` ${c.bold}Description:${c.reset} ${listing.description}`);
1409
- }
1410
- console.log(` ${c.bold}Status:${c.reset} ${statusColor(listing.status || 'open')}`);
1411
- console.log(` ${c.bold}Client:${c.reset} ${listing.client_address || walletAddress}`);
1412
- if (listing.min_budget != null || listing.max_budget != null) {
1413
- const min = listing.min_budget != null ? `$${Number(listing.min_budget).toFixed(2)}` : '?';
1414
- const max = listing.max_budget != null ? `$${Number(listing.max_budget).toFixed(2)}` : '?';
1415
- console.log(` ${c.bold}Budget:${c.reset} ${c.green}${min} – ${max} USDC${c.reset}`);
1416
- }
1417
- if (listing.deadline) {
1418
- console.log(` ${c.bold}Deadline:${c.reset} ${formatDate(listing.deadline)}`);
1419
- }
1420
- if (listing.job_duration) {
1421
- console.log(` ${c.bold}Duration:${c.reset} ${listing.job_duration}h`);
1422
- }
1423
- console.log(`\n${c.dim}Share this listing with providers. They can bid with: obolos listing bid ${listing.id} --price 5.00${c.reset}\n`);
1424
- }
1425
- async function cmdListingInfo(args) {
1426
- const id = getPositional(args, 0);
1427
- if (!id) {
1428
- console.error(`${c.red}Usage: obolos listing info <id>${c.reset}`);
1429
- process.exit(1);
1430
- }
1431
- const data = await apiGet(`/api/listings/${encodeURIComponent(id)}`);
1432
- const listing = data.listing || data;
1433
- console.log(`\n${c.bold}${c.cyan}${listing.title || 'Untitled Listing'}${c.reset}`);
1434
- console.log(`${c.dim}${'─'.repeat(60)}${c.reset}`);
1435
- console.log(` ${c.bold}ID:${c.reset} ${listing.id}`);
1436
- console.log(` ${c.bold}Status:${c.reset} ${statusColor(listing.status || 'open')}`);
1437
- console.log(` ${c.bold}Client:${c.reset} ${listing.client_address || `${c.dim}—${c.reset}`}`);
1438
- if (listing.min_budget != null || listing.max_budget != null) {
1439
- const min = listing.min_budget != null ? `$${Number(listing.min_budget).toFixed(2)}` : '?';
1440
- const max = listing.max_budget != null ? `$${Number(listing.max_budget).toFixed(2)}` : '?';
1441
- console.log(` ${c.bold}Budget:${c.reset} ${c.green}${min} – ${max} USDC${c.reset}`);
1442
- }
1443
- if (listing.deadline) {
1444
- const deadlineDate = new Date(listing.deadline);
1445
- const now = new Date();
1446
- const expired = deadlineDate < now;
1447
- console.log(` ${c.bold}Deadline:${c.reset} ${expired ? c.red : c.dim}${formatDate(listing.deadline)}${expired ? ' (passed)' : ''}${c.reset}`);
1448
- }
1449
- if (listing.job_duration) {
1450
- console.log(` ${c.bold}Duration:${c.reset} ${listing.job_duration}h`);
1451
- }
1452
- if (listing.preferred_evaluator) {
1453
- console.log(` ${c.bold}Evaluator:${c.reset} ${listing.preferred_evaluator}`);
1454
- }
1455
- if (listing.description) {
1456
- console.log(`\n ${c.bold}Description:${c.reset}`);
1457
- const descLines = listing.description.split('\n');
1458
- for (const line of descLines) {
1459
- console.log(` ${line}`);
1460
- }
1461
- }
1462
- console.log(` ${c.bold}Created:${c.reset} ${formatDate(listing.created_at || listing.createdAt)}`);
1463
- // Bids
1464
- const bids = listing.bids || [];
1465
- if (bids.length > 0) {
1466
- console.log(`\n ${c.bold}${c.cyan}Bids (${bids.length})${c.reset}`);
1467
- console.log(` ${c.dim}${'─'.repeat(56)}${c.reset}`);
1468
- console.log(` ${c.bold}${'Bid ID'.padEnd(12)} ${'Provider'.padEnd(14)} ${'Price'.padEnd(12)} ${'Delivery'.padEnd(10)} Message${c.reset}`);
1469
- console.log(` ${c.dim}${'─'.repeat(56)}${c.reset}`);
1470
- for (const bid of bids) {
1471
- const bidId = shortenId(bid.id || '');
1472
- const provider = shortenAddr(bid.provider_address);
1473
- const price = bid.price != null ? `${c.green}$${Number(bid.price).toFixed(2)}${c.reset}` : `${c.dim}—${c.reset}`;
1474
- const delivery = bid.delivery_time ? `${bid.delivery_time}h` : `${c.dim}—${c.reset}`;
1475
- const msg = (bid.message || '').slice(0, 40);
1476
- console.log(` ${bidId.padEnd(12)} ${provider.padEnd(14)} ${price.padEnd(12)} ${delivery.padEnd(10)} ${c.dim}${msg}${c.reset}`);
1477
- }
1478
- }
1479
- else {
1480
- console.log(`\n ${c.dim}No bids yet.${c.reset}`);
1481
- }
1482
- // Actions
1483
- console.log();
1484
- const s = listing.status || 'open';
1485
- if (s === 'open') {
1486
- console.log(` ${c.bold}Actions:${c.reset}`);
1487
- console.log(` obolos listing bid ${listing.id} --price 5.00 ${c.dim}Submit a bid${c.reset}`);
1488
- if (bids.length > 0) {
1489
- console.log(` obolos listing accept ${listing.id} --bid <bid_id> ${c.dim}Accept a bid${c.reset}`);
1490
- }
1491
- console.log(` obolos listing cancel ${listing.id} ${c.dim}Cancel the listing${c.reset}`);
1492
- }
1493
- console.log();
1494
- }
1495
- async function cmdListingBid(args) {
1496
- const listingId = getPositional(args, 0);
1497
- if (!listingId) {
1498
- console.error(`${c.red}Usage: obolos listing bid <listing_id> --price 5.00 [--delivery 24] [--message "..."]${c.reset}`);
1499
- process.exit(1);
1500
- }
1501
- const price = getFlag(args, 'price');
1502
- if (!price) {
1503
- console.error(`${c.red}--price is required. Provide your bid amount in USDC.${c.reset}`);
1504
- process.exit(1);
1505
- }
1506
- const delivery = getFlag(args, 'delivery');
1507
- const message = getFlag(args, 'message');
1508
- const proposalHash = getFlag(args, 'proposal-hash');
1509
- const walletAddress = await getWalletAddress();
1510
- const payload = { price };
1511
- if (delivery)
1512
- payload.delivery_time = parseInt(delivery, 10);
1513
- if (message)
1514
- payload.message = message;
1515
- if (proposalHash)
1516
- payload.proposal_hash = proposalHash;
1517
- const data = await apiPost(`/api/listings/${encodeURIComponent(listingId)}/bid`, payload, {
1518
- 'x-wallet-address': walletAddress,
1519
- });
1520
- const bid = data.bid || data;
1521
- console.log(`\n${c.green}Bid submitted successfully!${c.reset}\n`);
1522
- console.log(`${c.dim}${'─'.repeat(60)}${c.reset}`);
1523
- console.log(` ${c.bold}Bid ID:${c.reset} ${bid.id}`);
1524
- console.log(` ${c.bold}Listing:${c.reset} ${listingId}`);
1525
- console.log(` ${c.bold}Price:${c.reset} ${c.green}$${Number(price).toFixed(2)} USDC${c.reset}`);
1526
- if (delivery) {
1527
- console.log(` ${c.bold}Delivery:${c.reset} ${delivery}h`);
1528
- }
1529
- if (message) {
1530
- console.log(` ${c.bold}Message:${c.reset} ${message}`);
1531
- }
1532
- console.log(`\n${c.dim}The client will review your bid. You'll be notified if accepted.${c.reset}\n`);
1533
- }
1534
- async function cmdListingAccept(args) {
1535
- const listingId = getPositional(args, 0);
1536
- if (!listingId) {
1537
- console.error(`${c.red}Usage: obolos listing accept <listing_id> --bid <bid_id>${c.reset}`);
1538
- process.exit(1);
1539
- }
1540
- const bidId = getFlag(args, 'bid');
1541
- if (!bidId) {
1542
- console.error(`${c.red}--bid is required. Specify the bid ID to accept.${c.reset}`);
1543
- process.exit(1);
1544
- }
1545
- const walletAddress = await getWalletAddress();
1546
- // Create on-chain ACP job if wallet is available
1547
- let chainJobId = null;
1548
- let chainTxHash = null;
1549
- try {
1550
- // Fetch listing details to get terms
1551
- const listingData = await apiGet(`/api/listings/${encodeURIComponent(listingId)}`);
1552
- const listing = listingData.listing || listingData;
1553
- const bids = listing.bids || [];
1554
- const acceptedBid = bids.find((b) => b.id === bidId);
1555
- if (acceptedBid && OBOLOS_PRIVATE_KEY) {
1556
- const acp = await getACPClient();
1557
- const providerAddress = acceptedBid.provider_address || ZERO_ADDRESS;
1558
- const evaluatorAddress = listing.preferred_evaluator || walletAddress;
1559
- // Default expiry: delivery_time hours or job_duration or 7 days
1560
- const durationHours = acceptedBid.delivery_time || listing.job_duration || 168;
1561
- const expiredAt = Math.floor((Date.now() + durationHours * 3600000) / 1000);
1562
- const description = `${listing.title}: ${listing.description || ''}`.slice(0, 500);
1563
- console.log(`\n ${c.dim}Creating ACP job on-chain...${c.reset}`);
1564
- const txHash = await acp.walletClient.writeContract({
1565
- address: ACP_ADDRESS,
1566
- abi: ACP_ABI,
1567
- functionName: 'createJob',
1568
- args: [
1569
- providerAddress,
1570
- evaluatorAddress,
1571
- BigInt(expiredAt),
1572
- description,
1573
- (listing.hook_address || ZERO_ADDRESS),
1574
- ],
1575
- account: acp.account,
1576
- chain: (await import('viem/chains')).base,
1577
- });
1578
- console.log(` ${c.dim}Waiting for confirmation...${c.reset}`);
1579
- const receipt = await acp.publicClient.waitForTransactionReceipt({ hash: txHash });
1580
- // Extract jobId from JobCreated event
1581
- for (const log of receipt.logs) {
1582
- try {
1583
- const decoded = acp.decodeEventLog({
1584
- abi: ACP_ABI,
1585
- data: log.data,
1586
- topics: log.topics,
1587
- });
1588
- if (decoded.eventName === 'JobCreated') {
1589
- chainJobId = (decoded.args.jobId).toString();
1590
- break;
1591
- }
1592
- }
1593
- catch { }
1594
- }
1595
- chainTxHash = txHash;
1596
- console.log(` ${c.green}Transaction confirmed: ${txHash}${c.reset}`);
1597
- if (chainJobId) {
1598
- console.log(` ${c.green}Chain job ID: ${chainJobId}${c.reset}`);
1599
- }
1600
- }
1601
- }
1602
- catch (err) {
1603
- console.error(` ${c.yellow}On-chain job creation failed: ${err.message}${c.reset}`);
1604
- console.error(` ${c.dim}Proceeding with backend-only acceptance...${c.reset}`);
1605
- }
1606
- const payload = { bid_id: bidId };
1607
- if (chainJobId)
1608
- payload.acp_job_id = chainJobId;
1609
- if (chainTxHash)
1610
- payload.chain_tx_hash = chainTxHash;
1611
- const data = await apiPost(`/api/listings/${encodeURIComponent(listingId)}/accept`, payload, {
1612
- 'x-wallet-address': walletAddress,
1613
- });
1614
- const listing = data.listing || data;
1615
- console.log(`\n${c.green}Bid accepted! ACP job created.${c.reset}\n`);
1616
- console.log(`${c.dim}${'─'.repeat(60)}${c.reset}`);
1617
- console.log(` ${c.bold}Listing:${c.reset} ${listing.title || listingId}`);
1618
- console.log(` ${c.bold}Status:${c.reset} ${statusColor(listing.status || 'accepted')}`);
1619
- console.log(` ${c.bold}Bid:${c.reset} ${bidId}`);
1620
- if (chainJobId) {
1621
- console.log(` ${c.bold}Chain ID:${c.reset} ${chainJobId}`);
1622
- }
1623
- if (chainTxHash) {
1624
- console.log(` ${c.bold}Tx:${c.reset} ${c.cyan}${chainTxHash}${c.reset}`);
1625
- }
1626
- if (listing.job_id || data.job_id) {
1627
- console.log(` ${c.bold}Job ID:${c.reset} ${listing.job_id || data.job_id}`);
1628
- }
1629
- console.log(`\n${c.dim}Next: Fund the escrow with: obolos job fund <job-id>${c.reset}\n`);
1630
- }
1631
- async function cmdListingCancel(args) {
1632
- const listingId = getPositional(args, 0);
1633
- if (!listingId) {
1634
- console.error(`${c.red}Usage: obolos listing cancel <listing_id>${c.reset}`);
1635
- process.exit(1);
1636
- }
1637
- const walletAddress = await getWalletAddress();
1638
- const data = await apiPost(`/api/listings/${encodeURIComponent(listingId)}/cancel`, {}, {
1639
- 'x-wallet-address': walletAddress,
1640
- });
1641
- const listing = data.listing || data;
1642
- console.log(`\n${c.yellow}Listing cancelled.${c.reset}\n`);
1643
- console.log(`${c.dim}${'─'.repeat(60)}${c.reset}`);
1644
- console.log(` ${c.bold}Listing:${c.reset} ${listing.title || listingId}`);
1645
- console.log(` ${c.bold}Status:${c.reset} ${statusColor(listing.status || 'cancelled')}`);
1646
- console.log();
1647
- }
1648
- function showListingHelp() {
1649
- console.log(`
1650
- ${c.bold}${c.cyan}obolos listing${c.reset} — Agent-to-agent negotiation layer
28
+ obolos <command> [options]
29
+ obolos <group> <subcommand> [options]
1651
30
 
1652
- ${c.bold}Usage:${c.reset}
1653
- obolos listing list [options] Browse open job listings
1654
- obolos listing create [options] Create a new listing for agents to bid on
1655
- obolos listing info <id> Get listing details with all bids
1656
- obolos listing bid <id> [options] Submit a bid on a listing
1657
- obolos listing accept <id> [options] Accept a bid (auto-creates ACP job)
1658
- obolos listing cancel <id> Cancel a listing
31
+ ${c.bold}Top-level commands:${c.reset}
32
+ ${registry.all().filter(c => !c.name.includes('.')).map(c => ` ${c.name.padEnd(14)} ${c.summary}`).join('\n')}
1659
33
 
1660
- ${c.bold}List Options:${c.reset}
1661
- --status=open|negotiating|accepted|cancelled
1662
- --client=0x... Filter by client address
1663
- --limit=20 Max results (default: 20)
34
+ ${c.bold}Groups:${c.reset}
35
+ ${Object.entries(groups).filter(([, cmds]) => cmds.some(n => n.includes('.'))).map(([g, cmds]) => ` ${g.padEnd(14)} ${cmds.filter(n => n.includes('.')).length} subcommands (obolos ${g} --help)`).join('\n')}
1664
36
 
1665
- ${c.bold}Create Options:${c.reset}
1666
- --title "..." Listing title (required)
1667
- --description "..." Detailed description
1668
- --min-budget 1.00 Minimum budget in USDC
1669
- --max-budget 10.00 Maximum budget in USDC
1670
- --deadline 7d Bidding deadline (e.g., "24h", "7d")
1671
- --duration 24 Expected job duration in hours
1672
- --evaluator 0x... Preferred evaluator address
1673
- --hook 0x... Hook contract address
37
+ ${c.bold}Output:${c.reset}
38
+ --json Machine-readable JSON (stable schema, use this when scripting)
39
+ --dry-run Preview destructive actions without executing
40
+ -h, --help Show command help (includes JSON schema for MCP/scripting)
1674
41
 
1675
- ${c.bold}Bid Options:${c.reset}
1676
- --price 5.00 Your proposed price in USDC (required)
1677
- --delivery 24 Estimated delivery time in hours
1678
- --message "I can do this" Pitch to the client
1679
- --proposal-hash <hash> Hash of detailed proposal
42
+ ${c.bold}Config:${c.reset}
43
+ ~/.obolos/config.json (mode 0600) or OBOLOS_PRIVATE_KEY / OBOLOS_API_URL env vars.
44
+ Run ${c.cyan}obolos setup --generate${c.reset} to create a new wallet.
1680
45
 
1681
- ${c.bold}Accept Options:${c.reset}
1682
- --bid <bid_id> Bid ID to accept (required)
46
+ ${c.bold}MCP:${c.reset}
47
+ Every command above is also exposed as an MCP tool by @obolos_tech/mcp-server.
48
+ Run ${c.cyan}obolos setup-mcp${c.reset} for install + configuration instructions.
1683
49
 
1684
- ${c.bold}Examples:${c.reset}
1685
- obolos listing list --status=open
1686
- obolos listing create --title "Analyze dataset" --description "Parse and summarize CSV" --max-budget 10.00 --deadline 7d
1687
- obolos listing info abc123
1688
- obolos listing bid abc123 --price 5.00 --delivery 24 --message "I can do this in 12h"
1689
- obolos listing accept abc123 --bid bid456
1690
- obolos listing cancel abc123
50
+ ${c.bold}Docs:${c.reset} https://obolos.tech
1691
51
  `);
1692
52
  }
1693
- async function cmdListing(args) {
1694
- const sub = args[0];
1695
- const subArgs = args.slice(1);
1696
- switch (sub) {
1697
- case 'list':
1698
- case 'ls':
1699
- await cmdListingList(subArgs);
1700
- break;
1701
- case 'create':
1702
- case 'new':
1703
- await cmdListingCreate(subArgs);
1704
- break;
1705
- case 'info':
1706
- case 'show':
1707
- await cmdListingInfo(subArgs);
1708
- break;
1709
- case 'bid':
1710
- await cmdListingBid(subArgs);
1711
- break;
1712
- case 'accept':
1713
- await cmdListingAccept(subArgs);
1714
- break;
1715
- case 'cancel':
1716
- await cmdListingCancel(subArgs);
1717
- break;
1718
- case 'help':
1719
- case '--help':
1720
- case '-h':
1721
- case undefined:
1722
- showListingHelp();
1723
- break;
1724
- default:
1725
- console.error(`${c.red}Unknown listing subcommand: ${sub}${c.reset}`);
1726
- showListingHelp();
1727
- process.exit(1);
1728
- }
1729
- }
1730
- // ─── ANP Commands (Agent Negotiation Protocol) ──────────────────────────────
1731
- async function cmdAnpList(args) {
1732
- const params = new URLSearchParams();
1733
- const status = getFlag(args, 'status');
1734
- const limit = getFlag(args, 'limit') || '20';
1735
- if (status)
1736
- params.set('status', status);
1737
- params.set('limit', limit);
1738
- const data = await apiGet(`/api/anp/listings?${params}`);
1739
- const listings = data.listings || data.data || [];
1740
- if (listings.length === 0) {
1741
- console.log(`${c.yellow}No ANP listings found.${c.reset}`);
1742
- return;
1743
- }
1744
- const total = data.pagination?.total || data.total || listings.length;
1745
- console.log(`\n${c.bold}${c.cyan}ANP Listings${c.reset} ${c.dim}— ${total} listings${c.reset}\n`);
1746
- // Table header
1747
- console.log(` ${c.bold}${'CID'.padEnd(18)} ${'Title'.padEnd(28)} ${'Budget Range'.padEnd(20)} ${'Status'.padEnd(14)} ${'Bids'.padEnd(6)} Client${c.reset}`);
1748
- console.log(` ${c.dim}${'─'.repeat(100)}${c.reset}`);
1749
- for (const l of listings) {
1750
- const cid = (l.cid || l.id || '').slice(0, 16).padEnd(18);
1751
- const title = (l.title || 'Untitled').slice(0, 26).padEnd(28);
1752
- const minUsd = l.minBudgetUsd ?? l.min_budget_usd ?? (l.minBudget ? Number(l.minBudget) / 1e6 : null) ?? (l.min_budget ? Number(l.min_budget) / 1e6 : null);
1753
- const maxUsd = l.maxBudgetUsd ?? l.max_budget_usd ?? (l.maxBudget ? Number(l.maxBudget) / 1e6 : null) ?? (l.max_budget ? Number(l.max_budget) / 1e6 : null);
1754
- const budgetMin = minUsd != null ? `$${minUsd.toFixed(0)}` : '?';
1755
- const budgetMax = maxUsd != null ? `$${maxUsd.toFixed(0)}` : '?';
1756
- const budget = `${budgetMin}-${budgetMax}`.padEnd(20);
1757
- const st = statusColor((l.status || 'open').padEnd(12));
1758
- const bids = String(l.bidCount ?? l.bid_count ?? l.bids?.length ?? 0).padEnd(6);
1759
- const cl = shortenAddr(l.client_address || l.client || l.signer);
1760
- console.log(` ${cid} ${title} ${budget} ${st} ${bids} ${cl}`);
1761
- }
1762
- console.log(`\n${c.dim}Use: obolos anp info <cid> for full details${c.reset}\n`);
1763
- }
1764
- async function cmdAnpInfo(args) {
1765
- const cid = getPositional(args, 0);
1766
- if (!cid) {
1767
- console.error(`${c.red}Usage: obolos anp info <cid>${c.reset}`);
1768
- process.exit(1);
1769
- }
1770
- const data = await apiGet(`/api/anp/listings/${encodeURIComponent(cid)}`);
1771
- const listing = data.listing || data;
1772
- console.log(`\n${c.bold}${c.cyan}${listing.title || 'Untitled ANP Listing'}${c.reset}`);
1773
- console.log(`${c.dim}${'─'.repeat(60)}${c.reset}`);
1774
- console.log(` ${c.bold}CID:${c.reset} ${listing.cid || cid}`);
1775
- console.log(` ${c.bold}Status:${c.reset} ${statusColor(listing.status || 'open')}`);
1776
- console.log(` ${c.bold}Client:${c.reset} ${listing.client || listing.client_address || listing.signer || `${c.dim}—${c.reset}`}`);
1777
- const minRaw = listing.minBudget ?? listing.min_budget;
1778
- const maxRaw = listing.maxBudget ?? listing.max_budget;
1779
- const minUsd = listing.minBudgetUsd ?? (minRaw ? Number(minRaw) / 1e6 : null);
1780
- const maxUsd = listing.maxBudgetUsd ?? (maxRaw ? Number(maxRaw) / 1e6 : null);
1781
- if (minUsd != null || maxUsd != null) {
1782
- const min = minUsd != null ? `$${minUsd.toFixed(2)}` : '?';
1783
- const max = maxUsd != null ? `$${maxUsd.toFixed(2)}` : '?';
1784
- console.log(` ${c.bold}Budget:${c.reset} ${c.green}${min} – ${max} USDC${c.reset}`);
1785
- }
1786
- const deadlineRaw = listing.deadline;
1787
- if (deadlineRaw) {
1788
- const ts = Number(deadlineRaw);
1789
- const deadlineDate = new Date(ts > 1e12 ? ts : ts * 1000);
1790
- const now = new Date();
1791
- const expired = deadlineDate < now;
1792
- console.log(` ${c.bold}Deadline:${c.reset} ${expired ? c.red : c.dim}${formatDate(deadlineDate.toISOString())}${expired ? ' (passed)' : ''}${c.reset}`);
1793
- }
1794
- if (listing.job_duration || listing.jobDuration) {
1795
- const dur = listing.job_duration || listing.jobDuration;
1796
- console.log(` ${c.bold}Duration:${c.reset} ${dur >= 86400 ? `${Math.floor(dur / 86400)}d` : dur >= 3600 ? `${Math.floor(dur / 3600)}h` : `${dur}s`}`);
1797
- }
1798
- if (listing.preferred_evaluator || listing.preferredEvaluator) {
1799
- const ev = listing.preferred_evaluator || listing.preferredEvaluator;
1800
- if (ev !== ZERO_ADDRESS) {
1801
- console.log(` ${c.bold}Evaluator:${c.reset} ${ev}`);
1802
- }
1803
- }
1804
- if (listing.description) {
1805
- console.log(`\n ${c.bold}Description:${c.reset}`);
1806
- const descLines = listing.description.split('\n');
1807
- for (const line of descLines) {
1808
- console.log(` ${line}`);
1809
- }
1810
- }
1811
- if (listing.content_hash || listing.contentHash) {
1812
- console.log(` ${c.bold}Content Hash:${c.reset} ${c.dim}${listing.content_hash || listing.contentHash}${c.reset}`);
1813
- }
1814
- if (listing.nonce != null) {
1815
- console.log(` ${c.bold}Nonce:${c.reset} ${c.dim}${listing.nonce}${c.reset}`);
1816
- }
1817
- if (listing.signature) {
1818
- console.log(` ${c.bold}Signature:${c.reset} ${c.dim}${listing.signature.slice(0, 20)}...${c.reset}`);
1819
- }
1820
- // Bids
1821
- const bids = listing.bids || [];
1822
- if (bids.length > 0) {
1823
- console.log(`\n ${c.bold}${c.cyan}Bids (${bids.length})${c.reset}`);
1824
- console.log(` ${c.dim}${'─'.repeat(56)}${c.reset}`);
1825
- console.log(` ${c.bold}${'CID'.padEnd(18)} ${'Bidder'.padEnd(14)} ${'Price'.padEnd(12)} ${'Delivery'.padEnd(10)} Message${c.reset}`);
1826
- console.log(` ${c.dim}${'─'.repeat(56)}${c.reset}`);
1827
- for (const bid of bids) {
1828
- const bidCid = (bid.cid || bid.id || '').slice(0, 16).padEnd(18);
1829
- const bidder = shortenAddr(bid.provider || bid.signer || bid.provider_address);
1830
- const priceUsd = bid.priceUsd ?? (bid.price != null ? Number(bid.price) / 1e6 : null);
1831
- const price = priceUsd != null ? `${c.green}$${priceUsd.toFixed(2)}${c.reset}` : `${c.dim}—${c.reset}`;
1832
- const delivery = bid.deliveryTime || bid.delivery_time;
1833
- const deliveryStr = delivery ? (delivery >= 86400 ? `${Math.floor(delivery / 86400)}d` : delivery >= 3600 ? `${Math.floor(delivery / 3600)}h` : `${delivery}s`) : `${c.dim}—${c.reset}`;
1834
- const msg = (bid.message || '').slice(0, 40);
1835
- console.log(` ${bidCid} ${bidder.padEnd(14)} ${price.padEnd(12)} ${deliveryStr.padEnd(10)} ${c.dim}${msg}${c.reset}`);
1836
- }
1837
- }
1838
- else {
1839
- console.log(`\n ${c.dim}No bids yet.${c.reset}`);
1840
- }
1841
- // Actions
1842
- console.log();
1843
- const s = listing.status || 'open';
1844
- if (s === 'open' || s === 'negotiating') {
1845
- console.log(` ${c.bold}Actions:${c.reset}`);
1846
- console.log(` obolos anp bid ${cid} --price 5.00 ${c.dim}Submit a bid${c.reset}`);
1847
- if (bids.length > 0) {
1848
- console.log(` obolos anp accept ${cid} --bid <bid_cid> ${c.dim}Accept a bid${c.reset}`);
1849
- }
1850
- }
1851
- console.log(` obolos anp verify ${cid} ${c.dim}Verify document${c.reset}`);
1852
- console.log();
1853
- }
1854
- async function cmdAnpCreate(args) {
1855
- const title = getFlag(args, 'title');
1856
- const description = getFlag(args, 'description');
1857
- const minBudget = getFlag(args, 'min-budget');
1858
- const maxBudget = getFlag(args, 'max-budget');
1859
- const deadline = getFlag(args, 'deadline');
1860
- const duration = getFlag(args, 'duration');
1861
- const evaluator = getFlag(args, 'evaluator');
1862
- if (!title) {
1863
- console.error(`${c.red}Usage: obolos anp create --title "..." --description "..." --min-budget 5 --max-budget 50 --deadline 7d --duration 3d [--evaluator 0x...]${c.reset}`);
1864
- process.exit(1);
1865
- }
1866
- const anp = await getANPSigningClient();
1867
- // Compute content hash
1868
- const contentHash = await computeContentHash({ title, description: description || '' });
1869
- // Generate nonce
1870
- const nonce = generateNonce();
1871
- // Parse deadline to unix timestamp (seconds from now)
1872
- let deadlineTs;
1873
- if (deadline) {
1874
- const secs = parseTimeToSeconds(deadline);
1875
- deadlineTs = BigInt(Math.floor(Date.now() / 1000) + secs);
1876
- }
1877
- else {
1878
- deadlineTs = BigInt(Math.floor(Date.now() / 1000) + 7 * 86400); // default 7d
1879
- }
1880
- // Parse duration to seconds
1881
- let jobDuration;
1882
- if (duration) {
1883
- jobDuration = BigInt(parseTimeToSeconds(duration));
1884
- }
1885
- else {
1886
- jobDuration = BigInt(3 * 86400); // default 3d
1887
- }
1888
- const minBudgetWei = BigInt(Math.floor((minBudget ? parseFloat(minBudget) : 0) * 1e6));
1889
- const maxBudgetWei = BigInt(Math.floor((maxBudget ? parseFloat(maxBudget) : 0) * 1e6));
1890
- const preferredEvaluator = (evaluator || ZERO_ADDRESS);
1891
- const message = {
1892
- contentHash,
1893
- minBudget: minBudgetWei,
1894
- maxBudget: maxBudgetWei,
1895
- deadline: deadlineTs,
1896
- jobDuration: jobDuration,
1897
- preferredEvaluator,
1898
- nonce,
1899
- };
1900
- console.log(`\n ${c.dim}Signing ListingIntent...${c.reset}`);
1901
- const signature = await anp.walletClient.signTypedData({
1902
- account: anp.account,
1903
- domain: ANP_DOMAIN,
1904
- types: { ListingIntent: ANP_TYPES.ListingIntent },
1905
- primaryType: 'ListingIntent',
1906
- message,
1907
- });
1908
- console.log(` ${c.green}Signed.${c.reset} Publishing...`);
1909
- const document = {
1910
- protocol: 'anp/v1',
1911
- type: 'listing',
1912
- data: {
1913
- title,
1914
- description: description || '',
1915
- minBudget: minBudgetWei.toString(),
1916
- maxBudget: maxBudgetWei.toString(),
1917
- deadline: Number(deadlineTs),
1918
- jobDuration: Number(jobDuration),
1919
- preferredEvaluator,
1920
- nonce: Number(nonce),
1921
- },
1922
- signer: anp.account.address.toLowerCase(),
1923
- signature,
1924
- timestamp: Date.now(),
1925
- };
1926
- const data = await apiPost('/api/anp/publish', document);
1927
- const result = data.listing || data;
1928
- console.log(`\n${c.green}ANP listing published!${c.reset}\n`);
1929
- console.log(`${c.dim}${'─'.repeat(60)}${c.reset}`);
1930
- console.log(` ${c.bold}CID:${c.reset} ${result.cid || result.id}`);
1931
- console.log(` ${c.bold}Title:${c.reset} ${title}`);
1932
- console.log(` ${c.bold}Budget:${c.reset} ${c.green}$${(minBudget || '0')} – $${(maxBudget || '0')} USDC${c.reset}`);
1933
- console.log(` ${c.bold}Deadline:${c.reset} ${formatDate(new Date(Number(deadlineTs) * 1000).toISOString())}`);
1934
- console.log(` ${c.bold}Duration:${c.reset} ${duration || '3d'}`);
1935
- console.log(` ${c.bold}Signer:${c.reset} ${anp.account.address}`);
1936
- console.log(` ${c.bold}Signature:${c.reset} ${c.dim}${signature.slice(0, 20)}...${c.reset}`);
1937
- console.log(`\n${c.dim}Agents can bid with: obolos anp bid ${result.cid || result.id} --price 25 --delivery 48h${c.reset}\n`);
1938
- }
1939
- async function cmdAnpBid(args) {
1940
- const listingCid = getPositional(args, 0);
1941
- if (!listingCid) {
1942
- console.error(`${c.red}Usage: obolos anp bid <listing_cid> --price 25 --delivery 48h [--message "..."]${c.reset}`);
1943
- process.exit(1);
1944
- }
1945
- const price = getFlag(args, 'price');
1946
- if (!price) {
1947
- console.error(`${c.red}--price is required. Provide your bid amount in USDC.${c.reset}`);
1948
- process.exit(1);
1949
- }
1950
- const delivery = getFlag(args, 'delivery');
1951
- const message = getFlag(args, 'message');
1952
- const anp = await getANPSigningClient();
1953
- // Fetch listing document to compute listingHash
1954
- console.log(`\n ${c.dim}Fetching listing document...${c.reset}`);
1955
- const listingData = await apiGet(`/api/anp/objects/${encodeURIComponent(listingCid)}`);
1956
- const listingDoc = listingData;
1957
- const ld = listingDoc.data || listingDoc;
1958
- // Recompute listing content hash from title+description, then compute struct hash
1959
- const listingContentHash = await computeContentHash({ title: ld.title, description: ld.description });
1960
- const listingHash = anp.hashListingStruct({
1961
- contentHash: listingContentHash,
1962
- minBudget: BigInt(ld.minBudget || '0'),
1963
- maxBudget: BigInt(ld.maxBudget || '0'),
1964
- deadline: BigInt(ld.deadline || '0'),
1965
- jobDuration: BigInt(ld.jobDuration || '0'),
1966
- preferredEvaluator: (ld.preferredEvaluator || ZERO_ADDRESS),
1967
- nonce: BigInt(ld.nonce || '0'),
1968
- });
1969
- // Compute content hash for bid
1970
- const contentHash = await computeContentHash({ message: message || '', proposalCid: '' });
1971
- const nonce = generateNonce();
1972
- const priceWei = BigInt(Math.floor(parseFloat(price) * 1e6));
1973
- let deliveryTime;
1974
- if (delivery) {
1975
- deliveryTime = BigInt(parseTimeToSeconds(delivery));
1976
- }
1977
- else {
1978
- deliveryTime = BigInt(86400); // default 24h
1979
- }
1980
- const bidMessage = {
1981
- listingHash,
1982
- contentHash,
1983
- price: priceWei,
1984
- deliveryTime,
1985
- nonce,
1986
- };
1987
- console.log(` ${c.dim}Signing BidIntent...${c.reset}`);
1988
- const signature = await anp.walletClient.signTypedData({
1989
- account: anp.account,
1990
- domain: ANP_DOMAIN,
1991
- types: { BidIntent: ANP_TYPES.BidIntent },
1992
- primaryType: 'BidIntent',
1993
- message: bidMessage,
1994
- });
1995
- console.log(` ${c.green}Signed.${c.reset} Publishing...`);
1996
- const document = {
1997
- protocol: 'anp/v1',
1998
- type: 'bid',
1999
- data: {
2000
- listingCid,
2001
- listingHash,
2002
- price: priceWei.toString(),
2003
- deliveryTime: Number(deliveryTime),
2004
- message: message || '',
2005
- nonce: Number(nonce),
2006
- },
2007
- signer: anp.account.address.toLowerCase(),
2008
- signature,
2009
- timestamp: Date.now(),
2010
- };
2011
- const data = await apiPost('/api/anp/publish', document);
2012
- const result = data.bid || data;
2013
- console.log(`\n${c.green}ANP bid published!${c.reset}\n`);
2014
- console.log(`${c.dim}${'─'.repeat(60)}${c.reset}`);
2015
- console.log(` ${c.bold}CID:${c.reset} ${result.cid || result.id}`);
2016
- console.log(` ${c.bold}Listing:${c.reset} ${listingCid}`);
2017
- console.log(` ${c.bold}Price:${c.reset} ${c.green}$${parseFloat(price).toFixed(2)} USDC${c.reset}`);
2018
- console.log(` ${c.bold}Delivery:${c.reset} ${delivery || '24h'}`);
2019
- if (message) {
2020
- console.log(` ${c.bold}Message:${c.reset} ${message}`);
2021
- }
2022
- console.log(` ${c.bold}Signer:${c.reset} ${anp.account.address}`);
2023
- console.log(` ${c.bold}Signature:${c.reset} ${c.dim}${signature.slice(0, 20)}...${c.reset}`);
2024
- console.log(`\n${c.dim}The listing owner can accept with: obolos anp accept ${listingCid} --bid ${result.cid || result.id}${c.reset}\n`);
2025
- }
2026
- async function cmdAnpAccept(args) {
2027
- const listingCid = getPositional(args, 0);
2028
- if (!listingCid) {
2029
- console.error(`${c.red}Usage: obolos anp accept <listing_cid> --bid <bid_cid>${c.reset}`);
2030
- process.exit(1);
2031
- }
2032
- const bidCid = getFlag(args, 'bid');
2033
- if (!bidCid) {
2034
- console.error(`${c.red}--bid is required. Specify the bid CID to accept.${c.reset}`);
2035
- process.exit(1);
2036
- }
2037
- const anp = await getANPSigningClient();
2038
- // Fetch listing and bid documents
2039
- console.log(`\n ${c.dim}Fetching listing and bid documents...${c.reset}`);
2040
- const [listingData, bidData] = await Promise.all([
2041
- apiGet(`/api/anp/objects/${encodeURIComponent(listingCid)}`),
2042
- apiGet(`/api/anp/objects/${encodeURIComponent(bidCid)}`),
2043
- ]);
2044
- const ld = listingData.data || listingData;
2045
- const bd = bidData.data || bidData;
2046
- // Recompute listing content hash and struct hash
2047
- const listingContentHash = await computeContentHash({ title: ld.title, description: ld.description });
2048
- const listingHash = anp.hashListingStruct({
2049
- contentHash: listingContentHash,
2050
- minBudget: BigInt(ld.minBudget || '0'),
2051
- maxBudget: BigInt(ld.maxBudget || '0'),
2052
- deadline: BigInt(ld.deadline || '0'),
2053
- jobDuration: BigInt(ld.jobDuration || '0'),
2054
- preferredEvaluator: (ld.preferredEvaluator || ZERO_ADDRESS),
2055
- nonce: BigInt(ld.nonce || '0'),
2056
- });
2057
- // Recompute bid content hash and struct hash
2058
- const bidContentHash = await computeContentHash({ message: bd.message || '', proposalCid: bd.proposalCid || '' });
2059
- const bidHash = anp.hashBidStruct({
2060
- listingHash: (bd.listingHash || listingHash),
2061
- contentHash: bidContentHash,
2062
- price: BigInt(bd.price || '0'),
2063
- deliveryTime: BigInt(bd.deliveryTime || '0'),
2064
- nonce: BigInt(bd.nonce || '0'),
2065
- });
2066
- const nonce = generateNonce();
2067
- const acceptMessage = {
2068
- listingHash,
2069
- bidHash,
2070
- nonce,
2071
- };
2072
- console.log(` ${c.dim}Signing AcceptIntent...${c.reset}`);
2073
- const signature = await anp.walletClient.signTypedData({
2074
- account: anp.account,
2075
- domain: ANP_DOMAIN,
2076
- types: { AcceptIntent: ANP_TYPES.AcceptIntent },
2077
- primaryType: 'AcceptIntent',
2078
- message: acceptMessage,
2079
- });
2080
- console.log(` ${c.green}Signed.${c.reset} Publishing...`);
2081
- const document = {
2082
- protocol: 'anp/v1',
2083
- type: 'acceptance',
2084
- data: {
2085
- listingCid,
2086
- bidCid,
2087
- listingHash,
2088
- bidHash,
2089
- nonce: Number(nonce),
2090
- },
2091
- signer: anp.account.address.toLowerCase(),
2092
- signature,
2093
- timestamp: Date.now(),
2094
- };
2095
- const data = await apiPost('/api/anp/publish', document);
2096
- const result = data.accept || data;
2097
- console.log(`\n${c.green}Bid accepted! ANP agreement published.${c.reset}\n`);
2098
- console.log(`${c.dim}${'─'.repeat(60)}${c.reset}`);
2099
- console.log(` ${c.bold}CID:${c.reset} ${result.cid || result.id}`);
2100
- console.log(` ${c.bold}Listing:${c.reset} ${listingCid}`);
2101
- console.log(` ${c.bold}Bid:${c.reset} ${bidCid}`);
2102
- console.log(` ${c.bold}Signer:${c.reset} ${anp.account.address}`);
2103
- console.log(` ${c.bold}Signature:${c.reset} ${c.dim}${signature.slice(0, 20)}...${c.reset}`);
2104
- console.log(`\n${c.dim}The agreement is now verifiable on-chain.${c.reset}\n`);
2105
- }
2106
- async function cmdAnpVerify(args) {
2107
- const cid = getPositional(args, 0);
2108
- if (!cid) {
2109
- console.error(`${c.red}Usage: obolos anp verify <cid>${c.reset}`);
2110
- process.exit(1);
2111
- }
2112
- console.log(`\n ${c.dim}Verifying document...${c.reset}`);
2113
- const data = await apiGet(`/api/anp/verify/${encodeURIComponent(cid)}`);
2114
- console.log(`\n${c.bold}${c.cyan}ANP Document Verification${c.reset}`);
2115
- console.log(`${c.dim}${'─'.repeat(60)}${c.reset}`);
2116
- console.log(` ${c.bold}CID:${c.reset} ${cid}`);
2117
- console.log(` ${c.bold}Type:${c.reset} ${data.type || `${c.dim}—${c.reset}`}`);
2118
- console.log(` ${c.bold}Signer:${c.reset} ${data.signer || `${c.dim}—${c.reset}`}`);
2119
- if (data.valid || data.verified) {
2120
- console.log(` ${c.bold}Signature:${c.reset} ${c.green}Valid${c.reset}`);
2121
- }
2122
- else {
2123
- console.log(` ${c.bold}Signature:${c.reset} ${c.red}Invalid${c.reset}`);
2124
- }
2125
- if (data.content_valid != null) {
2126
- console.log(` ${c.bold}Content Hash:${c.reset} ${data.content_valid ? `${c.green}Matches${c.reset}` : `${c.red}Mismatch${c.reset}`}`);
2127
- }
2128
- if (data.chain_refs != null) {
2129
- console.log(` ${c.bold}Chain Refs:${c.reset} ${data.chain_refs ? `${c.green}Valid${c.reset}` : `${c.red}Invalid${c.reset}`}`);
2130
- }
2131
- if (data.details) {
2132
- console.log(`\n ${c.bold}Details:${c.reset}`);
2133
- const details = typeof data.details === 'string' ? data.details : JSON.stringify(data.details, null, 2);
2134
- for (const line of details.split('\n')) {
2135
- console.log(` ${c.dim}${line}${c.reset}`);
2136
- }
2137
- }
2138
- console.log();
2139
- }
2140
- function showAnpHelp() {
2141
- console.log(`
2142
- ${c.bold}${c.cyan}obolos anp${c.reset} — Agent Negotiation Protocol (EIP-712 signed documents)
53
+ function showGroupHelp(group) {
54
+ const subcommands = registry.all().filter(c => c.name.startsWith(`${group}.`));
55
+ if (subcommands.length === 0)
56
+ return false;
57
+ process.stdout.write(`
58
+ ${c.bold}${c.cyan}obolos ${group}${c.reset}
2143
59
 
2144
- ${c.bold}Usage:${c.reset}
2145
- obolos anp list [options] Browse ANP listings
2146
- obolos anp info <cid> Get listing details with bids
2147
- obolos anp create [options] Sign and publish a listing
2148
- obolos anp bid <cid> [options] Sign and publish a bid
2149
- obolos anp accept <cid> [options] Accept a bid (sign AcceptIntent)
2150
- obolos anp verify <cid> Verify document integrity
2151
-
2152
- ${c.bold}List Options:${c.reset}
2153
- --status=open|negotiating|accepted Filter by status
2154
- --limit=20 Max results (default: 20)
2155
-
2156
- ${c.bold}Create Options:${c.reset}
2157
- --title "..." Listing title (required)
2158
- --description "..." Detailed description
2159
- --min-budget 5 Minimum budget in USDC
2160
- --max-budget 50 Maximum budget in USDC
2161
- --deadline 7d Bidding deadline (e.g., "24h", "7d")
2162
- --duration 3d Expected job duration (e.g., "48h", "3d")
2163
- --evaluator 0x... Preferred evaluator address
2164
-
2165
- ${c.bold}Bid Options:${c.reset}
2166
- --price 25 Your proposed price in USDC (required)
2167
- --delivery 48h Estimated delivery time (e.g., "24h", "3d")
2168
- --message "I can do this" Message to the client
2169
-
2170
- ${c.bold}Accept Options:${c.reset}
2171
- --bid <bid_cid> Bid CID to accept (required)
60
+ ${c.bold}Subcommands:${c.reset}
61
+ ${subcommands.map(c => ` ${c.name.slice(group.length + 1).padEnd(16)} ${c.summary}`).join('\n')}
2172
62
 
2173
- ${c.bold}Examples:${c.reset}
2174
- obolos anp list --status=open
2175
- obolos anp create --title "Analyze dataset" --description "Parse CSV" --min-budget 5 --max-budget 50 --deadline 7d --duration 3d
2176
- obolos anp info sha256-abc123...
2177
- obolos anp bid sha256-abc123... --price 25 --delivery 48h --message "I can do this"
2178
- obolos anp accept sha256-listing... --bid sha256-bid...
2179
- obolos anp verify sha256-abc123...
63
+ Run ${c.cyan}obolos ${group} <subcommand> --help${c.reset} for subcommand details.
2180
64
  `);
65
+ return true;
2181
66
  }
2182
- async function cmdAnp(args) {
2183
- const sub = args[0];
2184
- const subArgs = args.slice(1);
2185
- switch (sub) {
2186
- case 'list':
2187
- case 'ls':
2188
- await cmdAnpList(subArgs);
2189
- break;
2190
- case 'info':
2191
- case 'show':
2192
- await cmdAnpInfo(subArgs);
2193
- break;
2194
- case 'create':
2195
- case 'new':
2196
- await cmdAnpCreate(subArgs);
2197
- break;
2198
- case 'bid':
2199
- await cmdAnpBid(subArgs);
2200
- break;
2201
- case 'accept':
2202
- await cmdAnpAccept(subArgs);
2203
- break;
2204
- case 'verify':
2205
- await cmdAnpVerify(subArgs);
2206
- break;
2207
- case 'help':
2208
- case '--help':
2209
- case '-h':
2210
- case undefined:
2211
- showAnpHelp();
2212
- break;
2213
- default:
2214
- console.error(`${c.red}Unknown anp subcommand: ${sub}${c.reset}`);
2215
- showAnpHelp();
2216
- process.exit(1);
2217
- }
2218
- }
2219
- // ─── Reputation Commands ─────────────────────────────────────────────────────
2220
- function tierColor(tier) {
2221
- switch (tier.toLowerCase()) {
2222
- case 'diamond': return `${c.bold}${c.cyan}${tier}${c.reset}`;
2223
- case 'platinum': return `${c.bold}${c.white}${tier}${c.reset}`;
2224
- case 'gold':
2225
- case 'established': return `${c.yellow}${tier}${c.reset}`;
2226
- case 'silver':
2227
- case 'developing': return `${c.dim}${c.white}${tier}${c.reset}`;
2228
- case 'bronze':
2229
- case 'limited': return `${c.dim}${tier}${c.reset}`;
2230
- case 'flagged': return `${c.red}${tier}${c.reset}`;
2231
- default: return `${c.dim}${tier}${c.reset}`;
2232
- }
2233
- }
2234
- function scoreBar(score) {
2235
- const width = 20;
2236
- const filled = Math.round((score / 100) * width);
2237
- const empty = width - filled;
2238
- let color = c.red;
2239
- if (score >= 70)
2240
- color = c.green;
2241
- else if (score >= 40)
2242
- color = c.yellow;
2243
- return `${color}${'█'.repeat(filled)}${c.dim}${'░'.repeat(empty)}${c.reset} ${color}${score}${c.reset}/100`;
2244
- }
2245
- function verdictLabel(pass) {
2246
- return pass
2247
- ? `${c.green}✔ PASS${c.reset}`
2248
- : `${c.red}✘ FAIL${c.reset}`;
2249
- }
2250
- async function cmdReputationCheck(args) {
2251
- const agentId = getPositional(args, 0);
2252
- if (!agentId) {
2253
- console.error(`${c.red}Usage: obolos reputation check <agentId> [--chain base]${c.reset}`);
2254
- process.exit(1);
2255
- }
2256
- const chain = getFlag(args, 'chain') || 'base';
2257
- console.log(`\n${c.dim}Checking reputation for agent ${c.bold}${agentId}${c.reset}${c.dim} on ${chain}...${c.reset}\n`);
2258
- const data = await apiGet(`/api/anp/reputation/${encodeURIComponent(agentId)}?chain=${encodeURIComponent(chain)}`);
2259
- // Header
2260
- console.log(`${c.bold}${c.cyan}Reputation Report${c.reset} ${c.dim}Agent ${agentId}${c.reset}`);
2261
- console.log(`${c.dim}${'─'.repeat(60)}${c.reset}`);
2262
- // Combined score
2263
- const combined = data.combined || {};
2264
- console.log(` ${c.bold}Combined Score:${c.reset} ${scoreBar(combined.score ?? 0)}`);
2265
- console.log(` ${c.bold}Tier:${c.reset} ${tierColor(combined.tier ?? 'unknown')}`);
2266
- console.log(` ${c.bold}Verdict:${c.reset} ${verdictLabel(combined.pass ?? false)}`);
2267
- console.log(` ${c.bold}Chain:${c.reset} ${data.chain || chain}`);
2268
- if (data.address) {
2269
- console.log(` ${c.bold}Address:${c.reset} ${data.address}`);
2270
- }
2271
- // Sybil warning
2272
- if (combined.hasSybilFlags) {
2273
- console.log(`\n ${c.red}${c.bold}⚠ Sybil flags detected${c.reset}`);
2274
- }
2275
- // Individual provider scores
2276
- const scores = data.scores || [];
2277
- if (scores.length > 0) {
2278
- console.log(`\n ${c.bold}${c.cyan}Provider Scores (${scores.length})${c.reset}`);
2279
- console.log(` ${c.dim}${'─'.repeat(56)}${c.reset}`);
2280
- for (const s of scores) {
2281
- const provider = s.provider === 'rnwy' ? 'RNWY' : s.provider === 'agentproof' ? 'AgentProof' : s.provider;
2282
- console.log(`\n ${c.bold}${provider}${c.reset}`);
2283
- console.log(` Score: ${scoreBar(s.score ?? 0)}`);
2284
- console.log(` Tier: ${tierColor(s.tier ?? 'unknown')}`);
2285
- console.log(` Verdict: ${verdictLabel(s.pass ?? false)}`);
2286
- if (s.sybilFlags && s.sybilFlags.length > 0) {
2287
- console.log(` ${c.red}Sybil: ${s.sybilFlags.join(', ')}${c.reset}`);
2288
- }
2289
- if (s.riskFlags && s.riskFlags.length > 0) {
2290
- console.log(` ${c.yellow}Risk: ${s.riskFlags.join(', ')}${c.reset}`);
2291
- }
2292
- }
2293
- }
2294
- else {
2295
- console.log(`\n ${c.dim}No provider scores available.${c.reset}`);
2296
- }
2297
- console.log(`\n ${c.dim}Checked: ${data.checkedAt ? formatDate(data.checkedAt) : 'just now'}${c.reset}\n`);
2298
- }
2299
- async function cmdReputationCompare(args) {
2300
- // Collect all positional args (agent IDs, optionally prefixed with chain:)
2301
- const agents = [];
2302
- for (const arg of args) {
2303
- if (arg.startsWith('--'))
2304
- continue;
2305
- const parts = arg.split(':');
2306
- if (parts.length === 2) {
2307
- const id = parseInt(parts[1], 10);
2308
- if (isNaN(id)) {
2309
- console.error(`${c.red}Invalid agent ID: ${parts[1]}${c.reset}`);
2310
- process.exit(1);
2311
- }
2312
- agents.push({ agentId: id, chain: parts[0] });
2313
- }
2314
- else {
2315
- const id = parseInt(parts[0], 10);
2316
- if (isNaN(id)) {
2317
- console.error(`${c.red}Invalid agent ID: ${parts[0]}${c.reset}`);
2318
- process.exit(1);
2319
- }
2320
- agents.push({ agentId: id, chain: 'base' });
2321
- }
2322
- }
2323
- if (agents.length < 2) {
2324
- console.error(`${c.red}Usage: obolos reputation compare <id1> <id2> [id3...]${c.reset}`);
2325
- console.error(`${c.dim} Prefix with chain: obolos rep compare base:123 ethereum:456${c.reset}`);
2326
- process.exit(1);
67
+ async function main() {
68
+ if (!command || command === 'help' || command === '--help' || command === '-h') {
69
+ showHelp();
70
+ return;
2327
71
  }
2328
- console.log(`\n${c.dim}Comparing ${agents.length} agents in parallel...${c.reset}\n`);
2329
- // Fetch all in parallel
2330
- const results = await Promise.all(agents.map(a => apiGet(`/api/anp/reputation/${a.agentId}?chain=${encodeURIComponent(a.chain)}`)
2331
- .catch((err) => ({ agentId: a.agentId, chain: a.chain, error: err.message }))));
2332
- // Sort by combined score descending
2333
- const sorted = results
2334
- .map((r, i) => ({ ...r, _input: agents[i] }))
2335
- .sort((a, b) => ((b.combined?.score ?? -1) - (a.combined?.score ?? -1)));
2336
- // Table header
2337
- console.log(`${c.bold}${c.cyan}Reputation Comparison${c.reset}`);
2338
- console.log(`${c.dim}${'─'.repeat(74)}${c.reset}`);
2339
- console.log(` ${c.bold}${'#'.padEnd(4)}${'Agent'.padEnd(12)}${'Chain'.padEnd(12)}${'Score'.padEnd(24)}${'Tier'.padEnd(14)}Verdict${c.reset}`);
2340
- console.log(` ${c.dim}${'─'.repeat(70)}${c.reset}`);
2341
- sorted.forEach((r, i) => {
2342
- const rank = `${i + 1}.`.padEnd(4);
2343
- const agent = String(r._input.agentId).padEnd(12);
2344
- const chain = r._input.chain.padEnd(12);
2345
- if (r.error) {
2346
- console.log(` ${rank}${agent}${chain}${c.red}Error: ${r.error}${c.reset}`);
72
+ // Group help: `obolos job --help` / `obolos anp --help`.
73
+ if (commandArgs.length === 0 || commandArgs[0] === '--help' || commandArgs[0] === '-h') {
74
+ if (showGroupHelp(command))
2347
75
  return;
2348
- }
2349
- const combined = r.combined || {};
2350
- const score = combined.score ?? 0;
2351
- const bar = scoreBar(score);
2352
- // scoreBar has ANSI codes so we can't pad it normally; we pad the raw number
2353
- const barPadded = bar; // already formatted
2354
- const tier = tierColor(combined.tier ?? 'unknown');
2355
- const verdict = verdictLabel(combined.pass ?? false);
2356
- const sybil = combined.hasSybilFlags ? ` ${c.red}⚠ sybil${c.reset}` : '';
2357
- console.log(` ${rank}${agent}${chain}${barPadded} ${tier.padEnd(14)} ${verdict}${sybil}`);
2358
- });
2359
- console.log(`${c.dim}${'─'.repeat(74)}${c.reset}\n`);
2360
- }
2361
- function showReputationHelp() {
2362
- console.log(`
2363
- ${c.bold}${c.cyan}obolos reputation${c.reset} — Agent trust & reputation checking
2364
-
2365
- ${c.bold}Subcommands:${c.reset}
2366
- check <agentId> Check reputation for an agent
2367
- compare <id1> <id2> [...] Compare multiple agents side-by-side
2368
-
2369
- ${c.bold}Options (check):${c.reset}
2370
- --chain <chain> Blockchain to check (default: base)
2371
-
2372
- ${c.bold}Examples:${c.reset}
2373
- obolos reputation check 16907
2374
- obolos reputation check 16907 --chain ethereum
2375
- obolos rep check 16907
2376
- obolos reputation compare 123 456 789
2377
- obolos rep compare base:123 base:456 ethereum:789
2378
- `);
2379
- }
2380
- async function cmdReputation(args) {
2381
- const sub = args[0];
2382
- const subArgs = args.slice(1);
2383
- switch (sub) {
2384
- case 'check':
2385
- await cmdReputationCheck(subArgs);
2386
- break;
2387
- case 'compare':
2388
- case 'cmp':
2389
- await cmdReputationCompare(subArgs);
2390
- break;
2391
- case 'help':
2392
- case '--help':
2393
- case '-h':
2394
- case undefined:
2395
- showReputationHelp();
2396
- break;
2397
- default:
2398
- console.error(`${c.red}Unknown reputation subcommand: ${sub}${c.reset}`);
2399
- showReputationHelp();
2400
- process.exit(1);
2401
76
  }
2402
- }
2403
- // ─── Help ───────────────────────────────────────────────────────────────────
2404
- function showHelp() {
2405
- console.log(`
2406
- ${c.bold}${c.cyan}obolos${c.reset} — CLI for the Obolos x402 API Marketplace
2407
-
2408
- ${c.bold}Usage:${c.reset}
2409
- obolos search [query] Search APIs by keyword
2410
- obolos categories List all API categories
2411
- obolos info <id> Get full API details
2412
- obolos call <id> [options] Call an API with payment
2413
- obolos balance Check wallet USDC balance
2414
- obolos setup Configure wallet (interactive)
2415
- obolos setup --generate Generate a new wallet
2416
- obolos setup --show Show current wallet config
2417
- obolos setup-mcp Show MCP server setup instructions
2418
-
2419
- ${c.bold}Job Commands (ERC-8183 ACP):${c.reset}
2420
- obolos job list [options] List jobs with filters
2421
- obolos job create [options] Create a new job
2422
- obolos job info <id> Get full job details
2423
- obolos job fund <id> Fund a job's escrow
2424
- obolos job submit <id> [opts] Submit work for a job
2425
- obolos job complete <id> Approve a job (evaluator)
2426
- obolos job reject <id> Reject a job submission
2427
- obolos job help Show job command help
2428
-
2429
- ${c.bold}Listing Commands (Negotiation):${c.reset}
2430
- obolos listing list [options] Browse open job listings
2431
- obolos listing create [opts] Create a listing for bids
2432
- obolos listing info <id> Get listing details + bids
2433
- obolos listing bid <id> [opts] Submit a bid on a listing
2434
- obolos listing accept <id> Accept a bid (creates job)
2435
- obolos listing cancel <id> Cancel a listing
2436
- obolos listing help Show listing command help
2437
-
2438
- ${c.bold}ANP Commands (Agent Negotiation Protocol):${c.reset}
2439
- obolos anp list [options] Browse ANP listings
2440
- obolos anp info <cid> Get listing details + bids
2441
- obolos anp create [options] Sign and publish a listing
2442
- obolos anp bid <cid> [opts] Sign and publish a bid
2443
- obolos anp accept <cid> [opts] Accept a bid (sign AcceptIntent)
2444
- obolos anp verify <cid> Verify document integrity
2445
- obolos anp help Show ANP command help
2446
-
2447
- ${c.bold}Reputation Commands:${c.reset}
2448
- obolos reputation check <id> Check agent trust score
2449
- obolos reputation compare ... Compare multiple agents
2450
- obolos reputation help Show reputation command help
2451
- ${c.dim}(alias: obolos rep ...)${c.reset}
2452
-
2453
- ${c.bold}Call Options:${c.reset}
2454
- --method POST|GET|PUT HTTP method (default: GET)
2455
- --body '{"key":"value"}' Request body (JSON)
2456
-
2457
- ${c.bold}Config:${c.reset}
2458
- Wallet key is loaded from ~/.obolos/config.json or OBOLOS_PRIVATE_KEY env var.
2459
- Run ${c.cyan}obolos setup${c.reset} to configure.
2460
-
2461
- ${c.bold}Examples:${c.reset}
2462
- obolos setup --generate
2463
- obolos search "token price"
2464
- obolos info a59a0377-d77b-4fee-...
2465
- obolos call a59a0377-... --body '{"prompt":"a cat in space"}'
2466
- obolos job list --status=open
2467
- obolos job create --title "Analyze data" --evaluator 0xABC... --budget 5.00
2468
- obolos listing list --status=open
2469
- obolos listing create --title "Parse CSV data" --max-budget 10.00 --deadline 7d
2470
- obolos listing bid abc123 --price 5.00 --message "I can do this"
2471
- obolos listing accept abc123 --bid bid456
2472
- obolos anp list --status=open
2473
- obolos anp create --title "Analyze data" --min-budget 5 --max-budget 50 --deadline 7d
2474
- obolos anp bid sha256-abc... --price 25 --delivery 48h --message "I can do this"
2475
- obolos anp accept sha256-listing... --bid sha256-bid...
2476
- obolos rep check 16907
2477
- obolos rep check 16907 --chain ethereum
2478
- obolos rep compare 123 456 789
2479
- obolos rep compare base:123 ethereum:456
2480
- `);
2481
- }
2482
- // ─── Main ───────────────────────────────────────────────────────────────────
2483
- const args = process.argv.slice(2);
2484
- const command = args[0];
2485
- const commandArgs = args.slice(1);
2486
- async function main() {
2487
- switch (command) {
2488
- case 'search':
2489
- case 's':
2490
- await cmdSearch(commandArgs);
2491
- break;
2492
- case 'categories':
2493
- case 'cats':
2494
- await cmdCategories();
2495
- break;
2496
- case 'info':
2497
- case 'i':
2498
- await cmdInfo(commandArgs);
2499
- break;
2500
- case 'call':
2501
- case 'c':
2502
- await cmdCall(commandArgs);
2503
- break;
2504
- case 'balance':
2505
- case 'bal':
2506
- await cmdBalance();
2507
- break;
2508
- case 'setup':
2509
- await cmdSetup(commandArgs);
2510
- break;
2511
- case 'setup-mcp':
2512
- case 'mcp':
2513
- await cmdSetupMcp();
2514
- break;
2515
- case 'job':
2516
- case 'j':
2517
- await cmdJob(commandArgs);
2518
- break;
2519
- case 'listing':
2520
- case 'l':
2521
- await cmdListing(commandArgs);
2522
- break;
2523
- case 'anp':
2524
- await cmdAnp(commandArgs);
2525
- break;
2526
- case 'reputation':
2527
- case 'rep':
2528
- await cmdReputation(commandArgs);
2529
- break;
2530
- case 'help':
2531
- case '--help':
2532
- case '-h':
2533
- case undefined:
2534
- showHelp();
2535
- break;
2536
- default:
2537
- console.error(`${c.red}Unknown command: ${command}${c.reset}`);
2538
- showHelp();
2539
- process.exit(1);
77
+ const result = await dispatch(command, commandArgs);
78
+ if (result.handled) {
79
+ if (result.exitCode && result.exitCode !== 0)
80
+ process.exit(result.exitCode);
81
+ return;
2540
82
  }
83
+ process.stderr.write(`${c.red}Unknown command: ${command}${c.reset}\n`);
84
+ showHelp();
85
+ process.exit(1);
2541
86
  }
2542
87
  main().catch((err) => {
2543
- console.error(`${c.red}Error: ${err.message}${c.reset}`);
88
+ process.stderr.write(`${c.red}Error: ${err.message}${c.reset}\n`);
2544
89
  process.exit(1);
2545
90
  });
2546
91
  //# sourceMappingURL=index.js.map