@obolos_tech/cli 0.3.2 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -21,6 +21,10 @@
21
21
  * npx @obolos_tech/cli anp bid <cid> --price 25 --delivery 48h
22
22
  * npx @obolos_tech/cli anp accept <cid> --bid <bid_cid>
23
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
24
28
  */
25
29
  import { homedir } from 'os';
26
30
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
@@ -323,19 +327,7 @@ function stateVisualization(status) {
323
327
  return ` ${parts.join(` ${c.dim}->${c.reset} `)}`;
324
328
  }
325
329
  // ─── ANP Helpers ─────────────────────────────────────────────────────────────
326
- function canonicalJSON(obj) {
327
- if (obj === null || typeof obj !== 'object')
328
- return JSON.stringify(obj);
329
- if (Array.isArray(obj))
330
- return '[' + obj.map(canonicalJSON).join(',') + ']';
331
- const keys = Object.keys(obj).sort();
332
- return '{' + keys.map(k => JSON.stringify(k) + ':' + canonicalJSON(obj[k])).join(',') + '}';
333
- }
334
- async function computeContentHash(data) {
335
- const { createHash } = await import('crypto');
336
- const hash = createHash('sha256').update(canonicalJSON(data)).digest('hex');
337
- return `0x${hash}`;
338
- }
330
+ import { computeContentHash, ANP_TYPES, getANPDomain, hashListingIntent, hashBidIntent, hashAcceptIntent, hashAmendmentIntent, hashCheckpointIntent, usdToUsdc } from '@obolos_tech/anp-sdk';
339
331
  function parseTimeToSeconds(input) {
340
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);
341
333
  if (!match) {
@@ -353,67 +345,26 @@ function parseTimeToSeconds(input) {
353
345
  return num * 86400;
354
346
  return num;
355
347
  }
356
- const ANP_DOMAIN = {
357
- name: 'ANP',
358
- version: '1',
359
- chainId: 8453,
360
- verifyingContract: '0xfEa362Bf569e97B20681289fB4D4a64CEBDFa792',
361
- };
362
- const ANP_LISTING_TYPES = {
363
- ListingIntent: [
364
- { name: 'contentHash', type: 'bytes32' },
365
- { name: 'minBudget', type: 'uint256' },
366
- { name: 'maxBudget', type: 'uint256' },
367
- { name: 'deadline', type: 'uint256' },
368
- { name: 'jobDuration', type: 'uint256' },
369
- { name: 'preferredEvaluator', type: 'address' },
370
- { name: 'nonce', type: 'uint256' },
371
- ],
372
- };
373
- const ANP_BID_TYPES = {
374
- BidIntent: [
375
- { name: 'listingHash', type: 'bytes32' },
376
- { name: 'contentHash', type: 'bytes32' },
377
- { name: 'price', type: 'uint256' },
378
- { name: 'deliveryTime', type: 'uint256' },
379
- { name: 'nonce', type: 'uint256' },
380
- ],
381
- };
382
- const ANP_ACCEPT_TYPES = {
383
- AcceptIntent: [
384
- { name: 'listingHash', type: 'bytes32' },
385
- { name: 'bidHash', type: 'bytes32' },
386
- { name: 'nonce', type: 'uint256' },
387
- ],
388
- };
348
+ const ANP_DOMAIN = getANPDomain(8453, '0xfEa362Bf569e97B20681289fB4D4a64CEBDFa792');
389
349
  async function getANPSigningClient() {
390
350
  if (!OBOLOS_PRIVATE_KEY) {
391
351
  console.error(`${c.red}No wallet configured.${c.reset} Run ${c.cyan}obolos setup${c.reset} first.`);
392
352
  process.exit(1);
393
353
  }
394
- const { createWalletClient, http: viemHttp, encodeAbiParameters, keccak256 } = await import('viem');
354
+ const { createWalletClient, http: viemHttp } = await import('viem');
395
355
  const { privateKeyToAccount } = await import('viem/accounts');
396
356
  const { base } = await import('viem/chains');
397
357
  const key = OBOLOS_PRIVATE_KEY.startsWith('0x') ? OBOLOS_PRIVATE_KEY : `0x${OBOLOS_PRIVATE_KEY}`;
398
358
  const account = privateKeyToAccount(key);
399
359
  const walletClient = createWalletClient({ account, chain: base, transport: viemHttp() });
400
- const LISTING_TYPEHASH = keccak256(Buffer.from('ListingIntent(bytes32 contentHash,uint256 minBudget,uint256 maxBudget,uint256 deadline,uint256 jobDuration,address preferredEvaluator,uint256 nonce)'));
401
- const BID_TYPEHASH = keccak256(Buffer.from('BidIntent(bytes32 listingHash,bytes32 contentHash,uint256 price,uint256 deliveryTime,uint256 nonce)'));
402
- const ACCEPT_TYPEHASH = keccak256(Buffer.from('AcceptIntent(bytes32 listingHash,bytes32 bidHash,uint256 nonce)'));
403
- function hashListingStruct(listing) {
404
- return keccak256(encodeAbiParameters([{ type: 'bytes32' }, { type: 'bytes32' }, { type: 'uint256' }, { type: 'uint256' }, { type: 'uint256' }, { type: 'uint256' }, { type: 'address' }, { type: 'uint256' }], [LISTING_TYPEHASH, listing.contentHash, listing.minBudget, listing.maxBudget, listing.deadline, listing.jobDuration, listing.preferredEvaluator, listing.nonce]));
405
- }
406
- function hashBidStruct(bid) {
407
- return keccak256(encodeAbiParameters([{ type: 'bytes32' }, { type: 'bytes32' }, { type: 'bytes32' }, { type: 'uint256' }, { type: 'uint256' }, { type: 'uint256' }], [BID_TYPEHASH, bid.listingHash, bid.contentHash, bid.price, bid.deliveryTime, bid.nonce]));
408
- }
409
- function hashAcceptStruct(accept) {
410
- return keccak256(encodeAbiParameters([{ type: 'bytes32' }, { type: 'bytes32' }, { type: 'bytes32' }, { type: 'uint256' }], [ACCEPT_TYPEHASH, accept.listingHash, accept.bidHash, accept.nonce]));
411
- }
412
- return { account, walletClient, hashListingStruct, hashBidStruct, hashAcceptStruct };
360
+ return { account, walletClient, hashListingStruct: hashListingIntent, hashBidStruct: hashBidIntent, hashAcceptStruct: hashAcceptIntent, hashAmendmentStruct: hashAmendmentIntent, hashCheckpointStruct: hashCheckpointIntent };
413
361
  }
414
362
  function generateNonce() {
415
363
  return BigInt(Math.floor(Math.random() * 2 ** 32));
416
364
  }
365
+ async function computeJobHash(jobId) {
366
+ return computeContentHash({ jobId });
367
+ }
417
368
  // ─── Commands ───────────────────────────────────────────────────────────────
418
369
  async function cmdSearch(args) {
419
370
  const query = args.join(' ');
@@ -1953,7 +1904,7 @@ async function cmdAnpCreate(args) {
1953
1904
  const signature = await anp.walletClient.signTypedData({
1954
1905
  account: anp.account,
1955
1906
  domain: ANP_DOMAIN,
1956
- types: ANP_LISTING_TYPES,
1907
+ types: { ListingIntent: ANP_TYPES.ListingIntent },
1957
1908
  primaryType: 'ListingIntent',
1958
1909
  message,
1959
1910
  });
@@ -2040,7 +1991,7 @@ async function cmdAnpBid(args) {
2040
1991
  const signature = await anp.walletClient.signTypedData({
2041
1992
  account: anp.account,
2042
1993
  domain: ANP_DOMAIN,
2043
- types: ANP_BID_TYPES,
1994
+ types: { BidIntent: ANP_TYPES.BidIntent },
2044
1995
  primaryType: 'BidIntent',
2045
1996
  message: bidMessage,
2046
1997
  });
@@ -2125,7 +2076,7 @@ async function cmdAnpAccept(args) {
2125
2076
  const signature = await anp.walletClient.signTypedData({
2126
2077
  account: anp.account,
2127
2078
  domain: ANP_DOMAIN,
2128
- types: ANP_ACCEPT_TYPES,
2079
+ types: { AcceptIntent: ANP_TYPES.AcceptIntent },
2129
2080
  primaryType: 'AcceptIntent',
2130
2081
  message: acceptMessage,
2131
2082
  });
@@ -2189,6 +2140,274 @@ async function cmdAnpVerify(args) {
2189
2140
  }
2190
2141
  console.log();
2191
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
+ }
2192
2411
  function showAnpHelp() {
2193
2412
  console.log(`
2194
2413
  ${c.bold}${c.cyan}obolos anp${c.reset} — Agent Negotiation Protocol (EIP-712 signed documents)
@@ -2222,13 +2441,41 @@ ${c.bold}Bid Options:${c.reset}
2222
2441
  ${c.bold}Accept Options:${c.reset}
2223
2442
  --bid <bid_cid> Bid CID to accept (required)
2224
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)
2457
+
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)
2469
+
2225
2470
  ${c.bold}Examples:${c.reset}
2226
2471
  obolos anp list --status=open
2227
2472
  obolos anp create --title "Analyze dataset" --description "Parse CSV" --min-budget 5 --max-budget 50 --deadline 7d --duration 3d
2228
- obolos anp info sha256-abc123...
2229
2473
  obolos anp bid sha256-abc123... --price 25 --delivery 48h --message "I can do this"
2230
2474
  obolos anp accept sha256-listing... --bid sha256-bid...
2231
- obolos anp verify sha256-abc123...
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"
2232
2479
  `);
2233
2480
  }
2234
2481
  async function cmdAnp(args) {
@@ -2256,6 +2503,34 @@ async function cmdAnp(args) {
2256
2503
  case 'verify':
2257
2504
  await cmdAnpVerify(subArgs);
2258
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;
2259
2534
  case 'help':
2260
2535
  case '--help':
2261
2536
  case '-h':
@@ -2268,6 +2543,190 @@ async function cmdAnp(args) {
2268
2543
  process.exit(1);
2269
2544
  }
2270
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);
2654
+ }
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}`);
2674
+ 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
+ }
2729
+ }
2271
2730
  // ─── Help ───────────────────────────────────────────────────────────────────
2272
2731
  function showHelp() {
2273
2732
  console.log(`
@@ -2310,8 +2769,18 @@ ${c.bold}ANP Commands (Agent Negotiation Protocol):${c.reset}
2310
2769
  obolos anp bid <cid> [opts] Sign and publish a bid
2311
2770
  obolos anp accept <cid> [opts] Accept a bid (sign AcceptIntent)
2312
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
2313
2776
  obolos anp help Show ANP command help
2314
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
+
2315
2784
  ${c.bold}Call Options:${c.reset}
2316
2785
  --method POST|GET|PUT HTTP method (default: GET)
2317
2786
  --body '{"key":"value"}' Request body (JSON)
@@ -2335,6 +2804,10 @@ ${c.bold}Examples:${c.reset}
2335
2804
  obolos anp create --title "Analyze data" --min-budget 5 --max-budget 50 --deadline 7d
2336
2805
  obolos anp bid sha256-abc... --price 25 --delivery 48h --message "I can do this"
2337
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
2338
2811
  `);
2339
2812
  }
2340
2813
  // ─── Main ───────────────────────────────────────────────────────────────────
@@ -2381,6 +2854,10 @@ async function main() {
2381
2854
  case 'anp':
2382
2855
  await cmdAnp(commandArgs);
2383
2856
  break;
2857
+ case 'reputation':
2858
+ case 'rep':
2859
+ await cmdReputation(commandArgs);
2860
+ break;
2384
2861
  case 'help':
2385
2862
  case '--help':
2386
2863
  case '-h':