@t2000/cli 10.30.2 → 10.30.3

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
@@ -30670,7 +30670,6 @@ function jobActionsFor(job, caller, nowMs = Date.now()) {
30670
30670
  const actions = [];
30671
30671
  if (job.state === "funded") {
30672
30672
  if (isSeller && nowMs <= job.deliverByMs) actions.push("deliver");
30673
- if (isBuyer) actions.push("release");
30674
30673
  if (nowMs > job.deliverByMs) actions.push("refund");
30675
30674
  } else if (job.state === "delivered") {
30676
30675
  const windowClosesMs = (job.deliveredAtMs ?? 0) + job.reviewWindowMs;
@@ -36404,6 +36403,10 @@ function printJob(job, me) {
36404
36403
  if (job.deliveryHash) printKeyValue("Delivery hash", job.deliveryHash);
36405
36404
  printKeyValue("Reject split", `${job.rejectSplitBps / 100}% buyer / ${(1e4 - job.rejectSplitBps) / 100}% seller`);
36406
36405
  }
36406
+ async function fetchBuyerJobs(base, buyer) {
36407
+ const json = await fetchJson4(`${base}/jobs?buyer=${encodeURIComponent(buyer)}&limit=100`);
36408
+ return json.jobs ?? [];
36409
+ }
36407
36410
  async function fetchSellerJobs(base, seller) {
36408
36411
  const json = await fetchJson4(`${base}/jobs?seller=${encodeURIComponent(seller)}&limit=100`);
36409
36412
  return json.jobs ?? [];
@@ -36463,7 +36466,7 @@ function mergeIndexedJobFromChain(row, chain) {
36463
36466
  updatedAtMs: chain.state === "delivered" && chain.deliveredAtMs != null ? chain.deliveredAtMs : row.updatedAtMs
36464
36467
  };
36465
36468
  }
36466
- async function hydrateSellerJobsFromChain(rows, getJobById, opts) {
36469
+ async function hydrateJobsFromChain(rows, getJobById, opts) {
36467
36470
  const max = opts?.maxHydrate ?? HYDRATE_MAX;
36468
36471
  const out = [...rows];
36469
36472
  const targets = [];
@@ -36482,6 +36485,43 @@ async function hydrateSellerJobsFromChain(rows, getJobById, opts) {
36482
36485
  }
36483
36486
  return out;
36484
36487
  }
36488
+ var hydrateSellerJobsFromChain = hydrateJobsFromChain;
36489
+ function bucketBuyerJob(job, nowMs) {
36490
+ if (TERMINAL_STATES.has(job.state)) {
36491
+ return "terminal";
36492
+ }
36493
+ if (job.state === "delivered") {
36494
+ return "needsYou";
36495
+ }
36496
+ return nowMs > job.deliverByMs ? "refundable" : "waiting";
36497
+ }
36498
+ function summarizeBuyerInbox(jobs, nowMs) {
36499
+ const buckets = {
36500
+ counts: { total: jobs.length, needsYou: 0, refundable: 0, waiting: 0, terminal: 0 },
36501
+ needsYou: [],
36502
+ refundable: [],
36503
+ waiting: [],
36504
+ terminal: []
36505
+ };
36506
+ for (const job of jobs) {
36507
+ const bucket = bucketBuyerJob(job, nowMs);
36508
+ buckets[bucket].push(job);
36509
+ buckets.counts[bucket] += 1;
36510
+ }
36511
+ return buckets;
36512
+ }
36513
+ function buyerInboxHint(job, bucket) {
36514
+ switch (bucket) {
36515
+ case "needsYou":
36516
+ return `delivered \u2014 grade it: t2 job release ${job.jobId} \xB7 t2 job reject ${job.jobId}`;
36517
+ case "refundable":
36518
+ return `deadline passed, no delivery \u2014 t2 job refund ${job.jobId} (fee-free, anyone may crank it)`;
36519
+ case "waiting":
36520
+ return `waiting on the seller \u2014 t2 job watch ${job.jobId}`;
36521
+ default:
36522
+ return "";
36523
+ }
36524
+ }
36485
36525
  function inboxHint(job, bucket) {
36486
36526
  switch (bucket) {
36487
36527
  case "needsYou":
@@ -36499,12 +36539,17 @@ function inboxHint(job, bucket) {
36499
36539
  }
36500
36540
  }
36501
36541
  function printInboxRow(job, bucket) {
36542
+ printInboxRowWithHint(job, inboxHint(job, bucket), "from", job.buyer);
36543
+ }
36544
+ function printBuyerInboxRow(job, bucket) {
36545
+ printInboxRowWithHint(job, buyerInboxHint(job, bucket), "seller", job.seller);
36546
+ }
36547
+ function printInboxRowWithHint(job, hint, partyLabel, party) {
36502
36548
  const deadline = job.state === "funded" ? ` \xB7 deliver by ${new Date(job.deliverByMs).toISOString()}` : "";
36503
36549
  printLine(
36504
- ` ${stateColor(job.state)} $${job.amountUsdc.toFixed(2)} USDC \xB7 from ${truncateAddress(job.buyer)}${deadline}`
36550
+ ` ${stateColor(job.state)} $${job.amountUsdc.toFixed(2)} USDC \xB7 ${partyLabel} ${truncateAddress(party)}${deadline}`
36505
36551
  );
36506
36552
  printLine(` ${import_picocolors13.default.dim(job.jobId)}`);
36507
- const hint = inboxHint(job, bucket);
36508
36553
  if (hint) printLine(` ${import_picocolors13.default.dim("\u2192")} ${hint}`);
36509
36554
  }
36510
36555
  function deliverPreflightError(state, deliverByMs, nowMs) {
@@ -36549,8 +36594,11 @@ function registerJob(program3) {
36549
36594
  The escrow is a Sui object, not a company: funds lock inside the Job object at
36550
36595
  create; release/refund are pure functions of (state, clock, caller). A ghosting
36551
36596
  buyer can't strand a delivering seller (anyone may release after the review
36552
- window) and a no-show seller can never keep funds (anyone may refund after the
36553
- deadline). v1 caps jobs at ${MAX_JOB_USDC} USDC.
36597
+ window), and a no-show seller cannot keep funds AS LONG AS the buyer uses the
36598
+ escrow's protections \u2014 wait out the deadline and refund (anyone may crank it),
36599
+ or reject a bad delivery in-window. Releasing early without a delivery
36600
+ (--pay-without-delivery) voluntarily waives that protection. v1 caps jobs at
36601
+ ${MAX_JOB_USDC} USDC.
36554
36602
 
36555
36603
  Typical flow:
36556
36604
  buyer $ t2 job hire 5 0xSELLER --spec brief.md --deadline 24h
@@ -36558,7 +36606,8 @@ Typical flow:
36558
36606
  seller $ t2 job deliver 0xJOB report.md
36559
36607
  buyer $ t2 job release 0xJOB (or: t2 job reject 0xJOB)
36560
36608
  either $ t2 job watch 0xJOB
36561
- seller $ t2 job watch --mine (the provider inbox \u2014 all your jobs)
36609
+ seller $ t2 job watch --mine (the provider inbox \u2014 all your sells)
36610
+ buyer $ t2 job watch --buying (the buyer inbox \u2014 every job you funded)
36562
36611
 
36563
36612
  Hiring a LISTING (t2 ACP) \u2014 price + terms come from the listing:
36564
36613
  buyer $ t2 services "market report"
@@ -36817,12 +36866,45 @@ no fund step; unclaimed openings refund fee-free):
36817
36866
  handleError(error);
36818
36867
  }
36819
36868
  });
36869
+ group.command("release").argument("<jobId>", "The Job object id (0x\u2026)").description("Accept delivery \u2014 funds go to the seller (buyer; or anyone once the review window lapses). On a FUNDED job with no delivery this pays the full escrow to the seller, terminally \u2014 refused unless --pay-without-delivery.").option("--pay-without-delivery", "DELIBERATE goodwill: release the full escrow on a funded job with NO delivery (off-band delivery only \u2014 the seller keeps everything, no refund path)").option("--key <path>", "Custom wallet path (default ~/.t2000/wallet.key)").option("--api <url>", `API base URL (default ${DEFAULT_API_BASE7})`).action(async (jobId, opts) => {
36870
+ try {
36871
+ const existing = await getJob(getSuiClient(), jobId).catch(() => null);
36872
+ const undelivered = existing?.state === "funded" && !existing.deliveryHash;
36873
+ if (undelivered && !opts.payWithoutDelivery) {
36874
+ throw new Error(
36875
+ "This job is FUNDED with no delivery \u2014 releasing now pays the full escrow to the seller with no recovery path. Wait for the delivery, refund after the deadline (t2 job refund), or \u2014 only if the work arrived off-band \u2014 re-run with --pay-without-delivery."
36876
+ );
36877
+ }
36878
+ if (undelivered && opts.payWithoutDelivery) {
36879
+ printWarning(
36880
+ "Paying WITHOUT an on-chain delivery: the full escrow goes to the seller, terminally. No refund path exists after this."
36881
+ );
36882
+ }
36883
+ const { digest } = await sponsoredJobVerb({
36884
+ base: opts.api ?? DEFAULT_API_BASE7,
36885
+ keyPath: opts.key,
36886
+ action: "release",
36887
+ params: {
36888
+ jobId,
36889
+ ...opts.payWithoutDelivery ? { payWithoutDelivery: true } : {}
36890
+ }
36891
+ });
36892
+ if (isJsonMode()) {
36893
+ printJson({ jobId, action: "release", digest, ...undelivered ? { paidWithoutDelivery: true } : {} });
36894
+ return;
36895
+ }
36896
+ printBlank();
36897
+ printSuccess("Funds released to the seller.");
36898
+ if (digest) printKeyValue("Tx", digest);
36899
+ if (existing?.deliveryHash) {
36900
+ printInfo(`Rate the work: t2 job review ${jobId} --stars <1-5>`);
36901
+ }
36902
+ printBlank();
36903
+ } catch (error) {
36904
+ handleError(error);
36905
+ }
36906
+ });
36820
36907
  for (const [verb, description, note] of [
36821
- [
36822
- "release",
36823
- "Accept delivery \u2014 funds go to the seller (buyer; or anyone once the review window lapses)",
36824
- "Funds released to the seller."
36825
- ],
36826
36908
  [
36827
36909
  "reject",
36828
36910
  "Reject a delivery within the review window \u2014 funds split per the create terms (buyer)",
@@ -36854,9 +36936,6 @@ no fund step; unclaimed openings refund fee-free):
36854
36936
  printBlank();
36855
36937
  printSuccess(note);
36856
36938
  if (digest) printKeyValue("Tx", digest);
36857
- if (verb === "release") {
36858
- printInfo(`Rate the work (builds the seller's on-chain-backed reputation): t2 job review ${jobId} --stars 5`);
36859
- }
36860
36939
  printBlank();
36861
36940
  } catch (error) {
36862
36941
  handleError(error);
@@ -36869,6 +36948,12 @@ no fund step; unclaimed openings refund fee-free):
36869
36948
  if (!Number.isInteger(stars) || stars < 1 || stars > 5) {
36870
36949
  throw new Error(`--stars must be an integer 1\u20135 (got "${opts.stars}").`);
36871
36950
  }
36951
+ const reviewedJob = await getJob(getSuiClient(), jobId).catch(() => null);
36952
+ if (reviewedJob && !reviewedJob.deliveryHash) {
36953
+ throw new Error(
36954
+ "This job has no on-chain delivery \u2014 there is no work to rate. Reviews attach only to jobs the seller actually delivered."
36955
+ );
36956
+ }
36872
36957
  const base = opts.api ?? DEFAULT_API_BASE7;
36873
36958
  const agent = await withAgent({ keyPath: opts.key });
36874
36959
  const address = agent.address();
@@ -36938,11 +37023,84 @@ no fund step; unclaimed openings refund fee-free):
36938
37023
  handleError(error);
36939
37024
  }
36940
37025
  });
36941
- group.command("watch").argument("[jobId]", "The Job object id (0x\u2026) \u2014 omit with --mine").description("Poll a job \u2014 or, with --mine, the provider inbox (every job selling to you)").option("--mine", "Watch ALL jobs where this wallet is the seller (the provider inbox)").option("--interval <seconds>", "Poll interval", "15").option("--once", "Print the current state and exit").option("--key <path>", "Custom wallet path (default ~/.t2000/wallet.key)").option("--api <url>", `API base URL (default ${DEFAULT_API_BASE7})`).action(async (jobId, opts) => {
37026
+ group.command("watch").argument("[jobId]", "The Job object id (0x\u2026) \u2014 omit with --mine or --buying").description("Poll a job \u2014 or an inbox: --mine (every job selling to you) / --buying (every job you funded)").option("--mine", "Watch ALL jobs where this wallet is the seller (the provider inbox)").option("--buying", "Watch ALL jobs where this wallet is the BUYER \u2014 recovers job ids the hire line printed once (S.1016)").option("--interval <seconds>", "Poll interval", "15").option("--once", "Print the current state and exit").option("--key <path>", "Custom wallet path (default ~/.t2000/wallet.key)").option("--api <url>", `API base URL (default ${DEFAULT_API_BASE7})`).action(async (jobId, opts) => {
36942
37027
  try {
37028
+ if (opts.mine && opts.buying) {
37029
+ throw new Error("Pick one seat: --mine (selling) or --buying (funding) \u2014 not both.");
37030
+ }
37031
+ if (jobId && (opts.mine || opts.buying)) {
37032
+ throw new Error("Pick one mode: a job id OR an inbox flag (--mine / --buying).");
37033
+ }
36943
37034
  const agent = await withAgent({ keyPath: opts.key });
36944
37035
  const me = agent.address();
36945
37036
  const intervalMs = Math.max(5, Number.parseInt(opts.interval, 10) || 15) * 1e3;
37037
+ if (opts.buying) {
37038
+ const base = opts.api ?? DEFAULT_API_BASE7;
37039
+ const seen = /* @__PURE__ */ new Map();
37040
+ const buyClient = getSuiClient();
37041
+ const loadInbox = async () => hydrateJobsFromChain(await fetchBuyerJobs(base, me), (id) => getJob(buyClient, id));
37042
+ const jobs = await loadInbox();
37043
+ const inbox = summarizeBuyerInbox(jobs, Date.now());
37044
+ if (isJsonMode()) {
37045
+ printJson({
37046
+ buyer: me,
37047
+ counts: inbox.counts,
37048
+ needsYou: inbox.needsYou,
37049
+ refundable: inbox.refundable,
37050
+ waiting: inbox.waiting,
37051
+ terminal: inbox.terminal,
37052
+ jobs
37053
+ });
37054
+ return;
37055
+ }
37056
+ printBlank();
37057
+ printInfo(
37058
+ `Buyer inbox for ${truncateAddress(me)} \u2014 ${jobs.length} job(s) \xB7 ${inbox.counts.needsYou} need you \xB7 ${inbox.counts.refundable} refundable \xB7 ${inbox.counts.waiting} waiting`
37059
+ );
37060
+ printBlank();
37061
+ for (const [bucket, rows] of [
37062
+ ["needsYou", inbox.needsYou],
37063
+ ["refundable", inbox.refundable],
37064
+ ["waiting", inbox.waiting]
37065
+ ]) {
37066
+ for (const job of rows) {
37067
+ printBuyerInboxRow(job, bucket);
37068
+ printBlank();
37069
+ }
37070
+ }
37071
+ if (jobs.length - inbox.counts.terminal === 0) {
37072
+ printInfo("No open buys. Hire a listing or post an Open job and it lands here.");
37073
+ printBlank();
37074
+ }
37075
+ for (const job of jobs) seen.set(job.jobId, bucketBuyerJob(job, Date.now()));
37076
+ if (opts.once) return;
37077
+ for (; ; ) {
37078
+ await new Promise((r) => setTimeout(r, intervalMs));
37079
+ let latest;
37080
+ try {
37081
+ latest = await loadInbox();
37082
+ } catch {
37083
+ continue;
37084
+ }
37085
+ const nowMs = Date.now();
37086
+ for (const job of latest) {
37087
+ const bucket = bucketBuyerJob(job, nowMs);
37088
+ const prev = seen.get(job.jobId);
37089
+ if (prev === bucket) continue;
37090
+ seen.set(job.jobId, bucket);
37091
+ printBlank();
37092
+ if (bucket === "needsYou") {
37093
+ printSuccess(`Delivered \u2014 grade it: t2 job release ${job.jobId} \xB7 t2 job reject ${job.jobId}`);
37094
+ } else if (bucket === "refundable") {
37095
+ printSuccess(`Refundable \u2014 t2 job refund ${job.jobId} (deadline passed, no delivery)`);
37096
+ } else {
37097
+ printInfo(`Job ${truncateAddress(job.jobId)}: ${prev ?? "new"} \u2192 ${job.state}`);
37098
+ }
37099
+ printBuyerInboxRow(job, bucket);
37100
+ printBlank();
37101
+ }
37102
+ }
37103
+ }
36946
37104
  if (opts.mine) {
36947
37105
  const base = opts.api ?? DEFAULT_API_BASE7;
36948
37106
  const seen = /* @__PURE__ */ new Map();
@@ -37017,7 +37175,7 @@ no fund step; unclaimed openings refund fee-free):
37017
37175
  }
37018
37176
  }
37019
37177
  if (!jobId) {
37020
- printError("Provide a job id \u2014 or use --mine for the provider inbox.");
37178
+ printError("Provide a job id \u2014 or an inbox: --mine (selling) / --buying (funding).");
37021
37179
  process.exitCode = 1;
37022
37180
  return;
37023
37181
  }
@@ -37127,10 +37285,13 @@ function printService(o) {
37127
37285
  `$${o.priceUsdc.toFixed(2)} USDC ${import_picocolors14.default.dim(`\xB7 seller receives ${settlementSplit(o.priceUsdc).payout}`)}`
37128
37286
  );
37129
37287
  printKeyValue("Delivery", `within ${formatSla(o.slaMinutes)}`);
37130
- printKeyValue(
37131
- "Seller",
37132
- `${o.agentName ?? "unnamed"} ${import_picocolors14.default.dim(truncateAddress(o.agent))}`
37133
- );
37288
+ {
37289
+ const id = o.agentNumericId != null ? ` #${o.agentNumericId}` : "";
37290
+ printKeyValue(
37291
+ "Seller",
37292
+ `${o.agentName ?? "unnamed"}${import_picocolors14.default.bold(id)} ${import_picocolors14.default.dim(truncateAddress(o.agent))}`
37293
+ );
37294
+ }
37134
37295
  printKeyValue("You get", o.deliverable);
37135
37296
  if (o.requirements != null) {
37136
37297
  printKeyValue(
@@ -37280,15 +37441,55 @@ Examples:
37280
37441
  }
37281
37442
  });
37282
37443
  }
37444
+ async function resolveServicesQuery(base, query) {
37445
+ const q = query?.trim();
37446
+ if (!q) {
37447
+ return { url: `${base}/services`, scope: { kind: "all" } };
37448
+ }
37449
+ if (q.startsWith("0x")) {
37450
+ try {
37451
+ const agent = validateAddress(q);
37452
+ return {
37453
+ url: `${base}/services?agent=${encodeURIComponent(agent)}`,
37454
+ scope: { kind: "agent", agent }
37455
+ };
37456
+ } catch {
37457
+ }
37458
+ } else if (looksLikeAgentRefValue(q)) {
37459
+ const ref = await resolveAgentRef(base, q);
37460
+ return {
37461
+ url: `${base}/services?agent=${encodeURIComponent(ref.address)}`,
37462
+ scope: { kind: "agent", agent: ref.address, numericId: ref.numericId }
37463
+ };
37464
+ }
37465
+ return {
37466
+ url: `${base}/services?q=${encodeURIComponent(q)}`,
37467
+ scope: { kind: "search", q }
37468
+ };
37469
+ }
37470
+ function scopeLabel(scope) {
37471
+ if (scope.kind !== "agent") {
37472
+ return "";
37473
+ }
37474
+ return scope.numericId != null ? `#${scope.numericId} (${truncateAddress(scope.agent)})` : truncateAddress(scope.agent);
37475
+ }
37283
37476
  function registerDiscovery(command, opts) {
37284
- command.argument("[query]", "What you need \u2014 free-text search (empty = everything)").option("--api <url>", `API base URL (default ${DEFAULT_API_BASE8})`).action(async (query, cmdOpts) => {
37477
+ command.argument("[query]", "What you need \u2014 free text, or a SELLER scope: 0x\u2026 address, #id, or @handle (empty = everything)").option("--api <url>", `API base URL (default ${DEFAULT_API_BASE8})`).action(async (query, cmdOpts) => {
37285
37478
  try {
37286
37479
  const base = cmdOpts.api ?? DEFAULT_API_BASE8;
37287
- const params = query ? `?q=${encodeURIComponent(query)}` : "";
37288
- const json = await fetchJson4(`${base}/services${params}`);
37289
- const rows = json.services ?? [];
37480
+ const { url, scope } = await resolveServicesQuery(base, query);
37481
+ const json = await fetchJson4(url);
37482
+ const all = json.services ?? [];
37483
+ const rows = scope.kind === "agent" ? all.filter((o) => !o.retired) : all;
37484
+ const retiredHidden = all.length - rows.length;
37290
37485
  if (isJsonMode()) {
37291
- printJson({ query: query ?? null, total: json.total ?? rows.length, services: rows });
37486
+ printJson({
37487
+ query: query ?? null,
37488
+ scope,
37489
+ total: scope.kind === "agent" ? rows.length : json.total ?? rows.length,
37490
+ ...retiredHidden > 0 ? { retiredHidden } : {},
37491
+ services: rows
37492
+ });
37292
37493
  return;
37293
37494
  }
37294
37495
  printBlank();
@@ -37297,7 +37498,9 @@ function registerDiscovery(command, opts) {
37297
37498
  printBlank();
37298
37499
  }
37299
37500
  if (rows.length === 0) {
37300
- printInfo(query ? `No services match "${query}".` : "No services listed yet.");
37501
+ printInfo(
37502
+ scope.kind === "agent" ? `No active services for ${scopeLabel(scope)}.` : scope.kind === "search" ? `No services match "${scope.q}".` : "No services listed yet."
37503
+ );
37301
37504
  printBlank();
37302
37505
  return;
37303
37506
  }
@@ -37305,6 +37508,17 @@ function registerDiscovery(command, opts) {
37305
37508
  printService(o);
37306
37509
  printBlank();
37307
37510
  }
37511
+ if (retiredHidden > 0) {
37512
+ printInfo(import_picocolors14.default.dim(`${retiredHidden} retired omitted \u2014 t2 service list ${scope.kind === "agent" ? scope.agent : ""}`));
37513
+ printBlank();
37514
+ }
37515
+ if (scope.kind === "search") {
37516
+ const sellers = new Set(rows.map((o) => o.agent)).size;
37517
+ if (sellers >= 2) {
37518
+ printInfo(import_picocolors14.default.dim(`Results span ${sellers} sellers \u2014 scope to one with t2 services 0x\u2026 or #id.`));
37519
+ printBlank();
37520
+ }
37521
+ }
37308
37522
  } catch (error) {
37309
37523
  handleError(error);
37310
37524
  }