@t2000/cli 10.30.0 → 10.30.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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,116 @@ 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) {
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";
36422
+ }
36391
36423
  if (job.state === "funded") {
36392
- return `t2 job spec ${job.jobId} \u2192 do the work \u2192 t2 job deliver ${job.jobId} <file>`;
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;
36393
36442
  }
36394
- if (job.state === "delivered") {
36395
- return `waiting on the buyer's review \u2014 anyone can \`t2 job release\` once it lapses`;
36443
+ return buckets;
36444
+ }
36445
+ var HYDRATE_MAX = 25;
36446
+ var HYDRATE_CONCURRENCY = 8;
36447
+ function mergeIndexedJobFromChain(row, chain) {
36448
+ return {
36449
+ ...row,
36450
+ jobId: chain.id ?? row.jobId,
36451
+ state: chain.state,
36452
+ buyer: chain.buyer ?? row.buyer,
36453
+ seller: chain.seller ?? row.seller,
36454
+ amountUsdc: chain.amountUsdc ?? row.amountUsdc,
36455
+ deliverByMs: chain.deliverByMs ?? row.deliverByMs,
36456
+ reviewWindowMs: chain.reviewWindowMs ?? row.reviewWindowMs,
36457
+ deliveryHash: chain.deliveryHash ?? row.deliveryHash,
36458
+ deliveredAtMs: chain.deliveredAtMs ?? row.deliveredAtMs ?? null,
36459
+ createdAtMs: chain.createdAtMs ?? row.createdAtMs,
36460
+ // Review clock prefers deliveredAtMs (reviewClosesMs anchors there when
36461
+ // set); for delivered rows keep updatedAtMs honest with the chain clock,
36462
+ // otherwise the index value stands — never invent a clock.
36463
+ updatedAtMs: chain.state === "delivered" && chain.deliveredAtMs != null ? chain.deliveredAtMs : row.updatedAtMs
36464
+ };
36465
+ }
36466
+ async function hydrateSellerJobsFromChain(rows, getJobById, opts) {
36467
+ const max = opts?.maxHydrate ?? HYDRATE_MAX;
36468
+ const out = [...rows];
36469
+ const targets = [];
36470
+ for (let i = 0; i < out.length && targets.length < max; i++) {
36471
+ if (!TERMINAL_STATES.has(out[i].state)) targets.push(i);
36472
+ }
36473
+ for (let at = 0; at < targets.length; at += HYDRATE_CONCURRENCY) {
36474
+ await Promise.all(
36475
+ targets.slice(at, at + HYDRATE_CONCURRENCY).map(async (i) => {
36476
+ try {
36477
+ out[i] = mergeIndexedJobFromChain(out[i], await getJobById(out[i].jobId));
36478
+ } catch {
36479
+ }
36480
+ })
36481
+ );
36396
36482
  }
36397
- return "";
36483
+ return out;
36398
36484
  }
36399
- function printInboxRow(job) {
36485
+ function inboxHint(job, bucket) {
36486
+ switch (bucket) {
36487
+ case "needsYou":
36488
+ return `t2 job spec ${job.jobId} \u2192 do the work \u2192 t2 job deliver ${job.jobId} <file>`;
36489
+ case "releasable":
36490
+ return `releasable now \u2014 t2 job release ${job.jobId}`;
36491
+ case "awaitingBuyer": {
36492
+ const closes = reviewClosesMs(job);
36493
+ 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`;
36494
+ }
36495
+ case "fundedLate":
36496
+ return `deliver deadline passed \u2014 the chain rejects late delivers; the buyer (or anyone) may t2 job refund ${job.jobId}`;
36497
+ default:
36498
+ return "";
36499
+ }
36500
+ }
36501
+ function printInboxRow(job, bucket) {
36400
36502
  const deadline = job.state === "funded" ? ` \xB7 deliver by ${new Date(job.deliverByMs).toISOString()}` : "";
36401
36503
  printLine(
36402
36504
  ` ${stateColor(job.state)} $${job.amountUsdc.toFixed(2)} USDC \xB7 from ${truncateAddress(job.buyer)}${deadline}`
36403
36505
  );
36404
36506
  printLine(` ${import_picocolors13.default.dim(job.jobId)}`);
36405
- const hint = inboxHint(job);
36507
+ const hint = inboxHint(job, bucket);
36406
36508
  if (hint) printLine(` ${import_picocolors13.default.dim("\u2192")} ${hint}`);
36407
36509
  }
36510
+ function deliverPreflightError(state, deliverByMs, nowMs) {
36511
+ if (state === "delivered") {
36512
+ 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.";
36513
+ }
36514
+ if (state === "released" || state === "rejected" || state === "refunded") {
36515
+ return `This job is ${state} \u2014 nothing can be delivered.`;
36516
+ }
36517
+ if (state === "funded" && nowMs > deliverByMs) {
36518
+ 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>\`.`;
36519
+ }
36520
+ return null;
36521
+ }
36408
36522
  async function sponsoredJobVerb(opts) {
36409
36523
  const agent = await withAgent({ keyPath: opts.keyPath });
36410
36524
  const address = agent.address();
@@ -36653,9 +36767,16 @@ no fund step; unclaimed openings refund fee-free):
36653
36767
  handleError(error);
36654
36768
  }
36655
36769
  });
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) => {
36770
+ 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
36771
  try {
36658
36772
  const base = opts.api ?? DEFAULT_API_BASE7;
36773
+ const existing = await getJob(getSuiClient(), jobId).catch(() => null);
36774
+ if (existing) {
36775
+ const preflightError = deliverPreflightError(existing.state, existing.deliverByMs, Date.now());
36776
+ if (preflightError) {
36777
+ throw new Error(preflightError.replaceAll("<jobId>", jobId));
36778
+ }
36779
+ }
36659
36780
  let deliveryHash;
36660
36781
  let uploaded = false;
36661
36782
  if (opts.hashOnly) {
@@ -36667,6 +36788,9 @@ no fund step; unclaimed openings refund fee-free):
36667
36788
  } else {
36668
36789
  ({ hash: deliveryHash, uploaded } = await resolveSpecUpload(base, proof));
36669
36790
  }
36791
+ if (!isJsonMode()) {
36792
+ printInfo("Delivery pins once. The buyer review window starts after this succeeds; you cannot replace the hash.");
36793
+ }
36670
36794
  const { digest } = await sponsoredJobVerb({
36671
36795
  base,
36672
36796
  keyPath: opts.key,
@@ -36674,11 +36798,12 @@ no fund step; unclaimed openings refund fee-free):
36674
36798
  params: { jobId, deliveryHash }
36675
36799
  });
36676
36800
  if (isJsonMode()) {
36677
- printJson({ jobId, deliveryHash, uploaded, digest });
36801
+ printJson({ jobId, deliveryHash, uploaded, digest, onceOnly: true });
36678
36802
  return;
36679
36803
  }
36680
36804
  printBlank();
36681
36805
  printSuccess("Delivery posted \u2014 the buyer's review window is now open.");
36806
+ printInfo("This delivery cannot be amended \u2014 further `t2 job deliver` calls on this job will fail.");
36682
36807
  printKeyValue("Delivery hash", deliveryHash);
36683
36808
  if (digest) printKeyValue("Tx", digest);
36684
36809
  if (uploaded) {
@@ -36821,44 +36946,72 @@ no fund step; unclaimed openings refund fee-free):
36821
36946
  if (opts.mine) {
36822
36947
  const base = opts.api ?? DEFAULT_API_BASE7;
36823
36948
  const seen = /* @__PURE__ */ new Map();
36824
- const jobs = await fetchSellerJobs(base, me);
36949
+ const mineClient = getSuiClient();
36950
+ const loadInbox = async () => hydrateSellerJobsFromChain(await fetchSellerJobs(base, me), (id) => getJob(mineClient, id));
36951
+ const jobs = await loadInbox();
36952
+ const inbox = summarizeSellerInbox(jobs, Date.now());
36825
36953
  if (isJsonMode()) {
36826
- printJson({ seller: me, jobs });
36954
+ printJson({
36955
+ seller: me,
36956
+ counts: inbox.counts,
36957
+ needsYou: inbox.needsYou,
36958
+ fundedLate: inbox.fundedLate,
36959
+ awaitingBuyer: inbox.awaitingBuyer,
36960
+ releasable: inbox.releasable,
36961
+ terminal: inbox.terminal,
36962
+ jobs
36963
+ });
36827
36964
  return;
36828
36965
  }
36829
- const open = jobs.filter((j) => !TERMINAL_STATES.has(j.state));
36830
36966
  printBlank();
36831
- printInfo(`Provider inbox for ${truncateAddress(me)} \u2014 ${jobs.length} job(s), ${open.length} open.`);
36967
+ printInfo(
36968
+ `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`
36969
+ );
36970
+ if (inbox.counts.fundedLate > 0) {
36971
+ printLine(import_picocolors13.default.dim(` ${inbox.counts.fundedLate} past deliver deadline (not deliverable \u2014 refund path is open to others)`));
36972
+ }
36832
36973
  printBlank();
36833
- for (const job of open) {
36834
- printInboxRow(job);
36835
- printBlank();
36974
+ for (const [bucket, rows] of [
36975
+ ["needsYou", inbox.needsYou],
36976
+ ["releasable", inbox.releasable],
36977
+ ["awaitingBuyer", inbox.awaitingBuyer],
36978
+ ["fundedLate", inbox.fundedLate]
36979
+ ]) {
36980
+ for (const job of rows) {
36981
+ printInboxRow(job, bucket);
36982
+ printBlank();
36983
+ }
36836
36984
  }
36837
- if (open.length === 0) {
36985
+ const openCount = jobs.length - inbox.counts.terminal;
36986
+ if (openCount === 0) {
36838
36987
  printInfo("No open jobs. New hires appear here the moment the escrow funds.");
36839
36988
  printBlank();
36840
36989
  }
36841
- for (const job of jobs) seen.set(job.jobId, job.state);
36990
+ for (const job of jobs) seen.set(job.jobId, bucketSellerJob(job, Date.now()));
36842
36991
  if (opts.once) return;
36843
36992
  for (; ; ) {
36844
36993
  await new Promise((r) => setTimeout(r, intervalMs));
36845
36994
  let latest;
36846
36995
  try {
36847
- latest = await fetchSellerJobs(base, me);
36996
+ latest = await loadInbox();
36848
36997
  } catch {
36849
36998
  continue;
36850
36999
  }
37000
+ const nowMs = Date.now();
36851
37001
  for (const job of latest) {
37002
+ const bucket = bucketSellerJob(job, nowMs);
36852
37003
  const prev = seen.get(job.jobId);
36853
- if (prev === job.state) continue;
36854
- seen.set(job.jobId, job.state);
37004
+ if (prev === bucket) continue;
37005
+ seen.set(job.jobId, bucket);
36855
37006
  printBlank();
36856
- if (prev === void 0 && job.state === "funded") {
37007
+ if (prev === void 0 && bucket === "needsYou") {
36857
37008
  printSuccess(`New job \u2014 $${job.amountUsdc.toFixed(2)} USDC escrowed for you.`);
37009
+ } else if (bucket === "releasable") {
37010
+ printSuccess(`Releasable now \u2014 t2 job release ${job.jobId}`);
36858
37011
  } else {
36859
37012
  printInfo(`Job ${truncateAddress(job.jobId)}: ${prev ?? "new"} \u2192 ${job.state}`);
36860
37013
  }
36861
- printInboxRow(job);
37014
+ printInboxRow(job, bucket);
36862
37015
  printBlank();
36863
37016
  }
36864
37017
  }