@t2000/cli 10.30.0 → 10.30.1

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
@@ -34368,7 +34368,7 @@ var import_picocolors8 = __toESM(require_picocolors(), 1);
34368
34368
  function registerPay(program3) {
34369
34369
  program3.command("pay <url>").description("Pay an x402 Service (USDC on Sui) \u2014 the seller's own endpoint").option("--key <path>", "Custom wallet path (default ~/.t2000/wallet.key)").option("--method <method>", "HTTP method (GET, POST, PUT)", "GET").option("--data <json>", "Request body for POST/PUT (auto-promotes --method to POST)").option("--header <key=value>", "Additional HTTP header (repeatable)", collectHeaders, {}).option("--max-price <amount>", "Max USDC price to auto-approve", "1.00").option(
34370
34370
  "--estimate",
34371
- "Preview the price + service info (no signing, no payment). Exits 0 if the service responds with a 402 challenge."
34371
+ 'Preview the price + service info (no signing, no payment). Exits 0 ONLY if the service answers a payable 402 x402 challenge; any other response (including 2xx "no payment required") exits 1.'
34372
34372
  ).option("--force", "Override spending limits for this call (see `t2 limit`)").addHelpText(
34373
34373
  "after",
34374
34374
  `
@@ -34439,6 +34439,23 @@ Examples:
34439
34439
  }
34440
34440
  });
34441
34441
  }
34442
+ var ESTIMATE_PREVIEW_MAX = 512;
34443
+ function truncatePreview(body2, max = ESTIMATE_PREVIEW_MAX) {
34444
+ if (body2.length <= max) {
34445
+ return body2;
34446
+ }
34447
+ return `${body2.slice(0, max)}\u2026 (truncated, ${body2.length} total bytes)`;
34448
+ }
34449
+ var EstimateFailure = class extends Error {
34450
+ constructor(message, payload) {
34451
+ super(message);
34452
+ this.payload = payload;
34453
+ this.name = "EstimateFailure";
34454
+ }
34455
+ toJSON() {
34456
+ return this.payload;
34457
+ }
34458
+ };
34442
34459
  async function runEstimate(url, opts) {
34443
34460
  const method = opts.data && opts.method === "GET" ? "POST" : opts.method;
34444
34461
  const canHaveBody = method !== "GET" && method !== "HEAD";
@@ -34461,28 +34478,33 @@ async function runEstimate(url, opts) {
34461
34478
  });
34462
34479
  if (response.status !== 402) {
34463
34480
  const body2 = await response.text().catch(() => "");
34464
- if (isJsonMode()) {
34465
- printJson({
34481
+ const preview = truncatePreview(body2);
34482
+ const open = response.status >= 200 && response.status < 300;
34483
+ const note = open ? `Endpoint responded ${response.status} without a 402 challenge \u2014 no payment required.` : `Endpoint responded ${response.status} (not a 402 payment challenge).`;
34484
+ if (!isJsonMode()) {
34485
+ if (open) {
34486
+ printInfo(`No payment required (status ${response.status}).`);
34487
+ } else {
34488
+ printInfo(`Status ${response.status} \u2014 not a 402 payment challenge.`);
34489
+ }
34490
+ if (preview) {
34491
+ printBlank();
34492
+ console.log(preview);
34493
+ printBlank();
34494
+ }
34495
+ }
34496
+ throw new EstimateFailure(
34497
+ `${note} --estimate exits 0 only for a payable 402 x402 challenge.`,
34498
+ {
34466
34499
  url,
34467
34500
  method,
34468
34501
  status: response.status,
34502
+ ok: false,
34469
34503
  estimate: null,
34470
- note: response.status >= 200 && response.status < 300 ? "Endpoint responded without a 402 challenge \u2014 no payment required." : `Endpoint responded with ${response.status} (not a 402 payment challenge).`,
34471
- body: body2
34472
- });
34473
- return;
34474
- }
34475
- if (response.status >= 200 && response.status < 300) {
34476
- printSuccess(`No payment required (status ${response.status}).`);
34477
- } else {
34478
- printInfo(`Status ${response.status} \u2014 not a 402 payment challenge.`);
34479
- }
34480
- if (body2) {
34481
- printBlank();
34482
- console.log(body2);
34483
- printBlank();
34484
- }
34485
- return;
34504
+ note,
34505
+ ...preview ? { bodyPreview: preview } : {}
34506
+ }
34507
+ );
34486
34508
  }
34487
34509
  let accepts = [];
34488
34510
  try {
@@ -36387,24 +36409,76 @@ async function fetchSellerJobs(base, seller) {
36387
36409
  return json.jobs ?? [];
36388
36410
  }
36389
36411
  var TERMINAL_STATES = /* @__PURE__ */ new Set(["released", "rejected", "refunded"]);
36390
- function inboxHint(job) {
36391
- if (job.state === "funded") {
36392
- return `t2 job spec ${job.jobId} \u2192 do the work \u2192 t2 job deliver ${job.jobId} <file>`;
36412
+ function reviewClosesMs(job) {
36413
+ const anchor = job.deliveredAtMs ?? job.updatedAtMs;
36414
+ if (!(typeof anchor === "number" && anchor > 0 && typeof job.reviewWindowMs === "number" && job.reviewWindowMs > 0)) {
36415
+ return null;
36416
+ }
36417
+ return anchor + job.reviewWindowMs;
36418
+ }
36419
+ function bucketSellerJob(job, nowMs) {
36420
+ if (TERMINAL_STATES.has(job.state)) {
36421
+ return "terminal";
36393
36422
  }
36394
- if (job.state === "delivered") {
36395
- return `waiting on the buyer's review \u2014 anyone can \`t2 job release\` once it lapses`;
36423
+ if (job.state === "funded") {
36424
+ return nowMs <= job.deliverByMs ? "needsYou" : "fundedLate";
36425
+ }
36426
+ const closes = reviewClosesMs(job);
36427
+ return closes !== null && nowMs > closes ? "releasable" : "awaitingBuyer";
36428
+ }
36429
+ function summarizeSellerInbox(jobs, nowMs) {
36430
+ const buckets = {
36431
+ counts: { total: jobs.length, needsYou: 0, fundedLate: 0, awaitingBuyer: 0, releasable: 0, terminal: 0 },
36432
+ needsYou: [],
36433
+ fundedLate: [],
36434
+ awaitingBuyer: [],
36435
+ releasable: [],
36436
+ terminal: []
36437
+ };
36438
+ for (const job of jobs) {
36439
+ const bucket = bucketSellerJob(job, nowMs);
36440
+ buckets[bucket].push(job);
36441
+ buckets.counts[bucket] += 1;
36442
+ }
36443
+ return buckets;
36444
+ }
36445
+ function inboxHint(job, bucket) {
36446
+ switch (bucket) {
36447
+ case "needsYou":
36448
+ return `t2 job spec ${job.jobId} \u2192 do the work \u2192 t2 job deliver ${job.jobId} <file>`;
36449
+ case "releasable":
36450
+ return `releasable now \u2014 t2 job release ${job.jobId}`;
36451
+ case "awaitingBuyer": {
36452
+ const closes = reviewClosesMs(job);
36453
+ return closes !== null ? `waiting on the buyer's review \u2014 release becomes permissionless after ${new Date(closes).toISOString()}` : `waiting on the buyer's review \u2014 anyone can \`t2 job release\` once it lapses`;
36454
+ }
36455
+ case "fundedLate":
36456
+ return `deliver deadline passed \u2014 the chain rejects late delivers; the buyer (or anyone) may t2 job refund ${job.jobId}`;
36457
+ default:
36458
+ return "";
36396
36459
  }
36397
- return "";
36398
36460
  }
36399
- function printInboxRow(job) {
36461
+ function printInboxRow(job, bucket) {
36400
36462
  const deadline = job.state === "funded" ? ` \xB7 deliver by ${new Date(job.deliverByMs).toISOString()}` : "";
36401
36463
  printLine(
36402
36464
  ` ${stateColor(job.state)} $${job.amountUsdc.toFixed(2)} USDC \xB7 from ${truncateAddress(job.buyer)}${deadline}`
36403
36465
  );
36404
36466
  printLine(` ${import_picocolors13.default.dim(job.jobId)}`);
36405
- const hint = inboxHint(job);
36467
+ const hint = inboxHint(job, bucket);
36406
36468
  if (hint) printLine(` ${import_picocolors13.default.dim("\u2192")} ${hint}`);
36407
36469
  }
36470
+ function deliverPreflightError(state, deliverByMs, nowMs) {
36471
+ if (state === "delivered") {
36472
+ return "This job is already delivered. The delivery hash is permanent and cannot be replaced \u2014 wait for the buyer's review, or once the window closes anyone may run `t2 job release <jobId>`. Fixes travel via buyer reject (inside the window) or out-of-band, never a second deliver.";
36473
+ }
36474
+ if (state === "released" || state === "rejected" || state === "refunded") {
36475
+ return `This job is ${state} \u2014 nothing can be delivered.`;
36476
+ }
36477
+ if (state === "funded" && nowMs > deliverByMs) {
36478
+ return `The deliver deadline (${new Date(deliverByMs).toISOString()}) has passed \u2014 the chain rejects late delivers, so nothing was uploaded or signed. The refund path is open: anyone may run \`t2 job refund <jobId>\`.`;
36479
+ }
36480
+ return null;
36481
+ }
36408
36482
  async function sponsoredJobVerb(opts) {
36409
36483
  const agent = await withAgent({ keyPath: opts.keyPath });
36410
36484
  const address = agent.address();
@@ -36653,9 +36727,16 @@ no fund step; unclaimed openings refund fee-free):
36653
36727
  handleError(error);
36654
36728
  }
36655
36729
  });
36656
- group.command("deliver").argument("<jobId>", "The Job object id (0x\u2026)").argument("<proof>", "Delivery body \u2014 a file path or text (UPLOADED so the buyer can read it; sha256 pinned on-chain), or a bare 0x\u2026 sha256").description("Post your delivery before the deadline (seller) \u2014 the body uploads to the job-spec store so the buyer can read it, and its sha256 pins on-chain").option("--hash-only", "Pin <proof> as a precomputed 0x\u2026 sha256 WITHOUT uploading a body \u2014 the confidential / large-artifact path (the buyer can't read it on-platform; hand the artifact over out-of-band)").option("--key <path>", "Custom wallet path (default ~/.t2000/wallet.key)").option("--api <url>", `API base URL (default ${DEFAULT_API_BASE7})`).action(async (jobId, proof, opts) => {
36730
+ group.command("deliver").argument("<jobId>", "The Job object id (0x\u2026)").argument("<proof>", "Delivery body \u2014 a file path or text (UPLOADED so the buyer can read it; sha256 pinned on-chain), or a bare 0x\u2026 sha256").description("Post your delivery before the deadline (seller) \u2014 ONE SHOT: the sha256 pins on-chain permanently and cannot be replaced. Fix mistakes via buyer reject (inside the review window) or out-of-band \u2014 never a second deliver. Decline is only possible BEFORE delivery.").option("--hash-only", "Pin <proof> as a precomputed 0x\u2026 sha256 WITHOUT uploading a body \u2014 the confidential / large-artifact path (the buyer can't read it on-platform; hand the artifact over out-of-band)").option("--key <path>", "Custom wallet path (default ~/.t2000/wallet.key)").option("--api <url>", `API base URL (default ${DEFAULT_API_BASE7})`).action(async (jobId, proof, opts) => {
36657
36731
  try {
36658
36732
  const base = opts.api ?? DEFAULT_API_BASE7;
36733
+ const existing = await getJob(getSuiClient(), jobId).catch(() => null);
36734
+ if (existing) {
36735
+ const preflightError = deliverPreflightError(existing.state, existing.deliverByMs, Date.now());
36736
+ if (preflightError) {
36737
+ throw new Error(preflightError.replaceAll("<jobId>", jobId));
36738
+ }
36739
+ }
36659
36740
  let deliveryHash;
36660
36741
  let uploaded = false;
36661
36742
  if (opts.hashOnly) {
@@ -36667,6 +36748,9 @@ no fund step; unclaimed openings refund fee-free):
36667
36748
  } else {
36668
36749
  ({ hash: deliveryHash, uploaded } = await resolveSpecUpload(base, proof));
36669
36750
  }
36751
+ if (!isJsonMode()) {
36752
+ printInfo("Delivery pins once. The buyer review window starts after this succeeds; you cannot replace the hash.");
36753
+ }
36670
36754
  const { digest } = await sponsoredJobVerb({
36671
36755
  base,
36672
36756
  keyPath: opts.key,
@@ -36674,11 +36758,12 @@ no fund step; unclaimed openings refund fee-free):
36674
36758
  params: { jobId, deliveryHash }
36675
36759
  });
36676
36760
  if (isJsonMode()) {
36677
- printJson({ jobId, deliveryHash, uploaded, digest });
36761
+ printJson({ jobId, deliveryHash, uploaded, digest, onceOnly: true });
36678
36762
  return;
36679
36763
  }
36680
36764
  printBlank();
36681
36765
  printSuccess("Delivery posted \u2014 the buyer's review window is now open.");
36766
+ printInfo("This delivery cannot be amended \u2014 further `t2 job deliver` calls on this job will fail.");
36682
36767
  printKeyValue("Delivery hash", deliveryHash);
36683
36768
  if (digest) printKeyValue("Tx", digest);
36684
36769
  if (uploaded) {
@@ -36822,23 +36907,45 @@ no fund step; unclaimed openings refund fee-free):
36822
36907
  const base = opts.api ?? DEFAULT_API_BASE7;
36823
36908
  const seen = /* @__PURE__ */ new Map();
36824
36909
  const jobs = await fetchSellerJobs(base, me);
36910
+ const inbox = summarizeSellerInbox(jobs, Date.now());
36825
36911
  if (isJsonMode()) {
36826
- printJson({ seller: me, jobs });
36912
+ printJson({
36913
+ seller: me,
36914
+ counts: inbox.counts,
36915
+ needsYou: inbox.needsYou,
36916
+ fundedLate: inbox.fundedLate,
36917
+ awaitingBuyer: inbox.awaitingBuyer,
36918
+ releasable: inbox.releasable,
36919
+ terminal: inbox.terminal,
36920
+ jobs
36921
+ });
36827
36922
  return;
36828
36923
  }
36829
- const open = jobs.filter((j) => !TERMINAL_STATES.has(j.state));
36830
36924
  printBlank();
36831
- printInfo(`Provider inbox for ${truncateAddress(me)} \u2014 ${jobs.length} job(s), ${open.length} open.`);
36925
+ printInfo(
36926
+ `Provider inbox for ${truncateAddress(me)} \u2014 ${jobs.length} job(s) \xB7 ${inbox.counts.needsYou} need you \xB7 ${inbox.counts.awaitingBuyer} awaiting buyer \xB7 ${inbox.counts.releasable} releasable now`
36927
+ );
36928
+ if (inbox.counts.fundedLate > 0) {
36929
+ printLine(import_picocolors13.default.dim(` ${inbox.counts.fundedLate} past deliver deadline (not deliverable \u2014 refund path is open to others)`));
36930
+ }
36832
36931
  printBlank();
36833
- for (const job of open) {
36834
- printInboxRow(job);
36835
- printBlank();
36932
+ for (const [bucket, rows] of [
36933
+ ["needsYou", inbox.needsYou],
36934
+ ["releasable", inbox.releasable],
36935
+ ["awaitingBuyer", inbox.awaitingBuyer],
36936
+ ["fundedLate", inbox.fundedLate]
36937
+ ]) {
36938
+ for (const job of rows) {
36939
+ printInboxRow(job, bucket);
36940
+ printBlank();
36941
+ }
36836
36942
  }
36837
- if (open.length === 0) {
36943
+ const openCount = jobs.length - inbox.counts.terminal;
36944
+ if (openCount === 0) {
36838
36945
  printInfo("No open jobs. New hires appear here the moment the escrow funds.");
36839
36946
  printBlank();
36840
36947
  }
36841
- for (const job of jobs) seen.set(job.jobId, job.state);
36948
+ for (const job of jobs) seen.set(job.jobId, bucketSellerJob(job, Date.now()));
36842
36949
  if (opts.once) return;
36843
36950
  for (; ; ) {
36844
36951
  await new Promise((r) => setTimeout(r, intervalMs));
@@ -36848,17 +36955,21 @@ no fund step; unclaimed openings refund fee-free):
36848
36955
  } catch {
36849
36956
  continue;
36850
36957
  }
36958
+ const nowMs = Date.now();
36851
36959
  for (const job of latest) {
36960
+ const bucket = bucketSellerJob(job, nowMs);
36852
36961
  const prev = seen.get(job.jobId);
36853
- if (prev === job.state) continue;
36854
- seen.set(job.jobId, job.state);
36962
+ if (prev === bucket) continue;
36963
+ seen.set(job.jobId, bucket);
36855
36964
  printBlank();
36856
- if (prev === void 0 && job.state === "funded") {
36965
+ if (prev === void 0 && bucket === "needsYou") {
36857
36966
  printSuccess(`New job \u2014 $${job.amountUsdc.toFixed(2)} USDC escrowed for you.`);
36967
+ } else if (bucket === "releasable") {
36968
+ printSuccess(`Releasable now \u2014 t2 job release ${job.jobId}`);
36858
36969
  } else {
36859
36970
  printInfo(`Job ${truncateAddress(job.jobId)}: ${prev ?? "new"} \u2192 ${job.state}`);
36860
36971
  }
36861
- printInboxRow(job);
36972
+ printInboxRow(job, bucket);
36862
36973
  printBlank();
36863
36974
  }
36864
36975
  }