@obolos_tech/cli 0.4.0 → 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 -2847
  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 +45 -3
  72. package/scripts/lint-stdout-purity.mjs +62 -0
package/dist/index.js CHANGED
@@ -1,2877 +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, hashAmendmentIntent, hashCheckpointIntent, usdToUsdc } 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, hashAmendmentStruct: hashAmendmentIntent, hashCheckpointStruct: hashCheckpointIntent };
361
- }
362
- function generateNonce() {
363
- return BigInt(Math.floor(Math.random() * 2 ** 32));
364
- }
365
- async function computeJobHash(jobId) {
366
- return computeContentHash({ jobId });
367
- }
368
- // ─── Commands ───────────────────────────────────────────────────────────────
369
- async function cmdSearch(args) {
370
- const query = args.join(' ');
371
- const params = new URLSearchParams();
372
- if (query)
373
- params.set('q', query);
374
- params.set('limit', '25');
375
- params.set('type', 'native');
376
- const data = await apiGet(`/api/marketplace/apis/search?${params}`);
377
- const apis = data.apis;
378
- if (apis.length === 0) {
379
- console.log(`${c.yellow}No APIs found${query ? ` for "${query}"` : ''}.${c.reset}`);
380
- return;
381
- }
382
- console.log(`\n${c.bold}${c.cyan}Obolos Marketplace${c.reset} ${c.dim}— ${data.pagination.total} APIs found${c.reset}\n`);
383
- for (const api of apis) {
384
- const name = (api.name || 'Unnamed').slice(0, 50);
385
- const price = `$${api.price_per_call.toFixed(4)}`;
386
- const cat = api.category;
387
- const id = api.id;
388
- console.log(` ${c.bold}${name}${c.reset}`);
389
- console.log(` ${c.green}${price}${c.reset} ${c.dim}${cat}${c.reset} ${c.cyan}${id}${c.reset}\n`);
390
- }
391
- console.log(`${c.dim}Use: obolos info <id> for details, or copy the full ID to call it${c.reset}\n`);
392
- }
393
- async function cmdCategories() {
394
- const data = await apiGet('/api/marketplace/categories');
395
- console.log(`\n${c.bold}${c.cyan}API Categories${c.reset}\n`);
396
- for (const cat of data.categories) {
397
- const bar = '█'.repeat(Math.min(50, Math.ceil(cat.count / 5)));
398
- console.log(` ${cat.name.padEnd(25)} ${c.green}${String(cat.count).padStart(4)}${c.reset} ${c.dim}${bar}${c.reset}`);
399
- }
400
- console.log(`\n ${c.bold}Total:${c.reset} ${data.nativeCount} native + ${data.externalCount} external\n`);
401
- }
402
- async function cmdInfo(args) {
403
- const id = args[0];
404
- if (!id) {
405
- console.error(`${c.red}Usage: obolos info <api-id>${c.reset}`);
406
- process.exit(1);
407
- }
408
- const api = await apiGet(`/api/marketplace/apis/${encodeURIComponent(id)}`);
409
- console.log(`\n${c.bold}${c.cyan}${api.name}${c.reset}`);
410
- console.log(`${c.dim}${'─'.repeat(60)}${c.reset}`);
411
- console.log(` ${c.bold}ID:${c.reset} ${api.id}`);
412
- console.log(` ${c.bold}Type:${c.reset} ${api.api_type}`);
413
- console.log(` ${c.bold}Price:${c.reset} ${formatPrice(api.price_per_call)}`);
414
- console.log(` ${c.bold}Method:${c.reset} ${api.http_method}`);
415
- console.log(` ${c.bold}Category:${c.reset} ${api.category}`);
416
- console.log(` ${c.bold}Seller:${c.reset} ${api.seller_name}`);
417
- console.log(` ${c.bold}Calls:${c.reset} ${api.total_calls}`);
418
- if (api.average_rating) {
419
- console.log(` ${c.bold}Rating:${c.reset} ${api.average_rating.toFixed(1)}/5 (${api.review_count} reviews)`);
420
- }
421
- if (api.description) {
422
- console.log(`\n ${c.bold}Description:${c.reset}`);
423
- console.log(` ${api.description}`);
424
- }
425
- if (api.input_schema?.fields && Object.keys(api.input_schema.fields).length > 0) {
426
- console.log(`\n ${c.bold}Input Fields:${c.reset}`);
427
- for (const [name, field] of Object.entries(api.input_schema.fields)) {
428
- const req = field.required ? `${c.red}*${c.reset}` : ' ';
429
- const ex = field.example ? `${c.dim}(e.g. ${JSON.stringify(field.example)})${c.reset}` : '';
430
- console.log(` ${req} ${c.cyan}${name}${c.reset}: ${field.type} ${ex}`);
431
- }
432
- }
433
- if (api.example_request) {
434
- console.log(`\n ${c.bold}Example Request:${c.reset}`);
435
- try {
436
- console.log(` ${c.dim}${JSON.stringify(JSON.parse(api.example_request), null, 2).replace(/\n/g, '\n ')}${c.reset}`);
437
- }
438
- catch {
439
- console.log(` ${c.dim}${api.example_request}${c.reset}`);
440
- }
441
- }
442
- if (api.example_response) {
443
- console.log(`\n ${c.bold}Example Response:${c.reset}`);
444
- try {
445
- const parsed = JSON.parse(api.example_response);
446
- const formatted = JSON.stringify(parsed, null, 2);
447
- // Truncate long responses
448
- const lines = formatted.split('\n');
449
- if (lines.length > 20) {
450
- console.log(` ${c.dim}${lines.slice(0, 20).join('\n ')}\n ... (${lines.length - 20} more lines)${c.reset}`);
451
- }
452
- else {
453
- console.log(` ${c.dim}${formatted.replace(/\n/g, '\n ')}${c.reset}`);
454
- }
455
- }
456
- catch {
457
- console.log(` ${c.dim}${api.example_response.slice(0, 500)}${c.reset}`);
458
- }
459
- }
460
- console.log(`\n ${c.bold}Call:${c.reset} obolos call ${api.id}${api.http_method === 'POST' ? " --body '{...}'" : ''}`);
461
- console.log(` ${c.bold}Proxy:${c.reset} ${OBOLOS_API_URL}/api/proxy/${api.id}`);
462
- if (api.slug) {
463
- console.log(` ${c.bold}Slug:${c.reset} ${OBOLOS_API_URL}/api/${api.slug}`);
464
- }
465
- 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`);
466
- }
467
- async function cmdCall(args) {
468
- const id = args[0];
469
- if (!id) {
470
- console.error(`${c.red}Usage: obolos call <api-id> [--body '{"key":"value"}'] [--method POST]${c.reset}`);
471
- process.exit(1);
472
- }
473
- // Parse flags
474
- let method = 'GET';
475
- let body = undefined;
476
- for (let i = 1; i < args.length; i++) {
477
- if (args[i] === '--method' && args[i + 1]) {
478
- method = args[++i].toUpperCase();
479
- }
480
- else if (args[i] === '--body' && args[i + 1]) {
481
- try {
482
- body = JSON.parse(args[++i]);
483
- }
484
- catch {
485
- console.error(`${c.red}Invalid JSON body${c.reset}`);
486
- process.exit(1);
487
- }
488
- }
489
- }
490
- if (!OBOLOS_PRIVATE_KEY) {
491
- // Free call attempt (for free APIs or to see the 402 response)
492
- console.log(`${c.yellow}No wallet configured — attempting without payment${c.reset}`);
493
- console.log(`${c.dim}Run "obolos setup" to configure a wallet for paid APIs${c.reset}`);
494
- }
495
- const url = `${OBOLOS_API_URL}/api/proxy/${encodeURIComponent(id)}`;
496
- const fetchOpts = { method };
497
- if (body && method !== 'GET') {
498
- fetchOpts.headers = { 'Content-Type': 'application/json' };
499
- fetchOpts.body = JSON.stringify(body);
500
- }
501
- console.log(`\n${c.dim}${method} ${url}${c.reset}`);
502
- let res = await fetch(url, fetchOpts);
503
- if (res.status === 402 && OBOLOS_PRIVATE_KEY) {
504
- console.log(`${c.yellow}402 Payment Required — signing payment...${c.reset}`);
505
- let paymentInfo;
506
- try {
507
- paymentInfo = await res.json();
508
- }
509
- catch {
510
- console.error(`${c.red}Could not parse 402 response${c.reset}`);
511
- process.exit(1);
512
- }
513
- // Dynamic import viem for signing
514
- const { createWalletClient, http: viemHttp, keccak256, encodePacked } = await import('viem');
515
- const { privateKeyToAccount } = await import('viem/accounts');
516
- const { base } = await import('viem/chains');
517
- const key = OBOLOS_PRIVATE_KEY.startsWith('0x')
518
- ? OBOLOS_PRIVATE_KEY
519
- : `0x${OBOLOS_PRIVATE_KEY}`;
520
- const account = privateKeyToAccount(key);
521
- const client = createWalletClient({ account, chain: base, transport: viemHttp() });
522
- const accepts = paymentInfo.accepts?.[0];
523
- if (!accepts) {
524
- console.error(`${c.red}No payment options in 402 response${c.reset}`);
525
- process.exit(1);
526
- }
527
- const amount = BigInt(accepts.maxAmountRequired || accepts.amount || '0');
528
- const payTo = accepts.payTo;
529
- const asset = accepts.asset || '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913';
530
- const scheme = accepts.scheme || 'exact';
531
- const rawNetwork = accepts.network || 'base';
532
- const network = rawNetwork.startsWith('eip155:') ? rawNetwork : 'eip155:8453';
533
- const deadline = BigInt(Math.floor(Date.now() / 1000) + 300);
534
- // EIP-712 domain must match the USDC contract's domain (not "x402")
535
- const domain = {
536
- name: accepts.extra?.name || 'USD Coin',
537
- version: accepts.extra?.version || '2',
538
- chainId: 8453n,
539
- verifyingContract: asset,
540
- };
541
- const types = {
542
- TransferWithAuthorization: [
543
- { name: 'from', type: 'address' },
544
- { name: 'to', type: 'address' },
545
- { name: 'value', type: 'uint256' },
546
- { name: 'validAfter', type: 'uint256' },
547
- { name: 'validBefore', type: 'uint256' },
548
- { name: 'nonce', type: 'bytes32' },
549
- ],
550
- };
551
- // Check for v2 router settlement extension
552
- const settlementKey = 'x402x-router-settlement';
553
- const settlementExt = accepts.extra?.[settlementKey];
554
- const settlementInfo = settlementExt?.info;
555
- // Determine nonce: commitment hash for router settlement, random otherwise
556
- let nonce;
557
- if (settlementInfo?.settlementRouter && settlementInfo?.salt) {
558
- nonce = keccak256(encodePacked(['string', 'uint256', 'address', 'address', 'address', 'uint256', 'uint256', 'uint256', 'bytes32', 'address', 'uint256', 'address', 'bytes32'], [
559
- 'X402/settle/v1',
560
- 8453n,
561
- settlementInfo.settlementRouter,
562
- asset,
563
- account.address,
564
- BigInt(amount),
565
- 0n,
566
- deadline,
567
- settlementInfo.salt,
568
- (settlementInfo.finalPayTo || payTo),
569
- BigInt(settlementInfo.facilitatorFee || '0'),
570
- settlementInfo.hook,
571
- keccak256(settlementInfo.hookData),
572
- ]));
573
- }
574
- else {
575
- const nonceBytes = new Uint8Array(32);
576
- crypto.getRandomValues(nonceBytes);
577
- nonce = `0x${Array.from(nonceBytes).map(b => b.toString(16).padStart(2, '0')).join('')}`;
578
- }
579
- const signature = await client.signTypedData({
580
- account,
581
- domain,
582
- types,
583
- primaryType: 'TransferWithAuthorization',
584
- message: {
585
- from: account.address,
586
- to: payTo,
587
- value: BigInt(amount),
588
- validAfter: 0n,
589
- validBefore: deadline,
590
- nonce,
591
- },
592
- });
593
- const authorization = {
594
- from: account.address,
595
- to: payTo,
596
- value: amount.toString(),
597
- validAfter: '0',
598
- validBefore: deadline.toString(),
599
- nonce,
600
- };
601
- let encoded;
602
- let headerName;
603
- if (paymentInfo.x402Version === 2) {
604
- const paymentPayload = {
605
- x402Version: 2,
606
- scheme,
607
- network,
608
- payload: { signature, authorization },
609
- accepted: { ...accepts, network },
610
- };
611
- if (settlementExt) {
612
- paymentPayload.extensions = { [settlementKey]: settlementExt };
613
- }
614
- encoded = Buffer.from(JSON.stringify(paymentPayload)).toString('base64');
615
- headerName = 'payment-signature';
616
- }
617
- else {
618
- const paymentPayload = {
619
- x402Version: 1,
620
- scheme,
621
- network,
622
- payload: { signature, authorization },
623
- };
624
- encoded = Buffer.from(JSON.stringify(paymentPayload)).toString('base64');
625
- headerName = 'x-payment';
626
- }
627
- console.log(`${c.green}Payment signed. Retrying...${c.reset}`);
628
- res = await fetch(url, {
629
- ...fetchOpts,
630
- headers: {
631
- ...(fetchOpts.headers || {}),
632
- [headerName]: encoded,
633
- },
634
- });
635
- }
636
- // Display response
637
- const status = res.status;
638
- const statusColor = status < 300 ? c.green : status < 400 ? c.yellow : c.red;
639
- console.log(`${statusColor}${status} ${res.statusText}${c.reset}\n`);
640
- const contentType = res.headers.get('content-type') || '';
641
- if (contentType.includes('json')) {
642
- const data = await res.json();
643
- console.log(JSON.stringify(data, null, 2));
644
- }
645
- else {
646
- const text = await res.text();
647
- console.log(text.slice(0, 2000));
648
- }
649
- console.log();
650
- }
651
- async function cmdBalance() {
652
- if (!OBOLOS_PRIVATE_KEY) {
653
- console.error(`${c.red}No wallet configured.${c.reset} Run ${c.cyan}obolos setup${c.reset} first.`);
654
- process.exit(1);
655
- }
656
- const { createPublicClient, http: viemHttp, formatUnits } = await import('viem');
657
- const { privateKeyToAccount } = await import('viem/accounts');
658
- const { base } = await import('viem/chains');
659
- const key = OBOLOS_PRIVATE_KEY.startsWith('0x') ? OBOLOS_PRIVATE_KEY : `0x${OBOLOS_PRIVATE_KEY}`;
660
- const account = privateKeyToAccount(key);
661
- const client = createPublicClient({ chain: base, transport: viemHttp() });
662
- const balance = await client.readContract({
663
- address: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913',
664
- abi: [{ inputs: [{ name: 'account', type: 'address' }], name: 'balanceOf', outputs: [{ name: '', type: 'uint256' }], stateMutability: 'view', type: 'function' }],
665
- functionName: 'balanceOf',
666
- args: [account.address],
667
- });
668
- console.log(`\n${c.bold}Wallet:${c.reset} ${account.address}`);
669
- console.log(`${c.bold}Balance:${c.reset} ${c.green}${formatUnits(balance, 6)} USDC${c.reset}`);
670
- console.log(`${c.bold}Network:${c.reset} Base (Chain ID: 8453)\n`);
671
- }
672
- function prompt(question) {
673
- const rl = createInterface({ input: process.stdin, output: process.stdout });
674
- return new Promise(resolve => {
675
- rl.question(question, answer => {
676
- rl.close();
677
- resolve(answer.trim());
678
- });
679
- });
680
- }
681
- async function cmdSetup(args) {
682
- console.log(`\n${c.bold}${c.cyan}Obolos Wallet Setup${c.reset}\n`);
683
- const existing = loadConfig();
684
- if (args.includes('--generate')) {
685
- // Generate a new wallet
686
- const { privateKeyToAccount, generatePrivateKey } = await import('viem/accounts');
687
- const key = generatePrivateKey();
688
- const account = privateKeyToAccount(key);
689
- existing.private_key = key;
690
- saveConfig(existing);
691
- console.log(`${c.green}New wallet generated and saved!${c.reset}\n`);
692
- console.log(` ${c.bold}Address:${c.reset} ${account.address}`);
693
- console.log(` ${c.bold}Config:${c.reset} ${CONFIG_FILE}\n`);
694
- console.log(`${c.yellow}Next steps:${c.reset}`);
695
- console.log(` 1. Fund this address with USDC on Base`);
696
- console.log(` Send USDC to: ${c.cyan}${account.address}${c.reset}`);
697
- console.log(` 2. Check your balance: ${c.dim}obolos balance${c.reset}`);
698
- console.log(` 3. Call an API: ${c.dim}obolos call <api-id> --body '{...}'${c.reset}\n`);
699
- return;
700
- }
701
- if (args.includes('--show')) {
702
- if (existing.private_key) {
703
- const { privateKeyToAccount } = await import('viem/accounts');
704
- const key = existing.private_key.startsWith('0x') ? existing.private_key : `0x${existing.private_key}`;
705
- const account = privateKeyToAccount(key);
706
- console.log(` ${c.bold}Address:${c.reset} ${account.address}`);
707
- console.log(` ${c.bold}Config:${c.reset} ${CONFIG_FILE}`);
708
- console.log(` ${c.bold}API URL:${c.reset} ${OBOLOS_API_URL}\n`);
709
- }
710
- else {
711
- console.log(` ${c.yellow}No wallet configured.${c.reset}`);
712
- console.log(` Run ${c.cyan}obolos setup --generate${c.reset} to create one,`);
713
- console.log(` or ${c.cyan}obolos setup${c.reset} to import an existing key.\n`);
714
- }
715
- return;
716
- }
717
- // Interactive setup
718
- console.log(` Config is saved to ${c.dim}${CONFIG_FILE}${c.reset} (permissions: 600)\n`);
719
- if (existing.private_key) {
720
- const { privateKeyToAccount } = await import('viem/accounts');
721
- const key = existing.private_key.startsWith('0x') ? existing.private_key : `0x${existing.private_key}`;
722
- const account = privateKeyToAccount(key);
723
- console.log(` ${c.dim}Current wallet: ${account.address}${c.reset}\n`);
724
- }
725
- const keyInput = await prompt(` Private key (0x...) or "generate" for a new wallet: `);
726
- if (!keyInput) {
727
- console.log(`\n${c.yellow}No changes made.${c.reset}\n`);
728
- return;
729
- }
730
- if (keyInput === 'generate') {
731
- return cmdSetup(['--generate']);
732
- }
733
- // Validate the key
734
- const normalizedKey = keyInput.startsWith('0x') ? keyInput : `0x${keyInput}`;
735
- try {
736
- const { privateKeyToAccount } = await import('viem/accounts');
737
- const account = privateKeyToAccount(normalizedKey);
738
- existing.private_key = normalizedKey;
739
- saveConfig(existing);
740
- console.log(`\n${c.green}Wallet saved!${c.reset}\n`);
741
- console.log(` ${c.bold}Address:${c.reset} ${account.address}`);
742
- console.log(` ${c.bold}Config:${c.reset} ${CONFIG_FILE}\n`);
743
- console.log(` Check your balance: ${c.dim}obolos balance${c.reset}\n`);
744
- }
745
- catch (err) {
746
- console.error(`\n${c.red}Invalid private key: ${err.message}${c.reset}\n`);
747
- process.exit(1);
748
- }
749
- }
750
- async function cmdSetupMcp() {
751
- console.log(`\n${c.bold}${c.cyan}Obolos MCP Server Setup${c.reset}\n`);
752
- console.log(`${c.bold}Install:${c.reset}`);
753
- console.log(` npm install -g @obolos_tech/mcp-server\n`);
754
- console.log(`${c.bold}For Claude Code (global — all projects):${c.reset}`);
755
- console.log(` claude mcp add obolos ${c.yellow}--scope user${c.reset} -e OBOLOS_PRIVATE_KEY=0xyour_key -- obolos-mcp\n`);
756
- console.log(`${c.bold}For Claude Code (current project only):${c.reset}`);
757
- console.log(` claude mcp add obolos -e OBOLOS_PRIVATE_KEY=0xyour_key -- obolos-mcp\n`);
758
- console.log(`${c.bold}Or use npx (no install):${c.reset}`);
759
- console.log(` claude mcp add obolos ${c.yellow}--scope user${c.reset} -e OBOLOS_PRIVATE_KEY=0xyour_key -- npx @obolos_tech/mcp-server\n`);
760
- console.log(` ${c.dim}Scope reference:${c.reset}`);
761
- console.log(` ${c.dim} (default) Current project only${c.reset}`);
762
- console.log(` ${c.dim} --scope user All projects on your machine${c.reset}`);
763
- console.log(` ${c.dim} --scope project Shared via .mcp.json (checked into git)${c.reset}\n`);
764
- console.log(`${c.bold}For Claude Desktop / Cursor / Windsurf:${c.reset}`);
765
- console.log(` Add to your MCP config:\n`);
766
- console.log(` ${c.dim}{`);
767
- console.log(` "mcpServers": {`);
768
- console.log(` "obolos": {`);
769
- console.log(` "command": "npx",`);
770
- console.log(` "args": ["@obolos_tech/mcp-server"],`);
771
- console.log(` "env": {`);
772
- console.log(` "OBOLOS_PRIVATE_KEY": "0xyour_private_key"`);
773
- console.log(` }`);
774
- console.log(` }`);
775
- console.log(` }`);
776
- console.log(` }${c.reset}\n`);
777
- }
778
- // ─── Job Commands (ERC-8183 ACP) ────────────────────────────────────────────
779
- async function cmdJobList(args) {
780
- const params = new URLSearchParams();
781
- const status = getFlag(args, 'status');
782
- const client = getFlag(args, 'client');
783
- const provider = getFlag(args, 'provider');
784
- const limit = getFlag(args, 'limit') || '20';
785
- if (status)
786
- params.set('status', status);
787
- if (client)
788
- params.set('client', client);
789
- if (provider)
790
- params.set('provider', provider);
791
- params.set('limit', limit);
792
- const data = await apiGet(`/api/jobs?${params}`);
793
- const jobs = data.jobs || data.data || [];
794
- if (jobs.length === 0) {
795
- console.log(`${c.yellow}No jobs found.${c.reset}`);
796
- return;
797
- }
798
- const total = data.pagination?.total || data.total || jobs.length;
799
- console.log(`\n${c.bold}${c.cyan}ACP Jobs${c.reset} ${c.dim}— ${total} jobs${c.reset}\n`);
800
- // Table header
801
- 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}`);
802
- console.log(` ${c.dim}${'─'.repeat(110)}${c.reset}`);
803
- for (const job of jobs) {
804
- const id = shortenId(job.id || '');
805
- const title = (job.title || 'Untitled').slice(0, 28).padEnd(30);
806
- const st = statusColor((job.status || 'open').padEnd(10));
807
- const budget = job.budget != null ? `$${Number(job.budget).toFixed(2)}`.padEnd(12) : `${c.dim}—${c.reset}`.padEnd(12);
808
- const cl = shortenAddr(job.client).padEnd(14);
809
- const prov = job.provider ? shortenAddr(job.provider).padEnd(14) : `${c.dim}Open${c.reset}`.padEnd(14);
810
- const created = formatDate(job.created_at || job.createdAt);
811
- console.log(` ${id.padEnd(12)} ${title} ${st} ${budget} ${cl} ${prov} ${created}`);
812
- }
813
- console.log(`\n${c.dim}Use: obolos job info <id> for full details${c.reset}\n`);
814
- }
815
- async function cmdJobCreate(args) {
816
- const title = getFlag(args, 'title');
817
- const description = getFlag(args, 'description');
818
- const evaluator = getFlag(args, 'evaluator');
819
- const provider = getFlag(args, 'provider');
820
- const budget = getFlag(args, 'budget');
821
- const expires = getFlag(args, 'expires');
822
- if (!title) {
823
- console.error(`${c.red}Usage: obolos job create --title "..." --description "..." --evaluator 0x... [--provider 0x...] [--budget 1.00] [--expires 24h]${c.reset}`);
824
- process.exit(1);
825
- }
826
- if (!evaluator) {
827
- console.error(`${c.red}--evaluator is required. Provide the evaluator address (0x...).${c.reset}`);
828
- process.exit(1);
829
- }
830
- const walletAddress = await getWalletAddress();
831
- // Create job on-chain first
832
- let chainJobId = null;
833
- let chainTxHash = null;
834
- try {
835
- const acp = await getACPClient();
836
- // Parse expiry to unix timestamp (default: 7 days)
837
- let expiredAt;
838
- if (expires) {
839
- const parsed = parseRelativeTime(expires);
840
- expiredAt = Math.floor(new Date(parsed).getTime() / 1000);
841
- }
842
- else {
843
- expiredAt = Math.floor((Date.now() + 7 * 86400000) / 1000);
844
- }
845
- console.log(`\n ${c.dim}Creating job on-chain...${c.reset}`);
846
- const txHash = await acp.walletClient.writeContract({
847
- address: ACP_ADDRESS,
848
- abi: ACP_ABI,
849
- functionName: 'createJob',
850
- args: [
851
- (provider || ZERO_ADDRESS),
852
- evaluator,
853
- BigInt(expiredAt),
854
- description || title,
855
- ZERO_ADDRESS,
856
- ],
857
- account: acp.account,
858
- chain: (await import('viem/chains')).base,
859
- });
860
- console.log(` ${c.dim}Waiting for confirmation...${c.reset}`);
861
- const receipt = await acp.publicClient.waitForTransactionReceipt({ hash: txHash });
862
- // Extract jobId from JobCreated event
863
- for (const log of receipt.logs) {
864
- try {
865
- const decoded = acp.decodeEventLog({
866
- abi: ACP_ABI,
867
- data: log.data,
868
- topics: log.topics,
869
- });
870
- if (decoded.eventName === 'JobCreated') {
871
- chainJobId = (decoded.args.jobId).toString();
872
- break;
873
- }
874
- }
875
- catch { }
876
- }
877
- chainTxHash = txHash;
878
- console.log(` ${c.green}Transaction confirmed: ${txHash}${c.reset}`);
879
- if (chainJobId) {
880
- console.log(` ${c.green}Chain job ID: ${chainJobId}${c.reset}`);
881
- }
882
- }
883
- catch (err) {
884
- console.error(` ${c.yellow}On-chain creation failed: ${err.message}${c.reset}`);
885
- console.error(` ${c.dim}Falling back to backend-only...${c.reset}`);
886
- }
887
- const payload = {
888
- title,
889
- evaluator,
890
- };
891
- if (description)
892
- payload.description = description;
893
- if (provider)
894
- payload.provider = provider;
895
- if (budget)
896
- payload.budget = parseFloat(budget);
897
- if (expires)
898
- payload.expires_at = parseRelativeTime(expires);
899
- if (chainJobId)
900
- payload.chain_job_id = chainJobId;
901
- if (chainTxHash)
902
- payload.chain_tx_hash = chainTxHash;
903
- const data = await apiPost('/api/jobs', payload, {
904
- 'x-wallet-address': walletAddress,
905
- });
906
- const job = data.job || data;
907
- console.log(`\n${c.green}Job created successfully!${c.reset}\n`);
908
- console.log(`${c.dim}${'─'.repeat(60)}${c.reset}`);
909
- console.log(` ${c.bold}ID:${c.reset} ${job.id}`);
910
- if (chainJobId) {
911
- console.log(` ${c.bold}Chain ID:${c.reset} ${chainJobId}`);
912
- }
913
- console.log(` ${c.bold}Title:${c.reset} ${job.title}`);
914
- if (job.description) {
915
- console.log(` ${c.bold}Description:${c.reset} ${job.description}`);
916
- }
917
- console.log(` ${c.bold}Status:${c.reset} ${statusColor(job.status || 'open')}`);
918
- console.log(` ${c.bold}Client:${c.reset} ${job.client || walletAddress}`);
919
- console.log(` ${c.bold}Evaluator:${c.reset} ${job.evaluator}`);
920
- if (job.provider) {
921
- console.log(` ${c.bold}Provider:${c.reset} ${job.provider}`);
922
- }
923
- if (job.budget != null) {
924
- console.log(` ${c.bold}Budget:${c.reset} ${c.green}$${Number(job.budget).toFixed(2)} USDC${c.reset}`);
925
- }
926
- if (job.expires_at) {
927
- console.log(` ${c.bold}Expires:${c.reset} ${formatDate(job.expires_at)}`);
928
- }
929
- if (chainTxHash) {
930
- console.log(` ${c.bold}Tx:${c.reset} ${c.cyan}${chainTxHash}${c.reset}`);
931
- }
932
- console.log(`\n${c.dim}Next: obolos job fund ${job.id}${c.reset}\n`);
933
- }
934
- async function cmdJobInfo(args) {
935
- const id = getPositional(args, 0);
936
- if (!id) {
937
- console.error(`${c.red}Usage: obolos job info <id>${c.reset}`);
938
- process.exit(1);
939
- }
940
- const data = await apiGet(`/api/jobs/${encodeURIComponent(id)}`);
941
- const job = data.job || data;
942
- console.log(`\n${c.bold}${c.cyan}${job.title || 'Untitled Job'}${c.reset}`);
943
- console.log(`${c.dim}${'─'.repeat(60)}${c.reset}`);
944
- console.log(` ${c.bold}ID:${c.reset} ${job.id}`);
945
- console.log(` ${c.bold}Status:${c.reset} ${statusColor(job.status || 'open')}`);
946
- // State machine visualization
947
- console.log(` ${c.bold}Progress:${c.reset}`);
948
- console.log(stateVisualization(job.status || 'open'));
949
- console.log(` ${c.bold}Client:${c.reset} ${job.client || `${c.dim}—${c.reset}`}`);
950
- console.log(` ${c.bold}Evaluator:${c.reset} ${job.evaluator || `${c.dim}—${c.reset}`}`);
951
- console.log(` ${c.bold}Provider:${c.reset} ${job.provider || `${c.dim}Open (anyone can claim)${c.reset}`}`);
952
- if (job.budget != null) {
953
- console.log(` ${c.bold}Budget:${c.reset} ${c.green}$${Number(job.budget).toFixed(2)} USDC${c.reset}`);
954
- }
955
- if (job.description) {
956
- console.log(`\n ${c.bold}Description:${c.reset}`);
957
- const descLines = job.description.split('\n');
958
- for (const line of descLines) {
959
- console.log(` ${line}`);
960
- }
961
- }
962
- if (job.deliverable) {
963
- console.log(`\n ${c.bold}Deliverable:${c.reset} ${c.cyan}${job.deliverable}${c.reset}`);
964
- }
965
- if (job.reason) {
966
- console.log(` ${c.bold}Reason:${c.reset} ${job.reason}`);
967
- }
968
- if (job.expires_at) {
969
- const expiryDate = new Date(job.expires_at);
970
- const now = new Date();
971
- const expired = expiryDate < now;
972
- console.log(` ${c.bold}Expires:${c.reset} ${expired ? c.red : c.dim}${formatDate(job.expires_at)}${expired ? ' (expired)' : ''}${c.reset}`);
973
- }
974
- console.log(` ${c.bold}Created:${c.reset} ${formatDate(job.created_at || job.createdAt)}`);
975
- if (job.updated_at || job.updatedAt) {
976
- console.log(` ${c.bold}Updated:${c.reset} ${formatDate(job.updated_at || job.updatedAt)}`);
977
- }
978
- // Actions hint based on status
979
- console.log();
980
- const s = job.status || 'open';
981
- if (s === 'open') {
982
- console.log(` ${c.bold}Actions:${c.reset}`);
983
- console.log(` obolos job fund ${job.id} ${c.dim}Fund the escrow${c.reset}`);
984
- }
985
- else if (s === 'funded') {
986
- console.log(` ${c.bold}Actions:${c.reset}`);
987
- console.log(` obolos job submit ${job.id} --deliverable <hash> ${c.dim}Submit work${c.reset}`);
988
- }
989
- else if (s === 'submitted') {
990
- console.log(` ${c.bold}Actions:${c.reset}`);
991
- console.log(` obolos job complete ${job.id} ${c.dim}Approve and release funds${c.reset}`);
992
- console.log(` obolos job reject ${job.id} ${c.dim}Reject the submission${c.reset}`);
993
- }
994
- console.log();
995
- }
996
- async function cmdJobFund(args) {
997
- const id = getPositional(args, 0);
998
- if (!id) {
999
- console.error(`${c.red}Usage: obolos job fund <id>${c.reset}`);
1000
- process.exit(1);
1001
- }
1002
- const walletAddress = await getWalletAddress();
1003
- // First fetch the job to show budget info
1004
- const jobData = await apiGet(`/api/jobs/${encodeURIComponent(id)}`);
1005
- const job = jobData.job || jobData;
1006
- console.log(`\n${c.bold}${c.cyan}Fund Job${c.reset}\n`);
1007
- console.log(`${c.dim}${'─'.repeat(60)}${c.reset}`);
1008
- console.log(` ${c.bold}Job:${c.reset} ${job.title || id}`);
1009
- if (job.budget != null) {
1010
- console.log(` ${c.bold}Budget:${c.reset} ${c.green}$${Number(job.budget).toFixed(2)} USDC${c.reset}`);
1011
- }
1012
- console.log(` ${c.bold}Status:${c.reset} ${statusColor(job.status || 'open')}`);
1013
- console.log();
1014
- const chainJobId = job.chain_job_id;
1015
- let txHash = null;
1016
- if (chainJobId && job.budget != null) {
1017
- try {
1018
- const acp = await getACPClient();
1019
- const budgetStr = String(job.budget);
1020
- // Check USDC allowance and approve if needed
1021
- console.log(` ${c.dim}Checking USDC allowance...${c.reset}`);
1022
- const amount = acp.parseUnits(budgetStr, 6);
1023
- const allowance = await acp.publicClient.readContract({
1024
- address: USDC_CONTRACT,
1025
- abi: ERC20_ABI,
1026
- functionName: 'allowance',
1027
- args: [acp.account.address, ACP_ADDRESS],
1028
- });
1029
- if (allowance < amount) {
1030
- console.log(` ${c.dim}Approving USDC spend...${c.reset}`);
1031
- const approveTx = await acp.walletClient.writeContract({
1032
- address: USDC_CONTRACT,
1033
- abi: ERC20_ABI,
1034
- functionName: 'approve',
1035
- args: [ACP_ADDRESS, amount],
1036
- account: acp.account,
1037
- chain: (await import('viem/chains')).base,
1038
- });
1039
- await acp.publicClient.waitForTransactionReceipt({ hash: approveTx });
1040
- console.log(` ${c.green}USDC approved${c.reset}`);
1041
- }
1042
- console.log(` ${c.dim}Funding escrow on-chain...${c.reset}`);
1043
- const fundTx = await acp.walletClient.writeContract({
1044
- address: ACP_ADDRESS,
1045
- abi: ACP_ABI,
1046
- functionName: 'fund',
1047
- args: [BigInt(chainJobId), amount, '0x'],
1048
- account: acp.account,
1049
- chain: (await import('viem/chains')).base,
1050
- });
1051
- console.log(` ${c.dim}Waiting for confirmation...${c.reset}`);
1052
- await acp.publicClient.waitForTransactionReceipt({ hash: fundTx });
1053
- txHash = fundTx;
1054
- console.log(` ${c.green}Transaction confirmed: ${txHash}${c.reset}\n`);
1055
- }
1056
- catch (err) {
1057
- console.error(` ${c.yellow}On-chain funding failed: ${err.message}${c.reset}`);
1058
- console.error(` ${c.dim}Recording funding intent in backend...${c.reset}\n`);
1059
- }
1060
- }
1061
- // Update backend
1062
- const fundPayload = {};
1063
- if (txHash)
1064
- fundPayload.tx_hash = txHash;
1065
- if (chainJobId)
1066
- fundPayload.chain_job_id = chainJobId;
1067
- const data = await apiPost(`/api/jobs/${encodeURIComponent(id)}/fund`, fundPayload, {
1068
- 'x-wallet-address': walletAddress,
1069
- });
1070
- const updated = data.job || data;
1071
- console.log(`${c.green}Job funded successfully!${c.reset}`);
1072
- console.log(` ${c.bold}Status:${c.reset} ${statusColor(updated.status || 'funded')}`);
1073
- if (txHash) {
1074
- console.log(` ${c.bold}Tx:${c.reset} ${c.cyan}${txHash}${c.reset}`);
1075
- }
1076
- console.log(`${c.dim}Next: Provider submits work with: obolos job submit ${id} --deliverable <hash>${c.reset}\n`);
1077
- }
1078
- async function cmdJobSubmit(args) {
1079
- const id = getPositional(args, 0);
1080
- if (!id) {
1081
- console.error(`${c.red}Usage: obolos job submit <id> --deliverable <hash/CID/URL>${c.reset}`);
1082
- process.exit(1);
1083
- }
1084
- const deliverable = getFlag(args, 'deliverable');
1085
- if (!deliverable) {
1086
- console.error(`${c.red}--deliverable is required. Provide a hash, CID, or URL for the work product.${c.reset}`);
1087
- process.exit(1);
1088
- }
1089
- const walletAddress = await getWalletAddress();
1090
- // Fetch job to get chain_job_id
1091
- const jobData = await apiGet(`/api/jobs/${encodeURIComponent(id)}`);
1092
- const existingJob = jobData.job || jobData;
1093
- const chainJobId = existingJob.chain_job_id;
1094
- let txHash = null;
1095
- if (chainJobId) {
1096
- try {
1097
- const acp = await getACPClient();
1098
- const deliverableHash = acp.keccak256(acp.toHex(deliverable));
1099
- console.log(`\n ${c.dim}Submitting work on-chain...${c.reset}`);
1100
- const submitTx = await acp.walletClient.writeContract({
1101
- address: ACP_ADDRESS,
1102
- abi: ACP_ABI,
1103
- functionName: 'submit',
1104
- args: [BigInt(chainJobId), deliverableHash, '0x'],
1105
- account: acp.account,
1106
- chain: (await import('viem/chains')).base,
1107
- });
1108
- console.log(` ${c.dim}Waiting for confirmation...${c.reset}`);
1109
- await acp.publicClient.waitForTransactionReceipt({ hash: submitTx });
1110
- txHash = submitTx;
1111
- console.log(` ${c.green}Transaction confirmed: ${txHash}${c.reset}`);
1112
- }
1113
- catch (err) {
1114
- console.error(` ${c.yellow}On-chain submission failed: ${err.message}${c.reset}`);
1115
- }
1116
- }
1117
- const submitPayload = { deliverable };
1118
- if (txHash)
1119
- submitPayload.tx_hash = txHash;
1120
- const data = await apiPost(`/api/jobs/${encodeURIComponent(id)}/submit`, submitPayload, {
1121
- 'x-wallet-address': walletAddress,
1122
- });
1123
- const job = data.job || data;
1124
- console.log(`\n${c.green}Work submitted successfully!${c.reset}\n`);
1125
- console.log(`${c.dim}${'─'.repeat(60)}${c.reset}`);
1126
- console.log(` ${c.bold}Job:${c.reset} ${job.title || id}`);
1127
- console.log(` ${c.bold}Status:${c.reset} ${statusColor(job.status || 'submitted')}`);
1128
- console.log(` ${c.bold}Deliverable:${c.reset} ${c.cyan}${deliverable}${c.reset}`);
1129
- if (txHash) {
1130
- console.log(` ${c.bold}Tx:${c.reset} ${c.cyan}${txHash}${c.reset}`);
1131
- }
1132
- console.log(`\n${c.dim}The evaluator will now review and approve or reject the submission.${c.reset}\n`);
1133
- }
1134
- async function cmdJobComplete(args) {
1135
- const id = getPositional(args, 0);
1136
- if (!id) {
1137
- console.error(`${c.red}Usage: obolos job complete <id> [--reason "..."]${c.reset}`);
1138
- process.exit(1);
1139
- }
1140
- const reason = getFlag(args, 'reason');
1141
- const walletAddress = await getWalletAddress();
1142
- // Fetch job to get chain_job_id
1143
- const jobData = await apiGet(`/api/jobs/${encodeURIComponent(id)}`);
1144
- const existingJob = jobData.job || jobData;
1145
- const chainJobId = existingJob.chain_job_id;
1146
- let txHash = null;
1147
- if (chainJobId) {
1148
- try {
1149
- const acp = await getACPClient();
1150
- const reasonHash = reason
1151
- ? acp.keccak256(acp.toHex(reason))
1152
- : ZERO_BYTES32;
1153
- console.log(`\n ${c.dim}Completing job on-chain...${c.reset}`);
1154
- const completeTx = await acp.walletClient.writeContract({
1155
- address: ACP_ADDRESS,
1156
- abi: ACP_ABI,
1157
- functionName: 'complete',
1158
- args: [BigInt(chainJobId), reasonHash, '0x'],
1159
- account: acp.account,
1160
- chain: (await import('viem/chains')).base,
1161
- });
1162
- console.log(` ${c.dim}Waiting for confirmation...${c.reset}`);
1163
- await acp.publicClient.waitForTransactionReceipt({ hash: completeTx });
1164
- txHash = completeTx;
1165
- console.log(` ${c.green}Transaction confirmed: ${txHash}${c.reset}`);
1166
- }
1167
- catch (err) {
1168
- console.error(` ${c.yellow}On-chain completion failed: ${err.message}${c.reset}`);
1169
- }
1170
- }
1171
- const payload = {};
1172
- if (reason)
1173
- payload.reason = reason;
1174
- if (txHash)
1175
- payload.tx_hash = txHash;
1176
- const data = await apiPost(`/api/jobs/${encodeURIComponent(id)}/complete`, payload, {
1177
- 'x-wallet-address': walletAddress,
1178
- });
1179
- const job = data.job || data;
1180
- console.log(`\n${c.green}Job completed and approved!${c.reset}\n`);
1181
- console.log(`${c.dim}${'─'.repeat(60)}${c.reset}`);
1182
- console.log(` ${c.bold}Job:${c.reset} ${job.title || id}`);
1183
- console.log(` ${c.bold}Status:${c.reset} ${statusColor(job.status || 'completed')}`);
1184
- if (reason) {
1185
- console.log(` ${c.bold}Reason:${c.reset} ${reason}`);
1186
- }
1187
- if (txHash) {
1188
- console.log(` ${c.bold}Tx:${c.reset} ${c.cyan}${txHash}${c.reset}`);
1189
- }
1190
- if (job.budget != null) {
1191
- console.log(`\n ${c.dim}Escrow of $${Number(job.budget).toFixed(2)} USDC released to provider.${c.reset}`);
1192
- }
1193
- console.log();
1194
- }
1195
- async function cmdJobReject(args) {
1196
- const id = getPositional(args, 0);
1197
- if (!id) {
1198
- console.error(`${c.red}Usage: obolos job reject <id> [--reason "..."]${c.reset}`);
1199
- process.exit(1);
1200
- }
1201
- const reason = getFlag(args, 'reason');
1202
- const walletAddress = await getWalletAddress();
1203
- // Fetch job to get chain_job_id
1204
- const jobData = await apiGet(`/api/jobs/${encodeURIComponent(id)}`);
1205
- const existingJob = jobData.job || jobData;
1206
- const chainJobId = existingJob.chain_job_id;
1207
- let txHash = null;
1208
- if (chainJobId) {
1209
- try {
1210
- const acp = await getACPClient();
1211
- const reasonHash = reason
1212
- ? acp.keccak256(acp.toHex(reason))
1213
- : ZERO_BYTES32;
1214
- console.log(`\n ${c.dim}Rejecting job on-chain...${c.reset}`);
1215
- const rejectTx = await acp.walletClient.writeContract({
1216
- address: ACP_ADDRESS,
1217
- abi: ACP_ABI,
1218
- functionName: 'reject',
1219
- args: [BigInt(chainJobId), reasonHash, '0x'],
1220
- account: acp.account,
1221
- chain: (await import('viem/chains')).base,
1222
- });
1223
- console.log(` ${c.dim}Waiting for confirmation...${c.reset}`);
1224
- await acp.publicClient.waitForTransactionReceipt({ hash: rejectTx });
1225
- txHash = rejectTx;
1226
- console.log(` ${c.green}Transaction confirmed: ${txHash}${c.reset}`);
1227
- }
1228
- catch (err) {
1229
- console.error(` ${c.yellow}On-chain rejection failed: ${err.message}${c.reset}`);
1230
- }
1231
- }
1232
- const payload = {};
1233
- if (reason)
1234
- payload.reason = reason;
1235
- if (txHash)
1236
- payload.tx_hash = txHash;
1237
- const data = await apiPost(`/api/jobs/${encodeURIComponent(id)}/reject`, payload, {
1238
- 'x-wallet-address': walletAddress,
1239
- });
1240
- const job = data.job || data;
1241
- console.log(`\n${c.red}Job rejected.${c.reset}\n`);
1242
- console.log(`${c.dim}${'─'.repeat(60)}${c.reset}`);
1243
- console.log(` ${c.bold}Job:${c.reset} ${job.title || id}`);
1244
- console.log(` ${c.bold}Status:${c.reset} ${statusColor(job.status || 'rejected')}`);
1245
- if (reason) {
1246
- console.log(` ${c.bold}Reason:${c.reset} ${reason}`);
1247
- }
1248
- if (txHash) {
1249
- 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);
1250
23
  }
1251
- console.log();
1252
- }
1253
- function showJobHelp() {
1254
- console.log(`
1255
- ${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
1256
26
 
1257
27
  ${c.bold}Usage:${c.reset}
1258
- obolos job list [options] List jobs with optional filters
1259
- obolos job create [options] Create a new job
1260
- obolos job info <id> Get full job details
1261
- obolos job fund <id> Fund a job's escrow
1262
- obolos job submit <id> [options] Submit work for a job
1263
- obolos job complete <id> [options] Approve a job (evaluator)
1264
- obolos job reject <id> [options] Reject a job submission
28
+ obolos <command> [options]
29
+ obolos <group> <subcommand> [options]
1265
30
 
1266
- ${c.bold}List Options:${c.reset}
1267
- --status=open|funded|submitted|completed|rejected|expired
1268
- --client=0x... Filter by client address
1269
- --provider=0x... Filter by provider address
1270
- --limit=20 Max results (default: 20)
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')}
1271
33
 
1272
- ${c.bold}Create Options:${c.reset}
1273
- --title "..." Job title (required)
1274
- --description "..." Job description
1275
- --evaluator 0x... Evaluator address (required)
1276
- --provider 0x... Specific provider (optional, open if omitted)
1277
- --budget 1.00 Budget in USDC
1278
- --expires 24h Expiry (e.g., "24h", "7d", "1h")
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')}
1279
36
 
1280
- ${c.bold}Submit Options:${c.reset}
1281
- --deliverable <hash/CID/URL> Work product reference (required)
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)
1282
41
 
1283
- ${c.bold}Complete/Reject Options:${c.reset}
1284
- --reason "..." Optional reason text
1285
-
1286
- ${c.bold}Examples:${c.reset}
1287
- obolos job list --status=open
1288
- obolos job create --title "Analyze dataset" --evaluator 0xABC... --budget 5.00 --expires 7d
1289
- obolos job info abc123
1290
- obolos job fund abc123
1291
- obolos job submit abc123 --deliverable ipfs://Qm...
1292
- obolos job complete abc123 --reason "Looks great"
1293
- obolos job reject abc123 --reason "Missing section 3"
1294
- `);
1295
- }
1296
- async function cmdJob(args) {
1297
- const subcommand = args[0];
1298
- const subArgs = args.slice(1);
1299
- switch (subcommand) {
1300
- case 'list':
1301
- case 'ls':
1302
- await cmdJobList(subArgs);
1303
- break;
1304
- case 'create':
1305
- case 'new':
1306
- await cmdJobCreate(subArgs);
1307
- break;
1308
- case 'info':
1309
- case 'show':
1310
- await cmdJobInfo(subArgs);
1311
- break;
1312
- case 'fund':
1313
- await cmdJobFund(subArgs);
1314
- break;
1315
- case 'submit':
1316
- await cmdJobSubmit(subArgs);
1317
- break;
1318
- case 'complete':
1319
- case 'approve':
1320
- await cmdJobComplete(subArgs);
1321
- break;
1322
- case 'reject':
1323
- await cmdJobReject(subArgs);
1324
- break;
1325
- case 'help':
1326
- case '--help':
1327
- case '-h':
1328
- case undefined:
1329
- showJobHelp();
1330
- break;
1331
- default:
1332
- console.error(`${c.red}Unknown job subcommand: ${subcommand}${c.reset}`);
1333
- showJobHelp();
1334
- process.exit(1);
1335
- }
1336
- }
1337
- // ─── Listing Commands (Negotiation Layer) ────────────────────────────────────
1338
- async function cmdListingList(args) {
1339
- const params = new URLSearchParams();
1340
- const status = getFlag(args, 'status');
1341
- const client = getFlag(args, 'client');
1342
- const limit = getFlag(args, 'limit') || '20';
1343
- if (status)
1344
- params.set('status', status);
1345
- if (client)
1346
- params.set('client', client);
1347
- params.set('limit', limit);
1348
- const data = await apiGet(`/api/listings?${params}`);
1349
- const listings = data.listings || data.data || [];
1350
- if (listings.length === 0) {
1351
- console.log(`${c.yellow}No listings found.${c.reset}`);
1352
- return;
1353
- }
1354
- const total = data.pagination?.total || data.total || listings.length;
1355
- console.log(`\n${c.bold}${c.cyan}Job Listings${c.reset} ${c.dim}— ${total} listings${c.reset}\n`);
1356
- // Table header
1357
- 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}`);
1358
- console.log(` ${c.dim}${'─'.repeat(110)}${c.reset}`);
1359
- for (const l of listings) {
1360
- const id = shortenId(l.id || '');
1361
- const title = (l.title || 'Untitled').slice(0, 26).padEnd(28);
1362
- const st = statusColor((l.status || 'open').padEnd(12));
1363
- const budgetMin = l.min_budget != null ? `$${Number(l.min_budget).toFixed(2)}` : '?';
1364
- const budgetMax = l.max_budget != null ? `$${Number(l.max_budget).toFixed(2)}` : '?';
1365
- const budget = `${budgetMin}-${budgetMax}`.padEnd(20);
1366
- const bids = String(l.bid_count ?? l.bids?.length ?? 0).padEnd(6);
1367
- const cl = shortenAddr(l.client_address || l.client).padEnd(14);
1368
- const deadline = l.deadline ? formatDate(l.deadline) : `${c.dim}—${c.reset}`;
1369
- console.log(` ${id.padEnd(12)} ${title} ${st} ${budget} ${bids} ${cl} ${deadline}`);
1370
- }
1371
- console.log(`\n${c.dim}Use: obolos listing info <id> for full details${c.reset}\n`);
1372
- }
1373
- async function cmdListingCreate(args) {
1374
- const title = getFlag(args, 'title');
1375
- const description = getFlag(args, 'description');
1376
- const minBudget = getFlag(args, 'min-budget');
1377
- const maxBudget = getFlag(args, 'max-budget');
1378
- const deadline = getFlag(args, 'deadline');
1379
- const duration = getFlag(args, 'duration');
1380
- const evaluator = getFlag(args, 'evaluator');
1381
- const hook = getFlag(args, 'hook');
1382
- if (!title) {
1383
- console.error(`${c.red}Usage: obolos listing create --title "..." --description "..." [--min-budget 1.00] [--max-budget 10.00] [--deadline 7d] [--duration 24]${c.reset}`);
1384
- process.exit(1);
1385
- }
1386
- const walletAddress = await getWalletAddress();
1387
- const payload = { title };
1388
- if (description)
1389
- payload.description = description;
1390
- if (minBudget)
1391
- payload.min_budget = minBudget;
1392
- if (maxBudget)
1393
- payload.max_budget = maxBudget;
1394
- if (deadline)
1395
- payload.deadline = deadline;
1396
- if (duration)
1397
- payload.job_duration = parseInt(duration, 10);
1398
- if (evaluator)
1399
- payload.preferred_evaluator = evaluator;
1400
- if (hook)
1401
- payload.hook_address = hook;
1402
- const data = await apiPost('/api/listings', payload, {
1403
- 'x-wallet-address': walletAddress,
1404
- });
1405
- const listing = data.listing || data;
1406
- console.log(`\n${c.green}Listing created successfully!${c.reset}\n`);
1407
- console.log(`${c.dim}${'─'.repeat(60)}${c.reset}`);
1408
- console.log(` ${c.bold}ID:${c.reset} ${listing.id}`);
1409
- console.log(` ${c.bold}Title:${c.reset} ${listing.title}`);
1410
- if (listing.description) {
1411
- console.log(` ${c.bold}Description:${c.reset} ${listing.description}`);
1412
- }
1413
- console.log(` ${c.bold}Status:${c.reset} ${statusColor(listing.status || 'open')}`);
1414
- console.log(` ${c.bold}Client:${c.reset} ${listing.client_address || walletAddress}`);
1415
- if (listing.min_budget != null || listing.max_budget != null) {
1416
- const min = listing.min_budget != null ? `$${Number(listing.min_budget).toFixed(2)}` : '?';
1417
- const max = listing.max_budget != null ? `$${Number(listing.max_budget).toFixed(2)}` : '?';
1418
- console.log(` ${c.bold}Budget:${c.reset} ${c.green}${min} – ${max} USDC${c.reset}`);
1419
- }
1420
- if (listing.deadline) {
1421
- console.log(` ${c.bold}Deadline:${c.reset} ${formatDate(listing.deadline)}`);
1422
- }
1423
- if (listing.job_duration) {
1424
- console.log(` ${c.bold}Duration:${c.reset} ${listing.job_duration}h`);
1425
- }
1426
- 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`);
1427
- }
1428
- async function cmdListingInfo(args) {
1429
- const id = getPositional(args, 0);
1430
- if (!id) {
1431
- console.error(`${c.red}Usage: obolos listing info <id>${c.reset}`);
1432
- process.exit(1);
1433
- }
1434
- const data = await apiGet(`/api/listings/${encodeURIComponent(id)}`);
1435
- const listing = data.listing || data;
1436
- console.log(`\n${c.bold}${c.cyan}${listing.title || 'Untitled Listing'}${c.reset}`);
1437
- console.log(`${c.dim}${'─'.repeat(60)}${c.reset}`);
1438
- console.log(` ${c.bold}ID:${c.reset} ${listing.id}`);
1439
- console.log(` ${c.bold}Status:${c.reset} ${statusColor(listing.status || 'open')}`);
1440
- console.log(` ${c.bold}Client:${c.reset} ${listing.client_address || `${c.dim}—${c.reset}`}`);
1441
- if (listing.min_budget != null || listing.max_budget != null) {
1442
- const min = listing.min_budget != null ? `$${Number(listing.min_budget).toFixed(2)}` : '?';
1443
- const max = listing.max_budget != null ? `$${Number(listing.max_budget).toFixed(2)}` : '?';
1444
- console.log(` ${c.bold}Budget:${c.reset} ${c.green}${min} – ${max} USDC${c.reset}`);
1445
- }
1446
- if (listing.deadline) {
1447
- const deadlineDate = new Date(listing.deadline);
1448
- const now = new Date();
1449
- const expired = deadlineDate < now;
1450
- console.log(` ${c.bold}Deadline:${c.reset} ${expired ? c.red : c.dim}${formatDate(listing.deadline)}${expired ? ' (passed)' : ''}${c.reset}`);
1451
- }
1452
- if (listing.job_duration) {
1453
- console.log(` ${c.bold}Duration:${c.reset} ${listing.job_duration}h`);
1454
- }
1455
- if (listing.preferred_evaluator) {
1456
- console.log(` ${c.bold}Evaluator:${c.reset} ${listing.preferred_evaluator}`);
1457
- }
1458
- if (listing.description) {
1459
- console.log(`\n ${c.bold}Description:${c.reset}`);
1460
- const descLines = listing.description.split('\n');
1461
- for (const line of descLines) {
1462
- console.log(` ${line}`);
1463
- }
1464
- }
1465
- console.log(` ${c.bold}Created:${c.reset} ${formatDate(listing.created_at || listing.createdAt)}`);
1466
- // Bids
1467
- const bids = listing.bids || [];
1468
- if (bids.length > 0) {
1469
- console.log(`\n ${c.bold}${c.cyan}Bids (${bids.length})${c.reset}`);
1470
- console.log(` ${c.dim}${'─'.repeat(56)}${c.reset}`);
1471
- console.log(` ${c.bold}${'Bid ID'.padEnd(12)} ${'Provider'.padEnd(14)} ${'Price'.padEnd(12)} ${'Delivery'.padEnd(10)} Message${c.reset}`);
1472
- console.log(` ${c.dim}${'─'.repeat(56)}${c.reset}`);
1473
- for (const bid of bids) {
1474
- const bidId = shortenId(bid.id || '');
1475
- const provider = shortenAddr(bid.provider_address);
1476
- const price = bid.price != null ? `${c.green}$${Number(bid.price).toFixed(2)}${c.reset}` : `${c.dim}—${c.reset}`;
1477
- const delivery = bid.delivery_time ? `${bid.delivery_time}h` : `${c.dim}—${c.reset}`;
1478
- const msg = (bid.message || '').slice(0, 40);
1479
- console.log(` ${bidId.padEnd(12)} ${provider.padEnd(14)} ${price.padEnd(12)} ${delivery.padEnd(10)} ${c.dim}${msg}${c.reset}`);
1480
- }
1481
- }
1482
- else {
1483
- console.log(`\n ${c.dim}No bids yet.${c.reset}`);
1484
- }
1485
- // Actions
1486
- console.log();
1487
- const s = listing.status || 'open';
1488
- if (s === 'open') {
1489
- console.log(` ${c.bold}Actions:${c.reset}`);
1490
- console.log(` obolos listing bid ${listing.id} --price 5.00 ${c.dim}Submit a bid${c.reset}`);
1491
- if (bids.length > 0) {
1492
- console.log(` obolos listing accept ${listing.id} --bid <bid_id> ${c.dim}Accept a bid${c.reset}`);
1493
- }
1494
- console.log(` obolos listing cancel ${listing.id} ${c.dim}Cancel the listing${c.reset}`);
1495
- }
1496
- console.log();
1497
- }
1498
- async function cmdListingBid(args) {
1499
- const listingId = getPositional(args, 0);
1500
- if (!listingId) {
1501
- console.error(`${c.red}Usage: obolos listing bid <listing_id> --price 5.00 [--delivery 24] [--message "..."]${c.reset}`);
1502
- process.exit(1);
1503
- }
1504
- const price = getFlag(args, 'price');
1505
- if (!price) {
1506
- console.error(`${c.red}--price is required. Provide your bid amount in USDC.${c.reset}`);
1507
- process.exit(1);
1508
- }
1509
- const delivery = getFlag(args, 'delivery');
1510
- const message = getFlag(args, 'message');
1511
- const proposalHash = getFlag(args, 'proposal-hash');
1512
- const walletAddress = await getWalletAddress();
1513
- const payload = { price };
1514
- if (delivery)
1515
- payload.delivery_time = parseInt(delivery, 10);
1516
- if (message)
1517
- payload.message = message;
1518
- if (proposalHash)
1519
- payload.proposal_hash = proposalHash;
1520
- const data = await apiPost(`/api/listings/${encodeURIComponent(listingId)}/bid`, payload, {
1521
- 'x-wallet-address': walletAddress,
1522
- });
1523
- const bid = data.bid || data;
1524
- console.log(`\n${c.green}Bid submitted successfully!${c.reset}\n`);
1525
- console.log(`${c.dim}${'─'.repeat(60)}${c.reset}`);
1526
- console.log(` ${c.bold}Bid ID:${c.reset} ${bid.id}`);
1527
- console.log(` ${c.bold}Listing:${c.reset} ${listingId}`);
1528
- console.log(` ${c.bold}Price:${c.reset} ${c.green}$${Number(price).toFixed(2)} USDC${c.reset}`);
1529
- if (delivery) {
1530
- console.log(` ${c.bold}Delivery:${c.reset} ${delivery}h`);
1531
- }
1532
- if (message) {
1533
- console.log(` ${c.bold}Message:${c.reset} ${message}`);
1534
- }
1535
- console.log(`\n${c.dim}The client will review your bid. You'll be notified if accepted.${c.reset}\n`);
1536
- }
1537
- async function cmdListingAccept(args) {
1538
- const listingId = getPositional(args, 0);
1539
- if (!listingId) {
1540
- console.error(`${c.red}Usage: obolos listing accept <listing_id> --bid <bid_id>${c.reset}`);
1541
- process.exit(1);
1542
- }
1543
- const bidId = getFlag(args, 'bid');
1544
- if (!bidId) {
1545
- console.error(`${c.red}--bid is required. Specify the bid ID to accept.${c.reset}`);
1546
- process.exit(1);
1547
- }
1548
- const walletAddress = await getWalletAddress();
1549
- // Create on-chain ACP job if wallet is available
1550
- let chainJobId = null;
1551
- let chainTxHash = null;
1552
- try {
1553
- // Fetch listing details to get terms
1554
- const listingData = await apiGet(`/api/listings/${encodeURIComponent(listingId)}`);
1555
- const listing = listingData.listing || listingData;
1556
- const bids = listing.bids || [];
1557
- const acceptedBid = bids.find((b) => b.id === bidId);
1558
- if (acceptedBid && OBOLOS_PRIVATE_KEY) {
1559
- const acp = await getACPClient();
1560
- const providerAddress = acceptedBid.provider_address || ZERO_ADDRESS;
1561
- const evaluatorAddress = listing.preferred_evaluator || walletAddress;
1562
- // Default expiry: delivery_time hours or job_duration or 7 days
1563
- const durationHours = acceptedBid.delivery_time || listing.job_duration || 168;
1564
- const expiredAt = Math.floor((Date.now() + durationHours * 3600000) / 1000);
1565
- const description = `${listing.title}: ${listing.description || ''}`.slice(0, 500);
1566
- console.log(`\n ${c.dim}Creating ACP job on-chain...${c.reset}`);
1567
- const txHash = await acp.walletClient.writeContract({
1568
- address: ACP_ADDRESS,
1569
- abi: ACP_ABI,
1570
- functionName: 'createJob',
1571
- args: [
1572
- providerAddress,
1573
- evaluatorAddress,
1574
- BigInt(expiredAt),
1575
- description,
1576
- (listing.hook_address || ZERO_ADDRESS),
1577
- ],
1578
- account: acp.account,
1579
- chain: (await import('viem/chains')).base,
1580
- });
1581
- console.log(` ${c.dim}Waiting for confirmation...${c.reset}`);
1582
- const receipt = await acp.publicClient.waitForTransactionReceipt({ hash: txHash });
1583
- // Extract jobId from JobCreated event
1584
- for (const log of receipt.logs) {
1585
- try {
1586
- const decoded = acp.decodeEventLog({
1587
- abi: ACP_ABI,
1588
- data: log.data,
1589
- topics: log.topics,
1590
- });
1591
- if (decoded.eventName === 'JobCreated') {
1592
- chainJobId = (decoded.args.jobId).toString();
1593
- break;
1594
- }
1595
- }
1596
- catch { }
1597
- }
1598
- chainTxHash = txHash;
1599
- console.log(` ${c.green}Transaction confirmed: ${txHash}${c.reset}`);
1600
- if (chainJobId) {
1601
- console.log(` ${c.green}Chain job ID: ${chainJobId}${c.reset}`);
1602
- }
1603
- }
1604
- }
1605
- catch (err) {
1606
- console.error(` ${c.yellow}On-chain job creation failed: ${err.message}${c.reset}`);
1607
- console.error(` ${c.dim}Proceeding with backend-only acceptance...${c.reset}`);
1608
- }
1609
- const payload = { bid_id: bidId };
1610
- if (chainJobId)
1611
- payload.acp_job_id = chainJobId;
1612
- if (chainTxHash)
1613
- payload.chain_tx_hash = chainTxHash;
1614
- const data = await apiPost(`/api/listings/${encodeURIComponent(listingId)}/accept`, payload, {
1615
- 'x-wallet-address': walletAddress,
1616
- });
1617
- const listing = data.listing || data;
1618
- console.log(`\n${c.green}Bid accepted! ACP job created.${c.reset}\n`);
1619
- console.log(`${c.dim}${'─'.repeat(60)}${c.reset}`);
1620
- console.log(` ${c.bold}Listing:${c.reset} ${listing.title || listingId}`);
1621
- console.log(` ${c.bold}Status:${c.reset} ${statusColor(listing.status || 'accepted')}`);
1622
- console.log(` ${c.bold}Bid:${c.reset} ${bidId}`);
1623
- if (chainJobId) {
1624
- console.log(` ${c.bold}Chain ID:${c.reset} ${chainJobId}`);
1625
- }
1626
- if (chainTxHash) {
1627
- console.log(` ${c.bold}Tx:${c.reset} ${c.cyan}${chainTxHash}${c.reset}`);
1628
- }
1629
- if (listing.job_id || data.job_id) {
1630
- console.log(` ${c.bold}Job ID:${c.reset} ${listing.job_id || data.job_id}`);
1631
- }
1632
- console.log(`\n${c.dim}Next: Fund the escrow with: obolos job fund <job-id>${c.reset}\n`);
1633
- }
1634
- async function cmdListingCancel(args) {
1635
- const listingId = getPositional(args, 0);
1636
- if (!listingId) {
1637
- console.error(`${c.red}Usage: obolos listing cancel <listing_id>${c.reset}`);
1638
- process.exit(1);
1639
- }
1640
- const walletAddress = await getWalletAddress();
1641
- const data = await apiPost(`/api/listings/${encodeURIComponent(listingId)}/cancel`, {}, {
1642
- 'x-wallet-address': walletAddress,
1643
- });
1644
- const listing = data.listing || data;
1645
- console.log(`\n${c.yellow}Listing cancelled.${c.reset}\n`);
1646
- console.log(`${c.dim}${'─'.repeat(60)}${c.reset}`);
1647
- console.log(` ${c.bold}Listing:${c.reset} ${listing.title || listingId}`);
1648
- console.log(` ${c.bold}Status:${c.reset} ${statusColor(listing.status || 'cancelled')}`);
1649
- console.log();
1650
- }
1651
- function showListingHelp() {
1652
- console.log(`
1653
- ${c.bold}${c.cyan}obolos listing${c.reset} — Agent-to-agent negotiation layer
1654
-
1655
- ${c.bold}Usage:${c.reset}
1656
- obolos listing list [options] Browse open job listings
1657
- obolos listing create [options] Create a new listing for agents to bid on
1658
- obolos listing info <id> Get listing details with all bids
1659
- obolos listing bid <id> [options] Submit a bid on a listing
1660
- obolos listing accept <id> [options] Accept a bid (auto-creates ACP job)
1661
- obolos listing cancel <id> Cancel a listing
1662
-
1663
- ${c.bold}List Options:${c.reset}
1664
- --status=open|negotiating|accepted|cancelled
1665
- --client=0x... Filter by client address
1666
- --limit=20 Max results (default: 20)
1667
-
1668
- ${c.bold}Create Options:${c.reset}
1669
- --title "..." Listing title (required)
1670
- --description "..." Detailed description
1671
- --min-budget 1.00 Minimum budget in USDC
1672
- --max-budget 10.00 Maximum budget in USDC
1673
- --deadline 7d Bidding deadline (e.g., "24h", "7d")
1674
- --duration 24 Expected job duration in hours
1675
- --evaluator 0x... Preferred evaluator address
1676
- --hook 0x... Hook contract address
1677
-
1678
- ${c.bold}Bid Options:${c.reset}
1679
- --price 5.00 Your proposed price in USDC (required)
1680
- --delivery 24 Estimated delivery time in hours
1681
- --message "I can do this" Pitch to the client
1682
- --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.
1683
45
 
1684
- ${c.bold}Accept Options:${c.reset}
1685
- --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.
1686
49
 
1687
- ${c.bold}Examples:${c.reset}
1688
- obolos listing list --status=open
1689
- obolos listing create --title "Analyze dataset" --description "Parse and summarize CSV" --max-budget 10.00 --deadline 7d
1690
- obolos listing info abc123
1691
- obolos listing bid abc123 --price 5.00 --delivery 24 --message "I can do this in 12h"
1692
- obolos listing accept abc123 --bid bid456
1693
- obolos listing cancel abc123
50
+ ${c.bold}Docs:${c.reset} https://obolos.tech
1694
51
  `);
1695
52
  }
1696
- async function cmdListing(args) {
1697
- const sub = args[0];
1698
- const subArgs = args.slice(1);
1699
- switch (sub) {
1700
- case 'list':
1701
- case 'ls':
1702
- await cmdListingList(subArgs);
1703
- break;
1704
- case 'create':
1705
- case 'new':
1706
- await cmdListingCreate(subArgs);
1707
- break;
1708
- case 'info':
1709
- case 'show':
1710
- await cmdListingInfo(subArgs);
1711
- break;
1712
- case 'bid':
1713
- await cmdListingBid(subArgs);
1714
- break;
1715
- case 'accept':
1716
- await cmdListingAccept(subArgs);
1717
- break;
1718
- case 'cancel':
1719
- await cmdListingCancel(subArgs);
1720
- break;
1721
- case 'help':
1722
- case '--help':
1723
- case '-h':
1724
- case undefined:
1725
- showListingHelp();
1726
- break;
1727
- default:
1728
- console.error(`${c.red}Unknown listing subcommand: ${sub}${c.reset}`);
1729
- showListingHelp();
1730
- process.exit(1);
1731
- }
1732
- }
1733
- // ─── ANP Commands (Agent Negotiation Protocol) ──────────────────────────────
1734
- async function cmdAnpList(args) {
1735
- const params = new URLSearchParams();
1736
- const status = getFlag(args, 'status');
1737
- const limit = getFlag(args, 'limit') || '20';
1738
- if (status)
1739
- params.set('status', status);
1740
- params.set('limit', limit);
1741
- const data = await apiGet(`/api/anp/listings?${params}`);
1742
- const listings = data.listings || data.data || [];
1743
- if (listings.length === 0) {
1744
- console.log(`${c.yellow}No ANP listings found.${c.reset}`);
1745
- return;
1746
- }
1747
- const total = data.pagination?.total || data.total || listings.length;
1748
- console.log(`\n${c.bold}${c.cyan}ANP Listings${c.reset} ${c.dim}— ${total} listings${c.reset}\n`);
1749
- // Table header
1750
- console.log(` ${c.bold}${'CID'.padEnd(18)} ${'Title'.padEnd(28)} ${'Budget Range'.padEnd(20)} ${'Status'.padEnd(14)} ${'Bids'.padEnd(6)} Client${c.reset}`);
1751
- console.log(` ${c.dim}${'─'.repeat(100)}${c.reset}`);
1752
- for (const l of listings) {
1753
- const cid = (l.cid || l.id || '').slice(0, 16).padEnd(18);
1754
- const title = (l.title || 'Untitled').slice(0, 26).padEnd(28);
1755
- const minUsd = l.minBudgetUsd ?? l.min_budget_usd ?? (l.minBudget ? Number(l.minBudget) / 1e6 : null) ?? (l.min_budget ? Number(l.min_budget) / 1e6 : null);
1756
- const maxUsd = l.maxBudgetUsd ?? l.max_budget_usd ?? (l.maxBudget ? Number(l.maxBudget) / 1e6 : null) ?? (l.max_budget ? Number(l.max_budget) / 1e6 : null);
1757
- const budgetMin = minUsd != null ? `$${minUsd.toFixed(0)}` : '?';
1758
- const budgetMax = maxUsd != null ? `$${maxUsd.toFixed(0)}` : '?';
1759
- const budget = `${budgetMin}-${budgetMax}`.padEnd(20);
1760
- const st = statusColor((l.status || 'open').padEnd(12));
1761
- const bids = String(l.bidCount ?? l.bid_count ?? l.bids?.length ?? 0).padEnd(6);
1762
- const cl = shortenAddr(l.client_address || l.client || l.signer);
1763
- console.log(` ${cid} ${title} ${budget} ${st} ${bids} ${cl}`);
1764
- }
1765
- console.log(`\n${c.dim}Use: obolos anp info <cid> for full details${c.reset}\n`);
1766
- }
1767
- async function cmdAnpInfo(args) {
1768
- const cid = getPositional(args, 0);
1769
- if (!cid) {
1770
- console.error(`${c.red}Usage: obolos anp info <cid>${c.reset}`);
1771
- process.exit(1);
1772
- }
1773
- const data = await apiGet(`/api/anp/listings/${encodeURIComponent(cid)}`);
1774
- const listing = data.listing || data;
1775
- console.log(`\n${c.bold}${c.cyan}${listing.title || 'Untitled ANP Listing'}${c.reset}`);
1776
- console.log(`${c.dim}${'─'.repeat(60)}${c.reset}`);
1777
- console.log(` ${c.bold}CID:${c.reset} ${listing.cid || cid}`);
1778
- console.log(` ${c.bold}Status:${c.reset} ${statusColor(listing.status || 'open')}`);
1779
- console.log(` ${c.bold}Client:${c.reset} ${listing.client || listing.client_address || listing.signer || `${c.dim}—${c.reset}`}`);
1780
- const minRaw = listing.minBudget ?? listing.min_budget;
1781
- const maxRaw = listing.maxBudget ?? listing.max_budget;
1782
- const minUsd = listing.minBudgetUsd ?? (minRaw ? Number(minRaw) / 1e6 : null);
1783
- const maxUsd = listing.maxBudgetUsd ?? (maxRaw ? Number(maxRaw) / 1e6 : null);
1784
- if (minUsd != null || maxUsd != null) {
1785
- const min = minUsd != null ? `$${minUsd.toFixed(2)}` : '?';
1786
- const max = maxUsd != null ? `$${maxUsd.toFixed(2)}` : '?';
1787
- console.log(` ${c.bold}Budget:${c.reset} ${c.green}${min} – ${max} USDC${c.reset}`);
1788
- }
1789
- const deadlineRaw = listing.deadline;
1790
- if (deadlineRaw) {
1791
- const ts = Number(deadlineRaw);
1792
- const deadlineDate = new Date(ts > 1e12 ? ts : ts * 1000);
1793
- const now = new Date();
1794
- const expired = deadlineDate < now;
1795
- console.log(` ${c.bold}Deadline:${c.reset} ${expired ? c.red : c.dim}${formatDate(deadlineDate.toISOString())}${expired ? ' (passed)' : ''}${c.reset}`);
1796
- }
1797
- if (listing.job_duration || listing.jobDuration) {
1798
- const dur = listing.job_duration || listing.jobDuration;
1799
- console.log(` ${c.bold}Duration:${c.reset} ${dur >= 86400 ? `${Math.floor(dur / 86400)}d` : dur >= 3600 ? `${Math.floor(dur / 3600)}h` : `${dur}s`}`);
1800
- }
1801
- if (listing.preferred_evaluator || listing.preferredEvaluator) {
1802
- const ev = listing.preferred_evaluator || listing.preferredEvaluator;
1803
- if (ev !== ZERO_ADDRESS) {
1804
- console.log(` ${c.bold}Evaluator:${c.reset} ${ev}`);
1805
- }
1806
- }
1807
- if (listing.description) {
1808
- console.log(`\n ${c.bold}Description:${c.reset}`);
1809
- const descLines = listing.description.split('\n');
1810
- for (const line of descLines) {
1811
- console.log(` ${line}`);
1812
- }
1813
- }
1814
- if (listing.content_hash || listing.contentHash) {
1815
- console.log(` ${c.bold}Content Hash:${c.reset} ${c.dim}${listing.content_hash || listing.contentHash}${c.reset}`);
1816
- }
1817
- if (listing.nonce != null) {
1818
- console.log(` ${c.bold}Nonce:${c.reset} ${c.dim}${listing.nonce}${c.reset}`);
1819
- }
1820
- if (listing.signature) {
1821
- console.log(` ${c.bold}Signature:${c.reset} ${c.dim}${listing.signature.slice(0, 20)}...${c.reset}`);
1822
- }
1823
- // Bids
1824
- const bids = listing.bids || [];
1825
- if (bids.length > 0) {
1826
- console.log(`\n ${c.bold}${c.cyan}Bids (${bids.length})${c.reset}`);
1827
- console.log(` ${c.dim}${'─'.repeat(56)}${c.reset}`);
1828
- console.log(` ${c.bold}${'CID'.padEnd(18)} ${'Bidder'.padEnd(14)} ${'Price'.padEnd(12)} ${'Delivery'.padEnd(10)} Message${c.reset}`);
1829
- console.log(` ${c.dim}${'─'.repeat(56)}${c.reset}`);
1830
- for (const bid of bids) {
1831
- const bidCid = (bid.cid || bid.id || '').slice(0, 16).padEnd(18);
1832
- const bidder = shortenAddr(bid.provider || bid.signer || bid.provider_address);
1833
- const priceUsd = bid.priceUsd ?? (bid.price != null ? Number(bid.price) / 1e6 : null);
1834
- const price = priceUsd != null ? `${c.green}$${priceUsd.toFixed(2)}${c.reset}` : `${c.dim}—${c.reset}`;
1835
- const delivery = bid.deliveryTime || bid.delivery_time;
1836
- const deliveryStr = delivery ? (delivery >= 86400 ? `${Math.floor(delivery / 86400)}d` : delivery >= 3600 ? `${Math.floor(delivery / 3600)}h` : `${delivery}s`) : `${c.dim}—${c.reset}`;
1837
- const msg = (bid.message || '').slice(0, 40);
1838
- console.log(` ${bidCid} ${bidder.padEnd(14)} ${price.padEnd(12)} ${deliveryStr.padEnd(10)} ${c.dim}${msg}${c.reset}`);
1839
- }
1840
- }
1841
- else {
1842
- console.log(`\n ${c.dim}No bids yet.${c.reset}`);
1843
- }
1844
- // Actions
1845
- console.log();
1846
- const s = listing.status || 'open';
1847
- if (s === 'open' || s === 'negotiating') {
1848
- console.log(` ${c.bold}Actions:${c.reset}`);
1849
- console.log(` obolos anp bid ${cid} --price 5.00 ${c.dim}Submit a bid${c.reset}`);
1850
- if (bids.length > 0) {
1851
- console.log(` obolos anp accept ${cid} --bid <bid_cid> ${c.dim}Accept a bid${c.reset}`);
1852
- }
1853
- }
1854
- console.log(` obolos anp verify ${cid} ${c.dim}Verify document${c.reset}`);
1855
- console.log();
1856
- }
1857
- async function cmdAnpCreate(args) {
1858
- const title = getFlag(args, 'title');
1859
- const description = getFlag(args, 'description');
1860
- const minBudget = getFlag(args, 'min-budget');
1861
- const maxBudget = getFlag(args, 'max-budget');
1862
- const deadline = getFlag(args, 'deadline');
1863
- const duration = getFlag(args, 'duration');
1864
- const evaluator = getFlag(args, 'evaluator');
1865
- if (!title) {
1866
- console.error(`${c.red}Usage: obolos anp create --title "..." --description "..." --min-budget 5 --max-budget 50 --deadline 7d --duration 3d [--evaluator 0x...]${c.reset}`);
1867
- process.exit(1);
1868
- }
1869
- const anp = await getANPSigningClient();
1870
- // Compute content hash
1871
- const contentHash = await computeContentHash({ title, description: description || '' });
1872
- // Generate nonce
1873
- const nonce = generateNonce();
1874
- // Parse deadline to unix timestamp (seconds from now)
1875
- let deadlineTs;
1876
- if (deadline) {
1877
- const secs = parseTimeToSeconds(deadline);
1878
- deadlineTs = BigInt(Math.floor(Date.now() / 1000) + secs);
1879
- }
1880
- else {
1881
- deadlineTs = BigInt(Math.floor(Date.now() / 1000) + 7 * 86400); // default 7d
1882
- }
1883
- // Parse duration to seconds
1884
- let jobDuration;
1885
- if (duration) {
1886
- jobDuration = BigInt(parseTimeToSeconds(duration));
1887
- }
1888
- else {
1889
- jobDuration = BigInt(3 * 86400); // default 3d
1890
- }
1891
- const minBudgetWei = BigInt(Math.floor((minBudget ? parseFloat(minBudget) : 0) * 1e6));
1892
- const maxBudgetWei = BigInt(Math.floor((maxBudget ? parseFloat(maxBudget) : 0) * 1e6));
1893
- const preferredEvaluator = (evaluator || ZERO_ADDRESS);
1894
- const message = {
1895
- contentHash,
1896
- minBudget: minBudgetWei,
1897
- maxBudget: maxBudgetWei,
1898
- deadline: deadlineTs,
1899
- jobDuration: jobDuration,
1900
- preferredEvaluator,
1901
- nonce,
1902
- };
1903
- console.log(`\n ${c.dim}Signing ListingIntent...${c.reset}`);
1904
- const signature = await anp.walletClient.signTypedData({
1905
- account: anp.account,
1906
- domain: ANP_DOMAIN,
1907
- types: { ListingIntent: ANP_TYPES.ListingIntent },
1908
- primaryType: 'ListingIntent',
1909
- message,
1910
- });
1911
- console.log(` ${c.green}Signed.${c.reset} Publishing...`);
1912
- const document = {
1913
- protocol: 'anp/v1',
1914
- type: 'listing',
1915
- data: {
1916
- title,
1917
- description: description || '',
1918
- minBudget: minBudgetWei.toString(),
1919
- maxBudget: maxBudgetWei.toString(),
1920
- deadline: Number(deadlineTs),
1921
- jobDuration: Number(jobDuration),
1922
- preferredEvaluator,
1923
- nonce: Number(nonce),
1924
- },
1925
- signer: anp.account.address.toLowerCase(),
1926
- signature,
1927
- timestamp: Date.now(),
1928
- };
1929
- const data = await apiPost('/api/anp/publish', document);
1930
- const result = data.listing || data;
1931
- console.log(`\n${c.green}ANP listing published!${c.reset}\n`);
1932
- console.log(`${c.dim}${'─'.repeat(60)}${c.reset}`);
1933
- console.log(` ${c.bold}CID:${c.reset} ${result.cid || result.id}`);
1934
- console.log(` ${c.bold}Title:${c.reset} ${title}`);
1935
- console.log(` ${c.bold}Budget:${c.reset} ${c.green}$${(minBudget || '0')} – $${(maxBudget || '0')} USDC${c.reset}`);
1936
- console.log(` ${c.bold}Deadline:${c.reset} ${formatDate(new Date(Number(deadlineTs) * 1000).toISOString())}`);
1937
- console.log(` ${c.bold}Duration:${c.reset} ${duration || '3d'}`);
1938
- console.log(` ${c.bold}Signer:${c.reset} ${anp.account.address}`);
1939
- console.log(` ${c.bold}Signature:${c.reset} ${c.dim}${signature.slice(0, 20)}...${c.reset}`);
1940
- console.log(`\n${c.dim}Agents can bid with: obolos anp bid ${result.cid || result.id} --price 25 --delivery 48h${c.reset}\n`);
1941
- }
1942
- async function cmdAnpBid(args) {
1943
- const listingCid = getPositional(args, 0);
1944
- if (!listingCid) {
1945
- console.error(`${c.red}Usage: obolos anp bid <listing_cid> --price 25 --delivery 48h [--message "..."]${c.reset}`);
1946
- process.exit(1);
1947
- }
1948
- const price = getFlag(args, 'price');
1949
- if (!price) {
1950
- console.error(`${c.red}--price is required. Provide your bid amount in USDC.${c.reset}`);
1951
- process.exit(1);
1952
- }
1953
- const delivery = getFlag(args, 'delivery');
1954
- const message = getFlag(args, 'message');
1955
- const anp = await getANPSigningClient();
1956
- // Fetch listing document to compute listingHash
1957
- console.log(`\n ${c.dim}Fetching listing document...${c.reset}`);
1958
- const listingData = await apiGet(`/api/anp/objects/${encodeURIComponent(listingCid)}`);
1959
- const listingDoc = listingData;
1960
- const ld = listingDoc.data || listingDoc;
1961
- // Recompute listing content hash from title+description, then compute struct hash
1962
- const listingContentHash = await computeContentHash({ title: ld.title, description: ld.description });
1963
- const listingHash = anp.hashListingStruct({
1964
- contentHash: listingContentHash,
1965
- minBudget: BigInt(ld.minBudget || '0'),
1966
- maxBudget: BigInt(ld.maxBudget || '0'),
1967
- deadline: BigInt(ld.deadline || '0'),
1968
- jobDuration: BigInt(ld.jobDuration || '0'),
1969
- preferredEvaluator: (ld.preferredEvaluator || ZERO_ADDRESS),
1970
- nonce: BigInt(ld.nonce || '0'),
1971
- });
1972
- // Compute content hash for bid
1973
- const contentHash = await computeContentHash({ message: message || '', proposalCid: '' });
1974
- const nonce = generateNonce();
1975
- const priceWei = BigInt(Math.floor(parseFloat(price) * 1e6));
1976
- let deliveryTime;
1977
- if (delivery) {
1978
- deliveryTime = BigInt(parseTimeToSeconds(delivery));
1979
- }
1980
- else {
1981
- deliveryTime = BigInt(86400); // default 24h
1982
- }
1983
- const bidMessage = {
1984
- listingHash,
1985
- contentHash,
1986
- price: priceWei,
1987
- deliveryTime,
1988
- nonce,
1989
- };
1990
- console.log(` ${c.dim}Signing BidIntent...${c.reset}`);
1991
- const signature = await anp.walletClient.signTypedData({
1992
- account: anp.account,
1993
- domain: ANP_DOMAIN,
1994
- types: { BidIntent: ANP_TYPES.BidIntent },
1995
- primaryType: 'BidIntent',
1996
- message: bidMessage,
1997
- });
1998
- console.log(` ${c.green}Signed.${c.reset} Publishing...`);
1999
- const document = {
2000
- protocol: 'anp/v1',
2001
- type: 'bid',
2002
- data: {
2003
- listingCid,
2004
- listingHash,
2005
- price: priceWei.toString(),
2006
- deliveryTime: Number(deliveryTime),
2007
- message: message || '',
2008
- nonce: Number(nonce),
2009
- },
2010
- signer: anp.account.address.toLowerCase(),
2011
- signature,
2012
- timestamp: Date.now(),
2013
- };
2014
- const data = await apiPost('/api/anp/publish', document);
2015
- const result = data.bid || data;
2016
- console.log(`\n${c.green}ANP bid published!${c.reset}\n`);
2017
- console.log(`${c.dim}${'─'.repeat(60)}${c.reset}`);
2018
- console.log(` ${c.bold}CID:${c.reset} ${result.cid || result.id}`);
2019
- console.log(` ${c.bold}Listing:${c.reset} ${listingCid}`);
2020
- console.log(` ${c.bold}Price:${c.reset} ${c.green}$${parseFloat(price).toFixed(2)} USDC${c.reset}`);
2021
- console.log(` ${c.bold}Delivery:${c.reset} ${delivery || '24h'}`);
2022
- if (message) {
2023
- console.log(` ${c.bold}Message:${c.reset} ${message}`);
2024
- }
2025
- console.log(` ${c.bold}Signer:${c.reset} ${anp.account.address}`);
2026
- console.log(` ${c.bold}Signature:${c.reset} ${c.dim}${signature.slice(0, 20)}...${c.reset}`);
2027
- console.log(`\n${c.dim}The listing owner can accept with: obolos anp accept ${listingCid} --bid ${result.cid || result.id}${c.reset}\n`);
2028
- }
2029
- async function cmdAnpAccept(args) {
2030
- const listingCid = getPositional(args, 0);
2031
- if (!listingCid) {
2032
- console.error(`${c.red}Usage: obolos anp accept <listing_cid> --bid <bid_cid>${c.reset}`);
2033
- process.exit(1);
2034
- }
2035
- const bidCid = getFlag(args, 'bid');
2036
- if (!bidCid) {
2037
- console.error(`${c.red}--bid is required. Specify the bid CID to accept.${c.reset}`);
2038
- process.exit(1);
2039
- }
2040
- const anp = await getANPSigningClient();
2041
- // Fetch listing and bid documents
2042
- console.log(`\n ${c.dim}Fetching listing and bid documents...${c.reset}`);
2043
- const [listingData, bidData] = await Promise.all([
2044
- apiGet(`/api/anp/objects/${encodeURIComponent(listingCid)}`),
2045
- apiGet(`/api/anp/objects/${encodeURIComponent(bidCid)}`),
2046
- ]);
2047
- const ld = listingData.data || listingData;
2048
- const bd = bidData.data || bidData;
2049
- // Recompute listing content hash and struct hash
2050
- const listingContentHash = await computeContentHash({ title: ld.title, description: ld.description });
2051
- const listingHash = anp.hashListingStruct({
2052
- contentHash: listingContentHash,
2053
- minBudget: BigInt(ld.minBudget || '0'),
2054
- maxBudget: BigInt(ld.maxBudget || '0'),
2055
- deadline: BigInt(ld.deadline || '0'),
2056
- jobDuration: BigInt(ld.jobDuration || '0'),
2057
- preferredEvaluator: (ld.preferredEvaluator || ZERO_ADDRESS),
2058
- nonce: BigInt(ld.nonce || '0'),
2059
- });
2060
- // Recompute bid content hash and struct hash
2061
- const bidContentHash = await computeContentHash({ message: bd.message || '', proposalCid: bd.proposalCid || '' });
2062
- const bidHash = anp.hashBidStruct({
2063
- listingHash: (bd.listingHash || listingHash),
2064
- contentHash: bidContentHash,
2065
- price: BigInt(bd.price || '0'),
2066
- deliveryTime: BigInt(bd.deliveryTime || '0'),
2067
- nonce: BigInt(bd.nonce || '0'),
2068
- });
2069
- const nonce = generateNonce();
2070
- const acceptMessage = {
2071
- listingHash,
2072
- bidHash,
2073
- nonce,
2074
- };
2075
- console.log(` ${c.dim}Signing AcceptIntent...${c.reset}`);
2076
- const signature = await anp.walletClient.signTypedData({
2077
- account: anp.account,
2078
- domain: ANP_DOMAIN,
2079
- types: { AcceptIntent: ANP_TYPES.AcceptIntent },
2080
- primaryType: 'AcceptIntent',
2081
- message: acceptMessage,
2082
- });
2083
- console.log(` ${c.green}Signed.${c.reset} Publishing...`);
2084
- const document = {
2085
- protocol: 'anp/v1',
2086
- type: 'acceptance',
2087
- data: {
2088
- listingCid,
2089
- bidCid,
2090
- listingHash,
2091
- bidHash,
2092
- nonce: Number(nonce),
2093
- },
2094
- signer: anp.account.address.toLowerCase(),
2095
- signature,
2096
- timestamp: Date.now(),
2097
- };
2098
- const data = await apiPost('/api/anp/publish', document);
2099
- const result = data.accept || data;
2100
- console.log(`\n${c.green}Bid accepted! ANP agreement published.${c.reset}\n`);
2101
- console.log(`${c.dim}${'─'.repeat(60)}${c.reset}`);
2102
- console.log(` ${c.bold}CID:${c.reset} ${result.cid || result.id}`);
2103
- console.log(` ${c.bold}Listing:${c.reset} ${listingCid}`);
2104
- console.log(` ${c.bold}Bid:${c.reset} ${bidCid}`);
2105
- console.log(` ${c.bold}Signer:${c.reset} ${anp.account.address}`);
2106
- console.log(` ${c.bold}Signature:${c.reset} ${c.dim}${signature.slice(0, 20)}...${c.reset}`);
2107
- console.log(`\n${c.dim}The agreement is now verifiable on-chain.${c.reset}\n`);
2108
- }
2109
- async function cmdAnpVerify(args) {
2110
- const cid = getPositional(args, 0);
2111
- if (!cid) {
2112
- console.error(`${c.red}Usage: obolos anp verify <cid>${c.reset}`);
2113
- process.exit(1);
2114
- }
2115
- console.log(`\n ${c.dim}Verifying document...${c.reset}`);
2116
- const data = await apiGet(`/api/anp/verify/${encodeURIComponent(cid)}`);
2117
- console.log(`\n${c.bold}${c.cyan}ANP Document Verification${c.reset}`);
2118
- console.log(`${c.dim}${'─'.repeat(60)}${c.reset}`);
2119
- console.log(` ${c.bold}CID:${c.reset} ${cid}`);
2120
- console.log(` ${c.bold}Type:${c.reset} ${data.type || `${c.dim}—${c.reset}`}`);
2121
- console.log(` ${c.bold}Signer:${c.reset} ${data.signer || `${c.dim}—${c.reset}`}`);
2122
- if (data.valid || data.verified) {
2123
- console.log(` ${c.bold}Signature:${c.reset} ${c.green}Valid${c.reset}`);
2124
- }
2125
- else {
2126
- console.log(` ${c.bold}Signature:${c.reset} ${c.red}Invalid${c.reset}`);
2127
- }
2128
- if (data.content_valid != null) {
2129
- console.log(` ${c.bold}Content Hash:${c.reset} ${data.content_valid ? `${c.green}Matches${c.reset}` : `${c.red}Mismatch${c.reset}`}`);
2130
- }
2131
- if (data.chain_refs != null) {
2132
- console.log(` ${c.bold}Chain Refs:${c.reset} ${data.chain_refs ? `${c.green}Valid${c.reset}` : `${c.red}Invalid${c.reset}`}`);
2133
- }
2134
- if (data.details) {
2135
- console.log(`\n ${c.bold}Details:${c.reset}`);
2136
- const details = typeof data.details === 'string' ? data.details : JSON.stringify(data.details, null, 2);
2137
- for (const line of details.split('\n')) {
2138
- console.log(` ${c.dim}${line}${c.reset}`);
2139
- }
2140
- }
2141
- console.log();
2142
- }
2143
- // ─── IML Commands (In-Job Messaging) ────────────────────────────────────────
2144
- async function cmdAnpMessage(args) {
2145
- const jobId = getPositional(args, 0);
2146
- const body = getFlag(args, 'message') || getFlag(args, 'body') || getFlag(args, 'm');
2147
- const roleStr = getFlag(args, 'role') || 'client';
2148
- if (!jobId || !body) {
2149
- console.error(`${c.red}Usage: obolos anp message <job_id> --message "..." --role client|provider|evaluator${c.reset}`);
2150
- process.exit(1);
2151
- }
2152
- const roleMap = { client: 0, provider: 1, evaluator: 2 };
2153
- const role = roleMap[roleStr];
2154
- if (role === undefined) {
2155
- console.error(`${c.red}Invalid role. Use: client, provider, or evaluator${c.reset}`);
2156
- process.exit(1);
2157
- }
2158
- const anp = await getANPSigningClient();
2159
- const jobHash = await computeJobHash(jobId);
2160
- const contentHash = await computeContentHash({ body, attachments: [] });
2161
- const nonce = generateNonce();
2162
- const signature = await anp.walletClient.signTypedData({
2163
- account: anp.account,
2164
- domain: ANP_DOMAIN,
2165
- types: { MessageIntent: ANP_TYPES.MessageIntent },
2166
- primaryType: 'MessageIntent',
2167
- message: { jobHash, contentHash, role, nonce },
2168
- });
2169
- const document = {
2170
- protocol: 'anp/v1', type: 'message',
2171
- data: { jobId, jobHash, body, role, nonce: Number(nonce) },
2172
- signer: anp.account.address.toLowerCase(),
2173
- signature, timestamp: Date.now(),
2174
- };
2175
- const data = await apiPost('/api/anp/publish', document);
2176
- console.log(`\n${c.green}Message sent!${c.reset}\n`);
2177
- console.log(` ${c.bold}CID:${c.reset} ${data.cid}`);
2178
- console.log(` ${c.bold}Job:${c.reset} ${jobId}`);
2179
- console.log(` ${c.bold}Role:${c.reset} ${roleStr}`);
2180
- console.log(` ${c.bold}Signer:${c.reset} ${anp.account.address}\n`);
2181
- }
2182
- async function cmdAnpThread(args) {
2183
- const jobId = getPositional(args, 0);
2184
- if (!jobId) {
2185
- console.error(`${c.red}Usage: obolos anp thread <job_id>${c.reset}`);
2186
- process.exit(1);
2187
- }
2188
- const data = await apiGet(`/api/anp/jobs/${encodeURIComponent(jobId)}/thread`);
2189
- const messages = data.messages || [];
2190
- if (messages.length === 0) {
2191
- console.log(`\n${c.yellow}No messages for job ${jobId}.${c.reset}\n`);
2192
- return;
2193
- }
2194
- console.log(`\n${c.bold}${c.cyan}Job Thread${c.reset} ${c.dim}— ${messages.length} messages${c.reset}\n`);
2195
- for (const msg of messages) {
2196
- const roleColors = { client: c.blue, provider: c.green, evaluator: c.yellow };
2197
- const roleColor = roleColors[msg.roleName] || c.dim;
2198
- console.log(` ${roleColor}${c.bold}[${msg.roleName}]${c.reset} ${c.dim}${msg.createdAt}${c.reset}`);
2199
- console.log(` ${msg.body}`);
2200
- console.log(` ${c.dim}CID: ${msg.cid} | Signer: ${msg.signer}${c.reset}\n`);
2201
- }
2202
- }
2203
- async function cmdAnpAmend(args) {
2204
- const jobId = getPositional(args, 0);
2205
- const bidHash = getFlag(args, 'bid-hash');
2206
- const reason = getFlag(args, 'reason');
2207
- const priceStr = getFlag(args, 'price');
2208
- const deliveryStr = getFlag(args, 'delivery');
2209
- const scopeDelta = getFlag(args, 'scope-delta') || '';
2210
- if (!jobId || !bidHash || !reason) {
2211
- console.error(`${c.red}Usage: obolos anp amend <job_id> --bid-hash 0x... --reason "..." [--price 25] [--delivery 48h] [--scope-delta "..."]${c.reset}`);
2212
- process.exit(1);
2213
- }
2214
- const anp = await getANPSigningClient();
2215
- const jobHash = await computeJobHash(jobId);
2216
- const newPriceUsdc = priceStr ? usdToUsdc(parseFloat(priceStr)) : '0';
2217
- const newDeliveryTime = deliveryStr ? parseTimeToSeconds(deliveryStr) : 0;
2218
- const contentHash = await computeContentHash({ reason, scopeDelta });
2219
- const nonce = generateNonce();
2220
- const signature = await anp.walletClient.signTypedData({
2221
- account: anp.account,
2222
- domain: ANP_DOMAIN,
2223
- types: { AmendmentIntent: ANP_TYPES.AmendmentIntent },
2224
- primaryType: 'AmendmentIntent',
2225
- message: {
2226
- jobHash, originalBidHash: bidHash,
2227
- newPrice: BigInt(newPriceUsdc), newDeliveryTime: BigInt(newDeliveryTime),
2228
- contentHash, nonce,
2229
- },
2230
- });
2231
- const document = {
2232
- protocol: 'anp/v1', type: 'amendment',
2233
- data: {
2234
- jobId, jobHash, originalBidHash: bidHash,
2235
- newPrice: newPriceUsdc, newDeliveryTime, reason, scopeDelta,
2236
- nonce: Number(nonce),
2237
- },
2238
- signer: anp.account.address.toLowerCase(),
2239
- signature, timestamp: Date.now(),
2240
- };
2241
- const data = await apiPost('/api/anp/publish', document);
2242
- console.log(`\n${c.green}Amendment proposed!${c.reset}\n`);
2243
- console.log(` ${c.bold}CID:${c.reset} ${data.cid}`);
2244
- console.log(` ${c.bold}Job:${c.reset} ${jobId}`);
2245
- if (priceStr)
2246
- console.log(` ${c.bold}New Price:${c.reset} $${priceStr} USDC`);
2247
- if (deliveryStr)
2248
- console.log(` ${c.bold}New Delivery:${c.reset} ${deliveryStr}`);
2249
- console.log(` ${c.bold}Reason:${c.reset} ${reason}`);
2250
- console.log(`\n${c.dim}Counterparty must accept with: obolos anp accept-amend ${jobId} --amendment ${data.cid}${c.reset}\n`);
2251
- }
2252
- async function cmdAnpAcceptAmend(args) {
2253
- const jobId = getPositional(args, 0);
2254
- const amendmentCid = getFlag(args, 'amendment');
2255
- if (!jobId || !amendmentCid) {
2256
- console.error(`${c.red}Usage: obolos anp accept-amend <job_id> --amendment <amendment_cid>${c.reset}`);
2257
- process.exit(1);
2258
- }
2259
- const anp = await getANPSigningClient();
2260
- // Fetch amendment to compute struct hash
2261
- const amendDoc = await apiGet(`/api/anp/objects/${encodeURIComponent(amendmentCid)}`);
2262
- const ad = amendDoc.data || amendDoc;
2263
- const contentHash = await computeContentHash({ reason: ad.reason, scopeDelta: ad.scopeDelta || '' });
2264
- const amendmentHash = anp.hashAmendmentStruct({
2265
- jobHash: ad.jobHash,
2266
- originalBidHash: ad.originalBidHash,
2267
- newPrice: BigInt(ad.newPrice),
2268
- newDeliveryTime: BigInt(ad.newDeliveryTime),
2269
- contentHash,
2270
- nonce: BigInt(ad.nonce),
2271
- });
2272
- const nonce = generateNonce();
2273
- const signature = await anp.walletClient.signTypedData({
2274
- account: anp.account,
2275
- domain: ANP_DOMAIN,
2276
- types: { AmendmentAcceptance: ANP_TYPES.AmendmentAcceptance },
2277
- primaryType: 'AmendmentAcceptance',
2278
- message: { amendmentHash, nonce },
2279
- });
2280
- const document = {
2281
- protocol: 'anp/v1', type: 'amendment_acceptance',
2282
- data: { jobId, amendmentCid, amendmentHash, nonce: Number(nonce) },
2283
- signer: anp.account.address.toLowerCase(),
2284
- signature, timestamp: Date.now(),
2285
- };
2286
- const data = await apiPost('/api/anp/publish', document);
2287
- console.log(`\n${c.green}Amendment accepted!${c.reset}\n`);
2288
- console.log(` ${c.bold}CID:${c.reset} ${data.cid}`);
2289
- console.log(` ${c.bold}Amendment CID:${c.reset} ${amendmentCid}`);
2290
- console.log(` ${c.bold}Job:${c.reset} ${jobId}\n`);
2291
- }
2292
- async function cmdAnpCheckpoint(args) {
2293
- const jobId = getPositional(args, 0);
2294
- const milestoneStr = getFlag(args, 'milestone') || '0';
2295
- const deliverable = getFlag(args, 'deliverable');
2296
- const notes = getFlag(args, 'notes') || '';
2297
- if (!jobId || !deliverable) {
2298
- console.error(`${c.red}Usage: obolos anp checkpoint <job_id> --deliverable "..." [--milestone 0] [--notes "..."]${c.reset}`);
2299
- process.exit(1);
2300
- }
2301
- const milestoneIndex = parseInt(milestoneStr, 10);
2302
- const anp = await getANPSigningClient();
2303
- const jobHash = await computeJobHash(jobId);
2304
- const contentHash = await computeContentHash({ deliverable, notes });
2305
- const nonce = generateNonce();
2306
- const signature = await anp.walletClient.signTypedData({
2307
- account: anp.account,
2308
- domain: ANP_DOMAIN,
2309
- types: { CheckpointIntent: ANP_TYPES.CheckpointIntent },
2310
- primaryType: 'CheckpointIntent',
2311
- message: { jobHash, milestoneIndex, contentHash, nonce },
2312
- });
2313
- const document = {
2314
- protocol: 'anp/v1', type: 'checkpoint',
2315
- data: { jobId, jobHash, milestoneIndex, deliverable, notes, nonce: Number(nonce) },
2316
- signer: anp.account.address.toLowerCase(),
2317
- signature, timestamp: Date.now(),
2318
- };
2319
- const data = await apiPost('/api/anp/publish', document);
2320
- console.log(`\n${c.green}Checkpoint submitted!${c.reset}\n`);
2321
- console.log(` ${c.bold}CID:${c.reset} ${data.cid}`);
2322
- console.log(` ${c.bold}Job:${c.reset} ${jobId}`);
2323
- console.log(` ${c.bold}Milestone:${c.reset} #${milestoneIndex}`);
2324
- console.log(`\n${c.dim}Approve with: obolos anp approve-cp ${jobId} --checkpoint ${data.cid}${c.reset}\n`);
2325
- }
2326
- async function cmdAnpApproveCp(args) {
2327
- const jobId = getPositional(args, 0);
2328
- const checkpointCid = getFlag(args, 'checkpoint');
2329
- if (!jobId || !checkpointCid) {
2330
- console.error(`${c.red}Usage: obolos anp approve-cp <job_id> --checkpoint <checkpoint_cid>${c.reset}`);
2331
- process.exit(1);
2332
- }
2333
- const anp = await getANPSigningClient();
2334
- const cpDoc = await apiGet(`/api/anp/objects/${encodeURIComponent(checkpointCid)}`);
2335
- const cd = cpDoc.data || cpDoc;
2336
- const contentHash = await computeContentHash({ deliverable: cd.deliverable, notes: cd.notes || '' });
2337
- const checkpointHash = anp.hashCheckpointStruct({
2338
- jobHash: cd.jobHash,
2339
- milestoneIndex: cd.milestoneIndex,
2340
- contentHash,
2341
- nonce: BigInt(cd.nonce),
2342
- });
2343
- const nonce = generateNonce();
2344
- const signature = await anp.walletClient.signTypedData({
2345
- account: anp.account,
2346
- domain: ANP_DOMAIN,
2347
- types: { CheckpointApproval: ANP_TYPES.CheckpointApproval },
2348
- primaryType: 'CheckpointApproval',
2349
- message: { checkpointHash, nonce },
2350
- });
2351
- const document = {
2352
- protocol: 'anp/v1', type: 'checkpoint_approval',
2353
- data: { jobId, checkpointCid, checkpointHash, nonce: Number(nonce) },
2354
- signer: anp.account.address.toLowerCase(),
2355
- signature, timestamp: Date.now(),
2356
- };
2357
- const data = await apiPost('/api/anp/publish', document);
2358
- console.log(`\n${c.green}Checkpoint approved!${c.reset}\n`);
2359
- console.log(` ${c.bold}CID:${c.reset} ${data.cid}`);
2360
- console.log(` ${c.bold}Checkpoint CID:${c.reset} ${checkpointCid}`);
2361
- console.log(` ${c.bold}Job:${c.reset} ${jobId}\n`);
2362
- }
2363
- async function cmdAnpAmendments(args) {
2364
- const jobId = getPositional(args, 0);
2365
- if (!jobId) {
2366
- console.error(`${c.red}Usage: obolos anp amendments <job_id>${c.reset}`);
2367
- process.exit(1);
2368
- }
2369
- const data = await apiGet(`/api/anp/jobs/${encodeURIComponent(jobId)}/amendments`);
2370
- const amendments = data.amendments || [];
2371
- if (amendments.length === 0) {
2372
- console.log(`\n${c.yellow}No amendments for job ${jobId}.${c.reset}\n`);
2373
- return;
2374
- }
2375
- console.log(`\n${c.bold}${c.cyan}Amendments${c.reset} ${c.dim}— ${amendments.length} total${c.reset}\n`);
2376
- for (const a of amendments) {
2377
- const status = a.accepted ? `${c.green}Accepted${c.reset}` : `${c.yellow}Pending${c.reset}`;
2378
- console.log(` ${c.bold}CID:${c.reset} ${a.cid}`);
2379
- console.log(` ${c.bold}Status:${c.reset} ${status}`);
2380
- if (a.newPrice && a.newPrice !== '0')
2381
- console.log(` ${c.bold}Price:${c.reset} $${(Number(a.newPrice) / 1_000_000).toFixed(2)} USDC`);
2382
- if (a.newDeliveryTime)
2383
- console.log(` ${c.bold}Delivery:${c.reset} ${Math.round(a.newDeliveryTime / 3600)}h`);
2384
- console.log(` ${c.bold}Reason:${c.reset} ${a.reason}`);
2385
- console.log(` ${c.bold}Signer:${c.reset} ${a.signer} ${c.dim}${a.createdAt}${c.reset}\n`);
2386
- }
2387
- }
2388
- async function cmdAnpCheckpoints(args) {
2389
- const jobId = getPositional(args, 0);
2390
- if (!jobId) {
2391
- console.error(`${c.red}Usage: obolos anp checkpoints <job_id>${c.reset}`);
2392
- process.exit(1);
2393
- }
2394
- const data = await apiGet(`/api/anp/jobs/${encodeURIComponent(jobId)}/checkpoints`);
2395
- const checkpoints = data.checkpoints || [];
2396
- if (checkpoints.length === 0) {
2397
- console.log(`\n${c.yellow}No checkpoints for job ${jobId}.${c.reset}\n`);
2398
- return;
2399
- }
2400
- console.log(`\n${c.bold}${c.cyan}Checkpoints${c.reset} ${c.dim}— ${checkpoints.length} total${c.reset}\n`);
2401
- for (const cp of checkpoints) {
2402
- const status = cp.approved ? `${c.green}Approved${c.reset}` : `${c.yellow}Pending${c.reset}`;
2403
- console.log(` ${c.bold}#${cp.milestoneIndex}${c.reset} ${status} ${c.dim}${cp.createdAt}${c.reset}`);
2404
- console.log(` ${c.bold}CID:${c.reset} ${cp.cid}`);
2405
- console.log(` ${c.bold}Deliverable:${c.reset} ${cp.deliverable}`);
2406
- if (cp.notes)
2407
- console.log(` ${c.bold}Notes:${c.reset} ${cp.notes}`);
2408
- console.log(` ${c.bold}Signer:${c.reset} ${cp.signer}\n`);
2409
- }
2410
- }
2411
- function showAnpHelp() {
2412
- console.log(`
2413
- ${c.bold}${c.cyan}obolos anp${c.reset} — Agent Negotiation Protocol (EIP-712 signed documents)
2414
-
2415
- ${c.bold}Usage:${c.reset}
2416
- obolos anp list [options] Browse ANP listings
2417
- obolos anp info <cid> Get listing details with bids
2418
- obolos anp create [options] Sign and publish a listing
2419
- obolos anp bid <cid> [options] Sign and publish a bid
2420
- obolos anp accept <cid> [options] Accept a bid (sign AcceptIntent)
2421
- obolos anp verify <cid> Verify document integrity
2422
-
2423
- ${c.bold}List Options:${c.reset}
2424
- --status=open|negotiating|accepted Filter by status
2425
- --limit=20 Max results (default: 20)
2426
-
2427
- ${c.bold}Create Options:${c.reset}
2428
- --title "..." Listing title (required)
2429
- --description "..." Detailed description
2430
- --min-budget 5 Minimum budget in USDC
2431
- --max-budget 50 Maximum budget in USDC
2432
- --deadline 7d Bidding deadline (e.g., "24h", "7d")
2433
- --duration 3d Expected job duration (e.g., "48h", "3d")
2434
- --evaluator 0x... Preferred evaluator address
2435
-
2436
- ${c.bold}Bid Options:${c.reset}
2437
- --price 25 Your proposed price in USDC (required)
2438
- --delivery 48h Estimated delivery time (e.g., "24h", "3d")
2439
- --message "I can do this" Message to the client
2440
-
2441
- ${c.bold}Accept Options:${c.reset}
2442
- --bid <bid_cid> Bid CID to accept (required)
2443
-
2444
- ${c.bold}In-Job Messaging (IML):${c.reset}
2445
- obolos anp message <job_id> [options] Send signed message on a running job
2446
- obolos anp thread <job_id> View message thread for a job
2447
- obolos anp amend <job_id> [options] Propose scope/price amendment
2448
- obolos anp accept-amend <job_id> [opts] Accept a pending amendment
2449
- obolos anp amendments <job_id> List amendments for a job
2450
- obolos anp checkpoint <job_id> [options] Submit milestone checkpoint
2451
- obolos anp approve-cp <job_id> [options] Approve a checkpoint
2452
- obolos anp checkpoints <job_id> List checkpoints for a job
2453
-
2454
- ${c.bold}Message Options:${c.reset}
2455
- --message "..." Message body (required)
2456
- --role client|provider|evaluator Your role (default: client)
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}
2457
59
 
2458
- ${c.bold}Amend Options:${c.reset}
2459
- --bid-hash 0x... EIP-712 hash of accepted bid (required)
2460
- --reason "..." Reason for amendment (required)
2461
- --price 25 New price in USDC (optional)
2462
- --delivery 48h New delivery time (optional)
2463
- --scope-delta "..." Scope change description (optional)
2464
-
2465
- ${c.bold}Checkpoint Options:${c.reset}
2466
- --deliverable "..." Deliverable content/URL (required)
2467
- --milestone 0 Milestone index (default: 0)
2468
- --notes "..." Additional notes (optional)
60
+ ${c.bold}Subcommands:${c.reset}
61
+ ${subcommands.map(c => ` ${c.name.slice(group.length + 1).padEnd(16)} ${c.summary}`).join('\n')}
2469
62
 
2470
- ${c.bold}Examples:${c.reset}
2471
- obolos anp list --status=open
2472
- obolos anp create --title "Analyze dataset" --description "Parse CSV" --min-budget 5 --max-budget 50 --deadline 7d --duration 3d
2473
- obolos anp bid sha256-abc123... --price 25 --delivery 48h --message "I can do this"
2474
- obolos anp accept sha256-listing... --bid sha256-bid...
2475
- obolos anp message job-uuid --message "Landscape or portrait?" --role provider
2476
- obolos anp thread job-uuid
2477
- obolos anp amend job-uuid --bid-hash 0x... --reason "Scope expanded" --price 35
2478
- obolos anp checkpoint job-uuid --deliverable "https://..." --milestone 0 --notes "Script draft"
63
+ Run ${c.cyan}obolos ${group} <subcommand> --help${c.reset} for subcommand details.
2479
64
  `);
65
+ return true;
2480
66
  }
2481
- async function cmdAnp(args) {
2482
- const sub = args[0];
2483
- const subArgs = args.slice(1);
2484
- switch (sub) {
2485
- case 'list':
2486
- case 'ls':
2487
- await cmdAnpList(subArgs);
2488
- break;
2489
- case 'info':
2490
- case 'show':
2491
- await cmdAnpInfo(subArgs);
2492
- break;
2493
- case 'create':
2494
- case 'new':
2495
- await cmdAnpCreate(subArgs);
2496
- break;
2497
- case 'bid':
2498
- await cmdAnpBid(subArgs);
2499
- break;
2500
- case 'accept':
2501
- await cmdAnpAccept(subArgs);
2502
- break;
2503
- case 'verify':
2504
- await cmdAnpVerify(subArgs);
2505
- break;
2506
- // IML commands
2507
- case 'message':
2508
- case 'msg':
2509
- await cmdAnpMessage(subArgs);
2510
- break;
2511
- case 'thread':
2512
- await cmdAnpThread(subArgs);
2513
- break;
2514
- case 'amend':
2515
- await cmdAnpAmend(subArgs);
2516
- break;
2517
- case 'accept-amend':
2518
- await cmdAnpAcceptAmend(subArgs);
2519
- break;
2520
- case 'amendments':
2521
- await cmdAnpAmendments(subArgs);
2522
- break;
2523
- case 'checkpoint':
2524
- case 'cp':
2525
- await cmdAnpCheckpoint(subArgs);
2526
- break;
2527
- case 'approve-cp':
2528
- await cmdAnpApproveCp(subArgs);
2529
- break;
2530
- case 'checkpoints':
2531
- case 'cps':
2532
- await cmdAnpCheckpoints(subArgs);
2533
- break;
2534
- case 'help':
2535
- case '--help':
2536
- case '-h':
2537
- case undefined:
2538
- showAnpHelp();
2539
- break;
2540
- default:
2541
- console.error(`${c.red}Unknown anp subcommand: ${sub}${c.reset}`);
2542
- showAnpHelp();
2543
- process.exit(1);
2544
- }
2545
- }
2546
- // ─── Reputation Commands ─────────────────────────────────────────────────────
2547
- function tierColor(tier) {
2548
- switch (tier.toLowerCase()) {
2549
- case 'diamond': return `${c.bold}${c.cyan}${tier}${c.reset}`;
2550
- case 'platinum': return `${c.bold}${c.white}${tier}${c.reset}`;
2551
- case 'gold':
2552
- case 'established': return `${c.yellow}${tier}${c.reset}`;
2553
- case 'silver':
2554
- case 'developing': return `${c.dim}${c.white}${tier}${c.reset}`;
2555
- case 'bronze':
2556
- case 'limited': return `${c.dim}${tier}${c.reset}`;
2557
- case 'flagged': return `${c.red}${tier}${c.reset}`;
2558
- default: return `${c.dim}${tier}${c.reset}`;
2559
- }
2560
- }
2561
- function scoreBar(score) {
2562
- const width = 20;
2563
- const filled = Math.round((score / 100) * width);
2564
- const empty = width - filled;
2565
- let color = c.red;
2566
- if (score >= 70)
2567
- color = c.green;
2568
- else if (score >= 40)
2569
- color = c.yellow;
2570
- return `${color}${'█'.repeat(filled)}${c.dim}${'░'.repeat(empty)}${c.reset} ${color}${score}${c.reset}/100`;
2571
- }
2572
- function verdictLabel(pass) {
2573
- return pass
2574
- ? `${c.green}✔ PASS${c.reset}`
2575
- : `${c.red}✘ FAIL${c.reset}`;
2576
- }
2577
- async function cmdReputationCheck(args) {
2578
- const agentId = getPositional(args, 0);
2579
- if (!agentId) {
2580
- console.error(`${c.red}Usage: obolos reputation check <agentId> [--chain base]${c.reset}`);
2581
- process.exit(1);
2582
- }
2583
- const chain = getFlag(args, 'chain') || 'base';
2584
- console.log(`\n${c.dim}Checking reputation for agent ${c.bold}${agentId}${c.reset}${c.dim} on ${chain}...${c.reset}\n`);
2585
- const data = await apiGet(`/api/anp/reputation/${encodeURIComponent(agentId)}?chain=${encodeURIComponent(chain)}`);
2586
- // Header
2587
- console.log(`${c.bold}${c.cyan}Reputation Report${c.reset} ${c.dim}Agent ${agentId}${c.reset}`);
2588
- console.log(`${c.dim}${'─'.repeat(60)}${c.reset}`);
2589
- // Combined score
2590
- const combined = data.combined || {};
2591
- console.log(` ${c.bold}Combined Score:${c.reset} ${scoreBar(combined.score ?? 0)}`);
2592
- console.log(` ${c.bold}Tier:${c.reset} ${tierColor(combined.tier ?? 'unknown')}`);
2593
- console.log(` ${c.bold}Verdict:${c.reset} ${verdictLabel(combined.pass ?? false)}`);
2594
- console.log(` ${c.bold}Chain:${c.reset} ${data.chain || chain}`);
2595
- if (data.address) {
2596
- console.log(` ${c.bold}Address:${c.reset} ${data.address}`);
2597
- }
2598
- // Sybil warning
2599
- if (combined.hasSybilFlags) {
2600
- console.log(`\n ${c.red}${c.bold}⚠ Sybil flags detected${c.reset}`);
2601
- }
2602
- // Individual provider scores
2603
- const scores = data.scores || [];
2604
- if (scores.length > 0) {
2605
- console.log(`\n ${c.bold}${c.cyan}Provider Scores (${scores.length})${c.reset}`);
2606
- console.log(` ${c.dim}${'─'.repeat(56)}${c.reset}`);
2607
- for (const s of scores) {
2608
- const provider = s.provider === 'rnwy' ? 'RNWY' : s.provider === 'agentproof' ? 'AgentProof' : s.provider;
2609
- console.log(`\n ${c.bold}${provider}${c.reset}`);
2610
- console.log(` Score: ${scoreBar(s.score ?? 0)}`);
2611
- console.log(` Tier: ${tierColor(s.tier ?? 'unknown')}`);
2612
- console.log(` Verdict: ${verdictLabel(s.pass ?? false)}`);
2613
- if (s.sybilFlags && s.sybilFlags.length > 0) {
2614
- console.log(` ${c.red}Sybil: ${s.sybilFlags.join(', ')}${c.reset}`);
2615
- }
2616
- if (s.riskFlags && s.riskFlags.length > 0) {
2617
- console.log(` ${c.yellow}Risk: ${s.riskFlags.join(', ')}${c.reset}`);
2618
- }
2619
- }
2620
- }
2621
- else {
2622
- console.log(`\n ${c.dim}No provider scores available.${c.reset}`);
2623
- }
2624
- console.log(`\n ${c.dim}Checked: ${data.checkedAt ? formatDate(data.checkedAt) : 'just now'}${c.reset}\n`);
2625
- }
2626
- async function cmdReputationCompare(args) {
2627
- // Collect all positional args (agent IDs, optionally prefixed with chain:)
2628
- const agents = [];
2629
- for (const arg of args) {
2630
- if (arg.startsWith('--'))
2631
- continue;
2632
- const parts = arg.split(':');
2633
- if (parts.length === 2) {
2634
- const id = parseInt(parts[1], 10);
2635
- if (isNaN(id)) {
2636
- console.error(`${c.red}Invalid agent ID: ${parts[1]}${c.reset}`);
2637
- process.exit(1);
2638
- }
2639
- agents.push({ agentId: id, chain: parts[0] });
2640
- }
2641
- else {
2642
- const id = parseInt(parts[0], 10);
2643
- if (isNaN(id)) {
2644
- console.error(`${c.red}Invalid agent ID: ${parts[0]}${c.reset}`);
2645
- process.exit(1);
2646
- }
2647
- agents.push({ agentId: id, chain: 'base' });
2648
- }
2649
- }
2650
- if (agents.length < 2) {
2651
- console.error(`${c.red}Usage: obolos reputation compare <id1> <id2> [id3...]${c.reset}`);
2652
- console.error(`${c.dim} Prefix with chain: obolos rep compare base:123 ethereum:456${c.reset}`);
2653
- process.exit(1);
67
+ async function main() {
68
+ if (!command || command === 'help' || command === '--help' || command === '-h') {
69
+ showHelp();
70
+ return;
2654
71
  }
2655
- console.log(`\n${c.dim}Comparing ${agents.length} agents in parallel...${c.reset}\n`);
2656
- // Fetch all in parallel
2657
- const results = await Promise.all(agents.map(a => apiGet(`/api/anp/reputation/${a.agentId}?chain=${encodeURIComponent(a.chain)}`)
2658
- .catch((err) => ({ agentId: a.agentId, chain: a.chain, error: err.message }))));
2659
- // Sort by combined score descending
2660
- const sorted = results
2661
- .map((r, i) => ({ ...r, _input: agents[i] }))
2662
- .sort((a, b) => ((b.combined?.score ?? -1) - (a.combined?.score ?? -1)));
2663
- // Table header
2664
- console.log(`${c.bold}${c.cyan}Reputation Comparison${c.reset}`);
2665
- console.log(`${c.dim}${'─'.repeat(74)}${c.reset}`);
2666
- console.log(` ${c.bold}${'#'.padEnd(4)}${'Agent'.padEnd(12)}${'Chain'.padEnd(12)}${'Score'.padEnd(24)}${'Tier'.padEnd(14)}Verdict${c.reset}`);
2667
- console.log(` ${c.dim}${'─'.repeat(70)}${c.reset}`);
2668
- sorted.forEach((r, i) => {
2669
- const rank = `${i + 1}.`.padEnd(4);
2670
- const agent = String(r._input.agentId).padEnd(12);
2671
- const chain = r._input.chain.padEnd(12);
2672
- if (r.error) {
2673
- 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))
2674
75
  return;
2675
- }
2676
- const combined = r.combined || {};
2677
- const score = combined.score ?? 0;
2678
- const bar = scoreBar(score);
2679
- // scoreBar has ANSI codes so we can't pad it normally; we pad the raw number
2680
- const barPadded = bar; // already formatted
2681
- const tier = tierColor(combined.tier ?? 'unknown');
2682
- const verdict = verdictLabel(combined.pass ?? false);
2683
- const sybil = combined.hasSybilFlags ? ` ${c.red}⚠ sybil${c.reset}` : '';
2684
- console.log(` ${rank}${agent}${chain}${barPadded} ${tier.padEnd(14)} ${verdict}${sybil}`);
2685
- });
2686
- console.log(`${c.dim}${'─'.repeat(74)}${c.reset}\n`);
2687
- }
2688
- function showReputationHelp() {
2689
- console.log(`
2690
- ${c.bold}${c.cyan}obolos reputation${c.reset} — Agent trust & reputation checking
2691
-
2692
- ${c.bold}Subcommands:${c.reset}
2693
- check <agentId> Check reputation for an agent
2694
- compare <id1> <id2> [...] Compare multiple agents side-by-side
2695
-
2696
- ${c.bold}Options (check):${c.reset}
2697
- --chain <chain> Blockchain to check (default: base)
2698
-
2699
- ${c.bold}Examples:${c.reset}
2700
- obolos reputation check 16907
2701
- obolos reputation check 16907 --chain ethereum
2702
- obolos rep check 16907
2703
- obolos reputation compare 123 456 789
2704
- obolos rep compare base:123 base:456 ethereum:789
2705
- `);
2706
- }
2707
- async function cmdReputation(args) {
2708
- const sub = args[0];
2709
- const subArgs = args.slice(1);
2710
- switch (sub) {
2711
- case 'check':
2712
- await cmdReputationCheck(subArgs);
2713
- break;
2714
- case 'compare':
2715
- case 'cmp':
2716
- await cmdReputationCompare(subArgs);
2717
- break;
2718
- case 'help':
2719
- case '--help':
2720
- case '-h':
2721
- case undefined:
2722
- showReputationHelp();
2723
- break;
2724
- default:
2725
- console.error(`${c.red}Unknown reputation subcommand: ${sub}${c.reset}`);
2726
- showReputationHelp();
2727
- process.exit(1);
2728
76
  }
2729
- }
2730
- // ─── Help ───────────────────────────────────────────────────────────────────
2731
- function showHelp() {
2732
- console.log(`
2733
- ${c.bold}${c.cyan}obolos${c.reset} — CLI for the Obolos x402 API Marketplace
2734
-
2735
- ${c.bold}Usage:${c.reset}
2736
- obolos search [query] Search APIs by keyword
2737
- obolos categories List all API categories
2738
- obolos info <id> Get full API details
2739
- obolos call <id> [options] Call an API with payment
2740
- obolos balance Check wallet USDC balance
2741
- obolos setup Configure wallet (interactive)
2742
- obolos setup --generate Generate a new wallet
2743
- obolos setup --show Show current wallet config
2744
- obolos setup-mcp Show MCP server setup instructions
2745
-
2746
- ${c.bold}Job Commands (ERC-8183 ACP):${c.reset}
2747
- obolos job list [options] List jobs with filters
2748
- obolos job create [options] Create a new job
2749
- obolos job info <id> Get full job details
2750
- obolos job fund <id> Fund a job's escrow
2751
- obolos job submit <id> [opts] Submit work for a job
2752
- obolos job complete <id> Approve a job (evaluator)
2753
- obolos job reject <id> Reject a job submission
2754
- obolos job help Show job command help
2755
-
2756
- ${c.bold}Listing Commands (Negotiation):${c.reset}
2757
- obolos listing list [options] Browse open job listings
2758
- obolos listing create [opts] Create a listing for bids
2759
- obolos listing info <id> Get listing details + bids
2760
- obolos listing bid <id> [opts] Submit a bid on a listing
2761
- obolos listing accept <id> Accept a bid (creates job)
2762
- obolos listing cancel <id> Cancel a listing
2763
- obolos listing help Show listing command help
2764
-
2765
- ${c.bold}ANP Commands (Agent Negotiation Protocol):${c.reset}
2766
- obolos anp list [options] Browse ANP listings
2767
- obolos anp info <cid> Get listing details + bids
2768
- obolos anp create [options] Sign and publish a listing
2769
- obolos anp bid <cid> [opts] Sign and publish a bid
2770
- obolos anp accept <cid> [opts] Accept a bid (sign AcceptIntent)
2771
- obolos anp verify <cid> Verify document integrity
2772
- obolos anp message <job> [opts] Send in-job message
2773
- obolos anp thread <job> View job message thread
2774
- obolos anp amend <job> [opts] Propose amendment
2775
- obolos anp checkpoint <job> Submit milestone checkpoint
2776
- obolos anp help Show ANP command help
2777
-
2778
- ${c.bold}Reputation Commands:${c.reset}
2779
- obolos reputation check <id> Check agent trust score
2780
- obolos reputation compare ... Compare multiple agents
2781
- obolos reputation help Show reputation command help
2782
- ${c.dim}(alias: obolos rep ...)${c.reset}
2783
-
2784
- ${c.bold}Call Options:${c.reset}
2785
- --method POST|GET|PUT HTTP method (default: GET)
2786
- --body '{"key":"value"}' Request body (JSON)
2787
-
2788
- ${c.bold}Config:${c.reset}
2789
- Wallet key is loaded from ~/.obolos/config.json or OBOLOS_PRIVATE_KEY env var.
2790
- Run ${c.cyan}obolos setup${c.reset} to configure.
2791
-
2792
- ${c.bold}Examples:${c.reset}
2793
- obolos setup --generate
2794
- obolos search "token price"
2795
- obolos info a59a0377-d77b-4fee-...
2796
- obolos call a59a0377-... --body '{"prompt":"a cat in space"}'
2797
- obolos job list --status=open
2798
- obolos job create --title "Analyze data" --evaluator 0xABC... --budget 5.00
2799
- obolos listing list --status=open
2800
- obolos listing create --title "Parse CSV data" --max-budget 10.00 --deadline 7d
2801
- obolos listing bid abc123 --price 5.00 --message "I can do this"
2802
- obolos listing accept abc123 --bid bid456
2803
- obolos anp list --status=open
2804
- obolos anp create --title "Analyze data" --min-budget 5 --max-budget 50 --deadline 7d
2805
- obolos anp bid sha256-abc... --price 25 --delivery 48h --message "I can do this"
2806
- obolos anp accept sha256-listing... --bid sha256-bid...
2807
- obolos rep check 16907
2808
- obolos rep check 16907 --chain ethereum
2809
- obolos rep compare 123 456 789
2810
- obolos rep compare base:123 ethereum:456
2811
- `);
2812
- }
2813
- // ─── Main ───────────────────────────────────────────────────────────────────
2814
- const args = process.argv.slice(2);
2815
- const command = args[0];
2816
- const commandArgs = args.slice(1);
2817
- async function main() {
2818
- switch (command) {
2819
- case 'search':
2820
- case 's':
2821
- await cmdSearch(commandArgs);
2822
- break;
2823
- case 'categories':
2824
- case 'cats':
2825
- await cmdCategories();
2826
- break;
2827
- case 'info':
2828
- case 'i':
2829
- await cmdInfo(commandArgs);
2830
- break;
2831
- case 'call':
2832
- case 'c':
2833
- await cmdCall(commandArgs);
2834
- break;
2835
- case 'balance':
2836
- case 'bal':
2837
- await cmdBalance();
2838
- break;
2839
- case 'setup':
2840
- await cmdSetup(commandArgs);
2841
- break;
2842
- case 'setup-mcp':
2843
- case 'mcp':
2844
- await cmdSetupMcp();
2845
- break;
2846
- case 'job':
2847
- case 'j':
2848
- await cmdJob(commandArgs);
2849
- break;
2850
- case 'listing':
2851
- case 'l':
2852
- await cmdListing(commandArgs);
2853
- break;
2854
- case 'anp':
2855
- await cmdAnp(commandArgs);
2856
- break;
2857
- case 'reputation':
2858
- case 'rep':
2859
- await cmdReputation(commandArgs);
2860
- break;
2861
- case 'help':
2862
- case '--help':
2863
- case '-h':
2864
- case undefined:
2865
- showHelp();
2866
- break;
2867
- default:
2868
- console.error(`${c.red}Unknown command: ${command}${c.reset}`);
2869
- showHelp();
2870
- 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;
2871
82
  }
83
+ process.stderr.write(`${c.red}Unknown command: ${command}${c.reset}\n`);
84
+ showHelp();
85
+ process.exit(1);
2872
86
  }
2873
87
  main().catch((err) => {
2874
- console.error(`${c.red}Error: ${err.message}${c.reset}`);
88
+ process.stderr.write(`${c.red}Error: ${err.message}${c.reset}\n`);
2875
89
  process.exit(1);
2876
90
  });
2877
91
  //# sourceMappingURL=index.js.map