@wji.ai/cli 0.9.2 → 0.9.5

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
@@ -6106,6 +6106,7 @@ function printChanges2(changes, warnings = []) {
6106
6106
  }
6107
6107
 
6108
6108
  // src/commands/backreel.ts
6109
+ import { stat as stat6 } from "fs/promises";
6109
6110
  import { Command as Command26 } from "commander";
6110
6111
 
6111
6112
  // src/lib/backreel.ts
@@ -6382,11 +6383,18 @@ async function resolveSource(client, opts, postId, file) {
6382
6383
  const asset = await client.upload("/assets/v2/upload", file, {});
6383
6384
  return { video: asset.id };
6384
6385
  }
6386
+ return { vodVid: await uploadVideoToVod(client, file, { postId, title: opts.title }) };
6387
+ }
6388
+ async function uploadVideoToVod(client, file, opts = {}) {
6389
+ const ext = extname2(file).toLowerCase();
6390
+ if (!VIDEO_EXTS.includes(ext)) throw new Error(`${basename6(file)}: videos must be ${VIDEO_EXTS.join(" / ")}`);
6391
+ const size = (await stat5(file)).size;
6392
+ if (size > VIDEO_MAX_BYTES) throw new Error(`${basename6(file)} is over 500 MB`);
6385
6393
  const token = await client.post("/backreel/vod/upload-token");
6386
6394
  const vid = await uploadToVod(token, file, (step) => info(color.dim(`VOD: ${step}\u2026`)));
6387
- const claimed = await client.post("/backreel/vod/claim", { vid, postId, title: opts.title?.slice(0, 128) });
6395
+ const claimed = await client.post("/backreel/vod/claim", { vid, postId: opts.postId ?? void 0, title: opts.title?.slice(0, 128) });
6388
6396
  info(color.dim(`VOD ${vid}: ${claimed?.status ?? "claimed"}`));
6389
- return { vodVid: vid };
6397
+ return vid;
6390
6398
  }
6391
6399
  function printEpisodes(post) {
6392
6400
  const rows = [...post.episodes || []].sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
@@ -6955,7 +6963,11 @@ function enquiryCommand(globals2) {
6955
6963
 
6956
6964
  // src/commands/backreel-fund.ts
6957
6965
  import { Command as Command22 } from "commander";
6958
- var CURRENCIES = ["hkd", "usd", "twd", "sgd", "eur", "gbp"];
6966
+ var CASH_CURRENCIES = ["hkd", "usd", "twd", "sgd", "eur", "gbp"];
6967
+ var CURRENCIES = ["credits", ...CASH_CURRENCIES];
6968
+ var MODES = ["credits", "cash", "both"];
6969
+ var hasCredits = (mode) => mode === "credits" || mode === "both";
6970
+ var fmt = (n) => Math.round(Number(n) || 0).toLocaleString("en-US");
6959
6971
  var MAX_TIERS = 10;
6960
6972
  var num = (value, flag, min = 0) => {
6961
6973
  const n = Number(value);
@@ -6975,6 +6987,10 @@ function itemIndex(n, count, what) {
6975
6987
  }
6976
6988
  function campaignDto(c) {
6977
6989
  return {
6990
+ // fundingMode / currency / creditGoal: the server keeps the stored value when a field is left out
6991
+ fundingMode: c?.fundingMode ?? void 0,
6992
+ currency: c?.fundingMode && c.fundingMode !== "credits" ? c?.currency ?? void 0 : void 0,
6993
+ creditGoal: c?.creditGoal ?? void 0,
6978
6994
  goalAmount: c?.goalAmount ?? 0,
6979
6995
  offlineRaisedAmount: c?.offlineRaisedAmount ?? 0,
6980
6996
  deadline: c?.deadline ?? null,
@@ -7004,22 +7020,40 @@ var put = (client, id, body) => client.put(`${POSTS}/list/${id}`, body);
7004
7020
  function printFund(post) {
7005
7021
  const c = post.campaign;
7006
7022
  const d = post.donation || {};
7007
- const cur = (d.currency || c?.currency || "hkd").toUpperCase();
7023
+ const mode = c?.fundingMode || "cash";
7024
+ const cur = (c?.currency || d.currency !== "credits" && d.currency || "hkd").toUpperCase();
7008
7025
  if (c) {
7009
- const pct = c.goalAmount ? ` (${Math.round((c.raisedAmount || 0) / c.goalAmount * 100)}%)` : "";
7010
- detail([
7011
- ["goal", `${c.goalAmount} ${cur}`],
7012
- ["raised", `${c.raisedAmount} ${cur}${pct} = offline ${c.offlineRaisedAmount ?? 0} + online ${(c.raisedAmount || 0) - (c.offlineRaisedAmount || 0)}`],
7013
- ["supporters", c.supporterCount],
7014
- ["deadline", c.deadline || color.dim("none")]
7015
- ]);
7026
+ const rows = [["funding", mode]];
7027
+ if (hasCredits(mode)) {
7028
+ const pct = c.creditGoal ? ` (${Math.round((c.creditsRaised || 0) / c.creditGoal * 100)}%)` : "";
7029
+ rows.push(
7030
+ ["credits goal", `${fmt(c.creditGoal)} credits`],
7031
+ ["credits raised", `${fmt(c.creditsRaised)} credits${pct} from ${c.creditBackerCount ?? 0} backer(s)`],
7032
+ ["outcome", c.outcome === "funded" ? color.green("funded") : c.outcome === "failed" ? color.red("failed \u2014 refunded") : "raising"]
7033
+ );
7034
+ }
7035
+ if (mode !== "credits") {
7036
+ const pct = c.goalAmount ? ` (${Math.round((c.raisedAmount || 0) / c.goalAmount * 100)}%)` : "";
7037
+ rows.push(
7038
+ ["cash goal", `${c.goalAmount} ${cur}`],
7039
+ ["cash raised", `${c.raisedAmount} ${cur}${pct} = offline ${c.offlineRaisedAmount ?? 0} + online ${(c.raisedAmount || 0) - (c.offlineRaisedAmount || 0)}`]
7040
+ );
7041
+ }
7042
+ if (c.production) {
7043
+ rows.push([
7044
+ "production",
7045
+ `${c.production.status} \u2014 ${c.production.milestonesApproved}/${c.production.milestonesTotal} milestones (contract: \`wji br contract get ${c.production.contractJobId}\`)`
7046
+ ]);
7047
+ }
7048
+ rows.push(["supporters", c.supporterCount], ["deadline", c.deadline || color.dim("none")]);
7049
+ detail(rows);
7016
7050
  process.stdout.write("\n");
7017
7051
  } else {
7018
7052
  info(color.dim(`${post.kind} post \u2014 no campaign (goal / offline supporters are crowdfunding-only); donations still work`));
7019
7053
  }
7020
7054
  detail([
7021
7055
  ["donations", d.enabled ? color.green("on") : "off"],
7022
- ["currency", (d.currency || "hkd").toUpperCase()],
7056
+ ["currency", d.currency === "credits" ? "WJI credits" : (d.currency || "hkd").toUpperCase()],
7023
7057
  ["minimum", d.minAmount ?? 0]
7024
7058
  ]);
7025
7059
  if ((d.tiers || []).length) {
@@ -7048,13 +7082,17 @@ function fundCommand(globals2) {
7048
7082
  const post = await loadPost(client, postId);
7049
7083
  render({ campaign: post.campaign, donation: post.donation, kind: post.kind }, () => printFund(post));
7050
7084
  });
7051
- fund.command("set <postId>").description("Campaign goal / offline total / deadline and donation switches; only what you pass changes").option("--goal <amount>", "campaign goal (\u22651 to publish a crowdfunding post)").option("--offline-raised <amount>", "money raised outside Backreel, added to the progress bar").option("--deadline <date>", 'ISO date (e.g. 2026-12-31); "none" clears \u2014 donations stop after it').option("--donations <on|off>", "accept online donations (Stripe)").option("--currency <code>", `${CURRENCIES.join(" | ")} \u2014 locked once a donation exists`).option("--min <amount>", "minimum donation").action(async (postId, opts) => {
7085
+ fund.command("set <postId>").description("Funding type, goals, deadline and donation switches; only what you pass changes").option("--mode <mode>", `${MODES.join(" | ")} \u2014 credits: any creator, all-or-nothing at the deadline; cash / both need Cash fundraiser verification`).option("--credit-goal <credits>", "credits goal (credits / both campaigns)").option("--goal <amount>", "cash goal (cash / both campaigns)").option("--cash-currency <code>", `${CASH_CURRENCIES.join(" | ")} \u2014 currency of the cash goal`).option("--offline-raised <amount>", "money raised outside Backreel, added to the progress bar").option("--deadline <date>", 'ISO date (e.g. 2026-12-31); "none" clears \u2014 donations stop after it').option("--donations <on|off>", "accept support (credits pledges, or legacy Stripe donations)").option("--currency <code>", `${CURRENCIES.join(" | ")} \u2014 credits = WJI credits; can't leave credits once someone pledged`).option("--min <amount>", "minimum donation").action(async (postId, opts) => {
7052
7086
  const client = backreelClient(globals2);
7053
7087
  const post = await loadPost(client, postId);
7054
7088
  const body = {};
7055
- if (opts.goal !== void 0 || opts.offlineRaised !== void 0 || opts.deadline !== void 0) {
7089
+ const campaignFlags = [opts.goal, opts.offlineRaised, opts.deadline, opts.mode, opts.creditGoal, opts.cashCurrency];
7090
+ if (campaignFlags.some((v) => v !== void 0)) {
7056
7091
  requireCampaign(post);
7057
7092
  const campaign = campaignDto(post.campaign);
7093
+ if (opts.mode !== void 0) campaign.fundingMode = oneOf2(String(opts.mode).toLowerCase(), MODES, "--mode");
7094
+ if (opts.creditGoal !== void 0) campaign.creditGoal = Math.round(num(opts.creditGoal, "--credit-goal", 1));
7095
+ if (opts.cashCurrency !== void 0) campaign.currency = oneOf2(String(opts.cashCurrency).toLowerCase(), CASH_CURRENCIES, "--cash-currency");
7058
7096
  if (opts.goal !== void 0) campaign.goalAmount = num(opts.goal, "--goal");
7059
7097
  if (opts.offlineRaised !== void 0) campaign.offlineRaisedAmount = num(opts.offlineRaised, "--offline-raised");
7060
7098
  if (opts.deadline !== void 0) {
@@ -7066,9 +7104,12 @@ function fundCommand(globals2) {
7066
7104
  }
7067
7105
  }
7068
7106
  body.campaign = campaign;
7107
+ if (hasCredits(campaign.fundingMode) && opts.currency === void 0 && opts.donations === void 0) {
7108
+ body.donation = { ...donationDto(post.donation), enabled: true, currency: "credits" };
7109
+ }
7069
7110
  }
7070
7111
  if (opts.donations !== void 0 || opts.currency !== void 0 || opts.min !== void 0) {
7071
- const donation = donationDto(post.donation);
7112
+ const donation = body.donation || donationDto(post.donation);
7072
7113
  if (opts.donations !== void 0) donation.enabled = oneOf2(String(opts.donations).toLowerCase(), ["on", "off"], "--donations") === "on";
7073
7114
  if (opts.currency !== void 0) donation.currency = oneOf2(String(opts.currency).toLowerCase(), CURRENCIES, "--currency");
7074
7115
  if (opts.min !== void 0) donation.minAmount = Math.round(num(opts.min, "--min"));
@@ -7185,6 +7226,46 @@ function fundCommand(globals2) {
7185
7226
  if ((r.payouts || []).length) info(color.dim(`${r.payouts.length} payout(s) \u2014 payouts and refunds are handled by Backreel staff`));
7186
7227
  });
7187
7228
  });
7229
+ fund.command("pledges <postId>").description("Credits backers of your post (anonymous backers stay hidden)").option("--limit <n>", "how many (\u2264200)", "100").action(async (postId, opts) => {
7230
+ const client = backreelClient(globals2);
7231
+ const r = await client.get(`/backreel/pledge/post/${postId}/manage`, { limit: Number(opts.limit) });
7232
+ render(r, () => {
7233
+ detail([
7234
+ ["credits", fmt(r.stats?.credits ?? 0)],
7235
+ ["backers", r.stats?.backerCount ?? 0],
7236
+ ["pledges", r.stats?.pledgeCount ?? 0]
7237
+ ]);
7238
+ process.stdout.write("\n");
7239
+ table(r.data || [], [
7240
+ { header: "when", value: (p) => relativeTime(p.createdAt) },
7241
+ { header: "backer", value: (p) => p.isAnonymous ? color.dim("anonymous") : truncate(p.backer?.name, 20) },
7242
+ { header: "credits", value: (p) => fmt(p.credits) },
7243
+ { header: "tier", value: (p) => truncate(p.tier?.title, 16) },
7244
+ { header: "status", value: (p) => p.status },
7245
+ { header: "message", value: (p) => truncate(p.message, 30) }
7246
+ ]);
7247
+ });
7248
+ });
7249
+ fund.command("back <postId> <credits>").description("Back someone's post with your WJI credits \u2014 campaigns hold them until the deadline (refunded if the goal is missed); showcase works send them to the creator").option("--tier <n>", "pick tier n (see `fund show`)").option("-m, --message <text>", "a note to the creator (\u2264500)").option("--anonymous", "hide your name on the supporter wall").option("-y, --yes", "skip confirmation").action(async (postId, credits, opts) => {
7250
+ const client = backreelClient(globals2);
7251
+ const post = await client.get(`${POSTS}/list/${postId}`);
7252
+ if (post.donation?.currency !== "credits" || !post.donation?.enabled) {
7253
+ throw new Error(`"${post.title}" doesn't take credits support`);
7254
+ }
7255
+ const tiers = post.donation.tiers || [];
7256
+ const tier2 = opts.tier ? tiers[itemIndex(opts.tier, tiers.length, "Tier")] : null;
7257
+ const amount = Math.round(num(credits, "<credits>", 1));
7258
+ const where = post.kind === "crowdfunding" ? "held by the campaign until its deadline" : "sent to the creator";
7259
+ if (!await confirm(`Back "${post.title}" with ${fmt(amount)} credits (${where})?`, opts.yes)) return info("Aborted");
7260
+ const pledge = await client.post("/backreel/pledge", {
7261
+ post: postId,
7262
+ credits: amount,
7263
+ tierId: tier2?.id,
7264
+ message: opts.message,
7265
+ isAnonymous: !!opts.anonymous
7266
+ });
7267
+ render(pledge, () => success(`Backed "${post.title}" with ${fmt(pledge.credits)} credits \u2014 ${pledge.status}`));
7268
+ });
7188
7269
  return fund;
7189
7270
  }
7190
7271
 
@@ -7211,12 +7292,12 @@ function parseDay(value, flag) {
7211
7292
  return d.toISOString();
7212
7293
  }
7213
7294
  var usd = (n) => n == null ? "" : `US$${Number(n).toLocaleString("en-US", { maximumFractionDigits: 2 })}`;
7214
- var fmt = (n) => n == null ? "" : Math.round(Number(n)).toLocaleString("en-US");
7295
+ var fmt2 = (n) => n == null ? "" : Math.round(Number(n)).toLocaleString("en-US");
7215
7296
  function budgetText(j) {
7216
7297
  if (j.budgetNegotiable && j.budgetMin == null && j.budgetMax == null) return "negotiable";
7217
- const cur = j.currency === "usd" || !j.currency ? usd : (n) => `${String(j.currency).toUpperCase()} ${fmt(n)}`;
7298
+ const cur = j.currency === "usd" || !j.currency ? usd : (n) => `${String(j.currency).toUpperCase()} ${fmt2(n)}`;
7218
7299
  const cash = j.budgetMin != null && j.budgetMax != null && j.budgetMin !== j.budgetMax ? `${cur(j.budgetMin)}\u2013${cur(j.budgetMax)}` : j.budgetMax != null || j.budgetMin != null ? cur(j.budgetMax ?? j.budgetMin) : "";
7219
- return [cash, j.budgetCredits ? `${fmt(j.budgetCredits)} credits` : ""].filter(Boolean).join(" + ");
7300
+ return [cash, j.budgetCredits ? `${fmt2(j.budgetCredits)} credits` : ""].filter(Boolean).join(" + ");
7220
7301
  }
7221
7302
  function jobOptions2(cmd) {
7222
7303
  return cmd.option("--category <c>", `one of ${CATEGORIES2.join(", ")}`).option("--brief <text|@file>", "what you need (max 5000)").option("--deliverables <text|@file>", "what gets delivered (max 2000)").option("--budget <min[-max]>", 'cash budget in --currency, e.g. 8000 or 5000-10000; "none" clears').option("--currency <code>", "currency of --budget (converted to USD by the server)", "USD").option("--negotiable", "budget is negotiable").option("--credits <n>", "WJI credits budget on top of cash; 0 clears").option("--duration <len>", "running time: 90, 3m, 1h30m, 2:30").option("--resolution <r>", `delivery resolution: ${RESOLUTIONS.join(", ")}`).option("--upscale", "upscaling to the resolution is acceptable").option("--no-upscale", "must be generated at the resolution").option("--model <name...>", "required WJI models (max 10)").option("--deadline <date>", "bid deadline, YYYY-MM-DD (\u2264 90 days)").option("--delivery <date>", 'delivery date, YYYY-MM-DD; "none" clears').option("--location <text>", "location, if any").option("--link <url...>", "reference links (max 5)").option("--verified-only", "only verified creators can bid").option("--open-to-all", "anyone can bid (undo --verified-only)").option("--org <orgId>", 'post as one of your organizations; "none" = personal');
@@ -7294,8 +7375,8 @@ function printBids(rows) {
7294
7375
  { header: "job", value: (b) => truncate(b.job?.title, 24) },
7295
7376
  { header: "bidder", value: (b) => truncate(b.organization?.name || b.bidder?.name, 18) },
7296
7377
  { header: "status", value: (b) => b.status },
7297
- { header: "price", value: (b) => b.price == null ? "" : b.currency === "usd" || !b.currency ? usd(b.price) : `${b.currency.toUpperCase()} ${fmt(b.price)}` },
7298
- { header: "credits", value: (b) => fmt(b.creditsPrice) },
7378
+ { header: "price", value: (b) => b.price == null ? "" : b.currency === "usd" || !b.currency ? usd(b.price) : `${b.currency.toUpperCase()} ${fmt2(b.price)}` },
7379
+ { header: "credits", value: (b) => fmt2(b.creditsPrice) },
7299
7380
  { header: "days", value: (b) => b.timelineDays ?? "" },
7300
7381
  { header: "pitch", value: (b) => truncate(b.pitch, 30) }
7301
7382
  ]);
@@ -7463,7 +7544,7 @@ var KIND_NAMES = {
7463
7544
  draft: "Draft"
7464
7545
  };
7465
7546
  var usd2 = (n) => n == null ? "" : `US$${Number(n).toLocaleString("en-US", { maximumFractionDigits: 2 })}`;
7466
- var fmt2 = (n) => n == null ? "" : Math.round(Number(n)).toLocaleString("en-US");
7547
+ var fmt3 = (n) => n == null ? "" : Math.round(Number(n)).toLocaleString("en-US");
7467
7548
  var msName = (m, i) => m.title || KIND_NAMES[m.kind] || `Payment ${i + 1}`;
7468
7549
  async function loadContract(client, ref) {
7469
7550
  try {
@@ -7506,12 +7587,12 @@ function printContract(c) {
7506
7587
  ["you are", c.mySide],
7507
7588
  ["poster", c.posterOrg?.name ? `${c.posterOrg.name} (org)` : c.poster?.name],
7508
7589
  ["creator", c.creatorOrg?.name ? `${c.creatorOrg.name} (org)` : c.creator?.name],
7509
- ["total", `${usd2(c.totalUsd)} = cash ${usd2(c.cashUsd)} + ${fmt2(c.credits)} credits (${Math.round((c.creditsShare ?? 0) * 100)}% credits, min ${Math.round((c.minCreditsShare ?? 0.25) * 100)}%)`],
7590
+ ["total", `${usd2(c.totalUsd)} = cash ${usd2(c.cashUsd)} + ${fmt3(c.credits)} credits (${Math.round((c.creditsShare ?? 0) * 100)}% credits, min ${Math.round((c.minCreditsShare ?? 0.25) * 100)}%)`],
7510
7591
  ["credits release", c.creditsRelease === "milestones" ? "with each payment" : "all at once"],
7511
7592
  ["plan", c.status === "planning" ? `v${c.planVersion}${c.proposedBy ? ` proposed by ${c.proposedBy}` : ""}` : void 0],
7512
7593
  [
7513
7594
  "project credits",
7514
- e ? e.settledAt ? `settled: ${e.settledTo === "split" ? `${fmt2(e.settledCreator)} to creator, ${fmt2(e.settledPoster)} to poster` : `${fmt2(e.settledAmount)} to ${e.settledTo}`}${e.topupReturned ? `, ${fmt2(e.topupReturned)} top-up returned` : ""}` : `${fmt2(Math.max(0, e.left))} left of ${fmt2(e.budget ?? e.funded)} (used ${fmt2(e.spent)}${e.toppedUp ? `, creator topped up ${fmt2(e.toppedUp)}` : ""}; overdraft ${fmt2(e.overdraft)})` : void 0
7595
+ e ? e.settledAt ? `settled: ${e.settledTo === "split" ? `${fmt3(e.settledCreator)} to creator, ${fmt3(e.settledPoster)} to poster` : `${fmt3(e.settledAmount)} to ${e.settledTo}`}${e.topupReturned ? `, ${fmt3(e.topupReturned)} top-up returned` : ""}` : `${fmt3(Math.max(0, e.left))} left of ${fmt3(e.budget ?? e.funded)} (used ${fmt3(e.spent)}${e.toppedUp ? `, creator topped up ${fmt3(e.toppedUp)}` : ""}; overdraft ${fmt3(e.overdraft)})` : void 0
7515
7596
  ],
7516
7597
  ["workspace", e?.workspaceId],
7517
7598
  ["cancel request", c.cancelRequest ? `${c.cancelRequest.side}${c.cancelRequest.reason ? `: ${c.cancelRequest.reason}` : ""}` : void 0],
@@ -7524,7 +7605,7 @@ function printContract(c) {
7524
7605
  { header: "payment", value: (m) => truncate(msName(m, c.milestones.indexOf(m)), 22) },
7525
7606
  { header: "due", value: (m) => new Date(m.dueDate).toLocaleDateString() },
7526
7607
  { header: "cash", value: (m) => usd2(m.cashUsd) },
7527
- ...c.creditsRelease === "milestones" ? [{ header: "credits", value: (m) => fmt2(m.credits) }] : [],
7608
+ ...c.creditsRelease === "milestones" ? [{ header: "credits", value: (m) => fmt3(m.credits) }] : [],
7528
7609
  { header: "status", value: (m) => m.status },
7529
7610
  { header: "revisions", value: (m) => m.revisionsUsed ? `${m.revisionsUsed}/${c.revisionAllowance}` : "" },
7530
7611
  {
@@ -7535,14 +7616,14 @@ function printContract(c) {
7535
7616
  const pending = (c.changeRequests || []).find((r) => r.status === "pending");
7536
7617
  if (pending) {
7537
7618
  info("");
7538
- info(`${color.yellow("Change request waiting")} ${pending.id}: +${fmt2(pending.credits)} credits${pending.cashUsd ? ` + ${usd2(pending.cashUsd)}` : ""} (${pending.reason}) \u2014 ${truncate(pending.note, 60)}`);
7619
+ info(`${color.yellow("Change request waiting")} ${pending.id}: +${fmt3(pending.credits)} credits${pending.cashUsd ? ` + ${usd2(pending.cashUsd)}` : ""} (${pending.reason}) \u2014 ${truncate(pending.note, 60)}`);
7539
7620
  }
7540
7621
  const d = c.dispute;
7541
7622
  if (d && d.status !== "withdrawn") {
7542
7623
  info("");
7543
7624
  info(`${d.status === "open" ? color.yellow("Dispute open") : "Dispute decided"} \u2014 raised by ${d.side}: ${truncate(d.reason, 80)}`);
7544
7625
  for (const st of d.statements || []) info(color.dim(` ${st.side} \xB7 ${relativeTime(st.at)}: ${truncate(st.text, 80)}`));
7545
- if (d.resolution) info(` Decision: ${d.resolution.outcome}${d.resolution.outcome === "cancel" ? ` (${fmt2(d.resolution.creatorCredits)} credits to creator)` : ""} \u2014 ${truncate(d.resolution.note, 60)}`);
7626
+ if (d.resolution) info(` Decision: ${d.resolution.outcome}${d.resolution.outcome === "cancel" ? ` (${fmt3(d.resolution.creatorCredits)} credits to creator)` : ""} \u2014 ${truncate(d.resolution.note, 60)}`);
7546
7627
  }
7547
7628
  }
7548
7629
  function parsePayment(spec, perMilestone) {
@@ -7645,9 +7726,9 @@ function contractCommand(globals2) {
7645
7726
  const client = backreelClient(globals2);
7646
7727
  const c = await loadContract(client, ref);
7647
7728
  if (c.myCredits != null && c.myCredits < c.credits) {
7648
- throw new Error(`You have ${fmt2(c.myCredits)} general credits; this contract needs ${fmt2(c.credits)}. Top up on WJI first.`);
7729
+ throw new Error(`You have ${fmt3(c.myCredits)} general credits; this contract needs ${fmt3(c.credits)}. Top up on WJI first.`);
7649
7730
  }
7650
- if (!await confirm(`Add ${fmt2(c.credits)} credits to "${c.job?.title}" and start production?`, opts.yes)) return info("Aborted");
7731
+ if (!await confirm(`Add ${fmt3(c.credits)} credits to "${c.job?.title}" and start production?`, opts.yes)) return info("Aborted");
7651
7732
  await run(c.id, "/fund", void 0, (u) => `Funded \u2014 project workspace ${u.escrow?.workspaceId}`);
7652
7733
  });
7653
7734
  ct.command("submit <contract> <payment>").description("Creator: submit a payment (milestone) for review").option("--note <text|@file>", "what you are delivering").option("--link <url...>", "links to the delivery (max 5)").action(async (ref, n, opts) => {
@@ -7695,7 +7776,7 @@ function contractCommand(globals2) {
7695
7776
  ct.command("topup <contract> <credits>").description("Creator: add your own credits to the project (unused ones come back to you)").action(async (ref, credits) => {
7696
7777
  const n = Math.floor(Number(credits));
7697
7778
  if (!(n > 0)) throw new Error("credits must be a positive number");
7698
- await run(ref, "/topup", { credits: n }, (u) => `Topped up ${fmt2(n)} \u2014 ${fmt2(Math.max(0, u.escrow?.left ?? 0))} project credits left`);
7779
+ await run(ref, "/topup", { credits: n }, (u) => `Topped up ${fmt3(n)} \u2014 ${fmt3(Math.max(0, u.escrow?.left ?? 0))} project credits left`);
7699
7780
  });
7700
7781
  const change = ct.command("change").description("Change requests: ask for more credits / cash when scope changes or extra revisions");
7701
7782
  change.command("request <contract>", { isDefault: true }).description("Creator: propose extra credits and/or cash").option("--credits <n>", "extra credits", "0").option("--cash <n>", "extra cash in USD").option("--reason <r>", `${CR_REASONS.join(", ")}`, "scope").option("--payment <n>", "the payment it relates to (for extra revisions)").requiredOption("--note <text|@file>", "what changed").action(async (ref, opts) => {
@@ -7848,6 +7929,7 @@ function backreelCommand(globals2) {
7848
7929
  { header: "publish", value: (p2) => p2.publishStatus },
7849
7930
  { header: "project", value: (p2) => p2.projectStatus },
7850
7931
  { header: "credits", value: (p2) => (p2.credits || []).length },
7932
+ { header: "interested", value: (p2) => p2.interestCount ?? 0 },
7851
7933
  { header: "updated", value: (p2) => relativeTime(p2.updatedAt) }
7852
7934
  ]);
7853
7935
  const p = r?.pagination;
@@ -7863,7 +7945,7 @@ function backreelCommand(globals2) {
7863
7945
  br.command("create <title>").description("Create a post (always a draft until `backreel publish`)")
7864
7946
  ).action(async (title, opts) => {
7865
7947
  const client = backreelClient(globals2);
7866
- const body = { kind: "showcase", description: "", ...await postBody(opts), title };
7948
+ const body = { kind: "showcase", description: "", ...await postBody(opts, { client, title }), title };
7867
7949
  const created = await client.post(`${POSTS}/create`, body);
7868
7950
  render(created, (p) => success(`Created draft "${p.title}" \u2014 ${p.id}`));
7869
7951
  });
@@ -7871,7 +7953,7 @@ function backreelCommand(globals2) {
7871
7953
  br.command("set <postId>").description("Update a post; only the fields you pass change").option("--title <text>", "title")
7872
7954
  ).action(async (id, opts) => {
7873
7955
  const client = backreelClient(globals2);
7874
- const body = await postBody(opts);
7956
+ const body = await postBody(opts, { client, postId: id, title: opts.title });
7875
7957
  if (opts.title) body.title = opts.title;
7876
7958
  if (!Object.keys(body).length) throw new Error("Nothing to change \u2014 pass at least one field flag");
7877
7959
  const updated = await client.put(`${POSTS}/list/${id}`, body);
@@ -8069,9 +8151,20 @@ function backreelCommand(globals2) {
8069
8151
  return br;
8070
8152
  }
8071
8153
  function postOptions(cmd) {
8072
- return cmd.option("--kind <kind>", KINDS.join(" | ")).option("--logline <text>", `one-line hook, max ${LOGLINE_MAX} chars; public even when the description is locked ("" clears it)`).option("--category <category>", "category").option("--project-status <status>", PROJECT_STATUSES.join(" | ")).option("--tags <tags>", "comma-separated tags (replaces the list)").option("-d, --description <text>", "description, plain text or HTML (@file / - supported)").option("--cover <assetId>", "landscape cover asset").option("--cover-portrait <assetId>", "portrait cover asset").option("--trailer <assetId>", "trailer video asset (\u2264500 MB, any length)").option("--media <assetId...>", `LookBook gallery, images or videos in order (replaces it; \u2264${MEDIA_MAX}; "none" clears)`).option("--org <orgId|none>", "publish as one of your organizations; none = yourself (only the author can change it later)").option("--body <json>", "any other post fields as JSON (@file / - supported); flags win");
8154
+ return cmd.option("--kind <kind>", KINDS.join(" | ")).option("--logline <text>", `one-line hook, max ${LOGLINE_MAX} chars; public even when the description is locked ("" clears it)`).option("--category <category>", "category").option("--project-status <status>", PROJECT_STATUSES.join(" | ")).option("--tags <tags>", "comma-separated tags (replaces the list)").option("-d, --description <text>", "description, plain text or HTML (@file / - supported)").option("--cover <assetId>", "landscape cover asset").option("--cover-portrait <assetId>", "portrait cover asset").option("--trailer <file|vid|assetId|none>", "trailer: a local video file is uploaded to VOD (\u2264500 MB, any length); vod:<Vid> reuses an upload; a WJI asset id is the old way; none clears").option("--media <assetId...>", `LookBook gallery, images or videos in order (replaces it; \u2264${MEDIA_MAX}; "none" clears)`).option("--org <orgId|none>", "publish as one of your organizations; none = yourself (only the author can change it later)").option("--on-slate <on|off>", "crowdfunding only: also list it on the Slate, not just the Crowdfunding page (default off)").option("--body <json>", "any other post fields as JSON (@file / - supported); flags win");
8155
+ }
8156
+ async function trailerFields(value, ctx) {
8157
+ if (value === "none") return { trailer: null, trailerVod: null };
8158
+ if (value.startsWith("vod:")) return { trailerVod: { vid: value.slice(4) } };
8159
+ const isFile = await stat6(value).then((s) => s.isFile()).catch(() => false);
8160
+ if (isFile) {
8161
+ if (!ctx.client) throw new Error("--trailer <file> needs a signed-in client");
8162
+ const vid = await uploadVideoToVod(ctx.client, value, { postId: ctx.postId, title: `${ctx.title || "trailer"} \xB7 trailer` });
8163
+ return { trailerVod: { vid } };
8164
+ }
8165
+ return { trailer: value };
8073
8166
  }
8074
- async function postBody(opts) {
8167
+ async function postBody(opts, ctx = {}) {
8075
8168
  const body = { ...await readJson(opts.body) || {} };
8076
8169
  if (opts.kind) body.kind = oneOf2(opts.kind, KINDS, "--kind");
8077
8170
  if (opts.logline !== void 0) {
@@ -8081,12 +8174,13 @@ async function postBody(opts) {
8081
8174
  }
8082
8175
  if (opts.category) body.category = opts.category;
8083
8176
  if (opts.org !== void 0) body.organization = opts.org === "none" ? null : opts.org;
8177
+ if (opts.onSlate !== void 0) body.showOnSlate = oneOf2(opts.onSlate, ["on", "off"], "--on-slate") === "on";
8084
8178
  if (opts.projectStatus) body.projectStatus = oneOf2(opts.projectStatus, PROJECT_STATUSES, "--project-status");
8085
8179
  if (opts.tags !== void 0) body.tags = String(opts.tags).split(",").map((t) => t.trim()).filter(Boolean);
8086
8180
  if (opts.description !== void 0) body.description = toHtml(await readValue(opts.description));
8087
8181
  if (opts.cover) body.cover = opts.cover;
8088
8182
  if (opts.coverPortrait) body.coverPortrait = opts.coverPortrait;
8089
- if (opts.trailer) body.trailer = opts.trailer;
8183
+ if (opts.trailer) Object.assign(body, await trailerFields(String(opts.trailer), ctx));
8090
8184
  if (opts.media) {
8091
8185
  const media = opts.media.filter((m) => m !== "none");
8092
8186
  if (media.length > MEDIA_MAX) throw new Error(`--media takes at most ${MEDIA_MAX} items (the web's LookBook limit)`);
@@ -8123,10 +8217,13 @@ function printPost(p) {
8123
8217
  ["media", (p.assets || []).length ? `${p.assets.length} item(s)` : void 0],
8124
8218
  ["episodes", (p.episodes || []).length ? `${p.episodes.length} (\`backreel episode list\`)` : void 0],
8125
8219
  ["campaign", p.campaign ? `${p.campaign.raisedAmount}/${p.campaign.goalAmount} ${(p.campaign.currency || "").toUpperCase()}` : void 0],
8220
+ ["deadline", p.campaign?.deadline ? String(p.campaign.deadline).slice(0, 10) : void 0],
8221
+ ["on slate", p.kind === "crowdfunding" ? p.showOnSlate ? "yes" : "no (Crowdfunding page only)" : void 0],
8126
8222
  ["donations", p.donation?.enabled ? "on" : void 0],
8127
- ["trailer", p.trailer ? `${assetLabel(p.trailer)}${p.trailerDurationSec ? ` (${p.trailerDurationSec}s)` : ""}` : void 0],
8223
+ ["trailer", p.trailerVod?.vid ? `vod ${p.trailerVod.vid} \xB7 ${p.trailerVod.status}${p.trailerDurationSec ? ` (${Math.round(p.trailerDurationSec)}s)` : ""}` : p.trailer ? `${assetLabel(p.trailer)}${p.trailerDurationSec ? ` (${p.trailerDurationSec}s)` : ""}` : void 0],
8128
8224
  ["creator", p.creator?.name],
8129
8225
  ["likes", p.likeCount],
8226
+ ["interested", p.interestCount ? `${p.interestCount} (members get new-episode and campaign-result notices)` : void 0],
8130
8227
  ["updated", relativeTime(p.updatedAt)]
8131
8228
  ]);
8132
8229
  process.stdout.write("\n");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wji.ai/cli",
3
- "version": "0.9.2",
3
+ "version": "0.9.5",
4
4
  "description": "Command line interface for the WJI AI platform",
5
5
  "license": "UNLICENSED",
6
6
  "author": "WJI AI",
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: backreel
3
- description: Run a creator's works on Backreel (the film / short-drama showcase and crowdfunding site that shares accounts with WJI) with the `wji` CLI — create and publish a post (作品), prepare and upload cover / portrait / LookBook images at the right size and ratio, add cast and crew credits, upload and maintain episode videos (分集 via BytePlus VOD), set who can watch each episode, run a crowdfunding campaign with donation tiers and offline supporters, read donation records, answer enquiries (查詢 / 合作邀請 / 投資 / 授權) — reply, accept or decline, grant partner access — write blog posts (文章 / 作品動態) on a profile or under a project, and work the Job Plaza (接單廣場) — post a job, place a sealed bid, award it, then run the contract: payment plan, project credits, milestones, top-ups, change requests and disputes. Use whenever a task mentions Backreel, a Backreel post / work / reel, its episodes, trailer, cover or poster sizes, crowdfunding / 眾籌 / 捐款, supporters, enquiries / 查詢 on a creator's work, a Backreel blog post, project update or behind-the-scenes note, or a Backreel job, bid, quote, contract / 合約, milestone or dispute.
3
+ description: Run a creator's works on Backreel (the film / short-drama showcase and crowdfunding site that shares accounts with WJI) with the `wji` CLI — create and publish a post (作品), prepare and upload cover / portrait / LookBook images at the right size and ratio, add cast and crew credits, upload and maintain episode videos (分集 via BytePlus VOD), set who can watch each episode, run a crowdfunding campaign in WJI credits (all-or-nothing, tiers) or, for verified fundraisers, cash with offline supporters, back a post with credits, read backers and donation records, answer enquiries (查詢 / 合作邀請 / 投資 / 授權) — reply, accept or decline, grant partner access — write blog posts (文章 / 作品動態) on a profile or under a project, and work the Job Plaza (接單廣場) — post a job, place a sealed bid, award it, then run the contract: payment plan, project credits, milestones, top-ups, change requests and disputes. Use whenever a task mentions Backreel, a Backreel post / work / reel, its episodes, trailer, cover or poster sizes, crowdfunding / 眾籌 / 捐款, supporters, enquiries / 查詢 on a creator's work, a Backreel blog post, project update or behind-the-scenes note, or a Backreel job, bid, quote, contract / 合約, milestone or dispute.
4
4
  ---
5
5
 
6
6
  # Backreel
@@ -26,11 +26,12 @@ wji br list # your posts, drafts included
26
26
  | kind | `showcase` (episodes, trailer) or `crowdfunding` (goal, supporters) — switching wipes the other side's data | `br create --kind`, `br set --kind` |
27
27
  | title / logline / description | logline = one public line ≤200 chars; description = HTML or plain text | `br set --logline`, `-d @file.md` |
28
28
  | images | 16:9 cover, 9:16 portrait, LookBook gallery (≤9), credit photos | `br image <file> --slot … --post` |
29
- | trailer | a video asset, ≤500 MB, any length | `wji asset upload` → `br set --trailer` |
29
+ | trailer | a video on VOD, ≤500 MB, any length | `br set --trailer <file>` |
30
30
  | episodes | ≤30, each with its own viewer tier, hosted on VOD | `br episode …` |
31
31
  | credits | cast / crew / creative, ≤30, may link a WJI user or an NPC | `br credit …` |
32
- | campaign + donations | goal, offline total, deadline, donation tiers, offline supporters | `br fund …` |
32
+ | campaign + support | funding type (credits / cash / both), credits goal, cash goal, deadline, tiers, offline supporters | `br fund …` |
33
33
  | enquiries | messages from people who want to work with / invest in / license the work | `br enquiry …` |
34
+ | where it lists | showcase → the Slate (`/slate`); crowdfunding → the Crowdfunding page (`/crowdfunding`, filterable by Raising / Ended), and also the Slate when the creator opts in | `br set --on-slate on` |
34
35
 
35
36
  Viewer tiers, lowest to highest: `public` (anyone) < `member` (signed in) <
36
37
  `supporter` (has donated) < `partner` (granted by the creator or a verified
@@ -111,24 +112,37 @@ transcoding". Numbers on the command line are 1-based positions. Read
111
112
  [reference/episodes.md](reference/episodes.md) for the trailer, the legacy
112
113
  asset path, tier strategy, subtitles and failure handling.
113
114
 
114
- ## 4. Crowdfunding and donations
115
+ ## 4. Crowdfunding and credits support
115
116
 
116
117
  ```bash
117
- wji br create "下一部長片" --kind crowdfunding -d @pitch.md
118
- wji br fund set <postId> --goal 500000 --deadline 2026-12-31 \
119
- --donations on --currency hkd --min 50
120
- wji br fund tier add <postId> --title "早鳥" --amount 200 --perks "片尾鳴謝" --limit 300
118
+ wji br create "下一部長片" --kind crowdfunding -d @pitch.md # new campaigns default to credits
119
+ wji br fund set <postId> --mode credits --credit-goal 2000000 --deadline 2026-12-31
120
+ wji br fund tier add <postId> --title "早鳥" --amount 2000 --perks "片尾鳴謝" --limit 300
121
+ wji br fund show <postId> # funding type, progress, outcome, tiers
122
+ wji br fund pledges <postId> # credits backers
123
+ wji br fund back <postId> 500 --tier 1 -y # back someone else's post with your credits
124
+
125
+ # cash — only for publishers with Cash fundraiser verification
126
+ wji br fund set <postId> --mode both --goal 500000 --cash-currency hkd
121
127
  wji br fund supporter add <postId> --name "ABC Films" --amount 50000
122
128
  wji br fund set <postId> --offline-raised 50000 # money raised outside Backreel
123
- wji br fund show <postId> # progress, settings, tiers
124
- wji br fund donations <postId> # online donations, balance, payouts
125
129
  ```
126
130
 
127
- Progress = offline total + online donations. Donations (Stripe) work on any
128
- published post, not just crowdfunding, and stop at the deadline; the currency
129
- locks after the first donation. Payouts and refunds are done by Backreel
130
- staff, not the CLI. Read [reference/crowdfunding.md](reference/crowdfunding.md)
131
- for tier design and what supporters unlock.
131
+ Support is in **WJI credits**, from the backer's wallet — no card. On a
132
+ crowdfunding post the credits are held until the deadline: **all or
133
+ nothing** — goal missed → every backer refunded; goal reached → the campaign
134
+ is `funded` and becomes a production contract released by milestone. On a
135
+ showcase post credits go straight to the creator. Funding type: `credits`
136
+ (any creator), `cash` or `both` (need Cash fundraiser verification for the
137
+ person or organization publishing). Read
138
+ [reference/crowdfunding.md](reference/crowdfunding.md) for the rules and tier
139
+ design.
140
+
141
+ Members can also mark any published work **Interested** (有興趣). It is a
142
+ public count (`interested` in `backreel list` / `backreel get`); only the
143
+ creator sees who, on the site's Manage → Supporters tab. Interested members get
144
+ an in-app notice when a new episode goes live or a credits campaign ends. A
145
+ creator cannot mark their own work, and the CLI cannot set it.
132
146
 
133
147
  ## 5. Enquiries
134
148
 
@@ -1,35 +1,65 @@
1
- # Crowdfunding, donations and supporters
1
+ # Crowdfunding, credits support and supporters
2
2
 
3
- ## Two pieces that work together
3
+ ## Funding types
4
4
 
5
- | | Campaign | Donations |
5
+ | | Credits | Cash |
6
6
  | --- | --- | --- |
7
- | on | crowdfunding posts only | any published post |
8
- | holds | goal, offline total, deadline, offline supporter list | on/off, currency, minimum, tiers |
9
- | set with | `br fund set --goal --offline-raised --deadline`, `br fund supporter …` | `br fund set --donations --currency --min`, `br fund tier …` |
7
+ | who can run it | any creator | publishers (person or organization) with **Cash fundraiser** verification |
8
+ | goal | `--credit-goal` (credits) | `--goal` + `--cash-currency` |
9
+ | how backers give | pledge WJI credits on the site (`br fund back`) — held until the deadline | commitments arranged with Backreel, money paid outside the site, recorded as `--offline-raised` / `fund supporter` |
10
+ | at the deadline | **all or nothing**: goal reached → `funded` (becomes a production contract, credits released by milestone); missed → `failed`, every backer refunded automatically | nothing automatic |
10
11
 
11
- The public progress bar = `--offline-raised` + online donations; the
12
- supporter count = offline supporters + online donors.
12
+ `--mode both` runs both at once with two progress bars. New campaigns default
13
+ to credits. Posts made before funding types existed read as `cash`.
13
14
 
14
- ## Setting up a campaign
15
+ Credits support is also how showcase works take support: credits go straight
16
+ to the creator (no deadline, nothing held). Legacy Stripe donations stay in
17
+ `fund donations` but new support is credits.
18
+
19
+ ## Setting up a credits campaign
15
20
 
16
21
  ```bash
17
22
  wji br create "下一部長片" --kind crowdfunding -d @pitch.md --logline "…"
18
- wji br fund set <postId> --goal 500000 --deadline 2026-12-31
19
- wji br fund set <postId> --donations on --currency hkd --min 50
20
- wji br fund tier add <postId> --title "早鳥支持" --amount 200 --perks "片尾鳴謝 + 數碼海報" --limit 300
21
- wji br fund tier add <postId> --title "製片人" --amount 20000 --perks "聯合製片人名銜、探班一次" --limit 5
23
+ wji br fund set <postId> --mode credits --credit-goal 2000000 --deadline 2026-12-31
24
+ wji br fund tier add <postId> --title "早鳥支持" --amount 2000 --perks "片尾鳴謝 + 數碼海報" --limit 300
25
+ wji br fund tier add <postId> --title "製片人" --amount 200000 --perks "聯合製片人名銜、探班一次" --limit 5
22
26
  wji br check <postId>
23
27
  wji br publish <postId> # ask first
24
28
  ```
25
29
 
26
- - A crowdfunding post cannot publish with a goal below 1.
27
- - Currencies: hkd, usd, twd, sgd, eur, gbp. **Locked after the first
28
- donation** — choose before publishing.
30
+ - `--mode credits` (or `both`) switches credits support on by itself.
31
+ - A credits campaign needs a goal ≥ 1 and a **deadline in the future** to
32
+ publish; it settles when the deadline passes (checked every 10 minutes).
33
+ - **Funded → production contract.** The moment a credits campaign is funded,
34
+ Backreel turns it into a job contract (`wji br contract get <jobId>`, the
35
+ id is in `br fund show`): credits only, released milestone by milestone,
36
+ milestones must add up to exactly the credits raised. The creator sends the
37
+ plan (`br contract plan`), a Backreel admin accepts it on the backers'
38
+ behalf, the project workspace opens with the first milestone's credits, and
39
+ each later milestone unlocks when Backreel approves the previous one. If
40
+ the contract is cancelled, unused credits go back to the backers pro rata;
41
+ if it completes, what's left is the creator's.
42
+ - Once anyone has pledged, the campaign can't drop credits, the post can't be
43
+ deleted or switched to showcase, and support can't go back to cash. After it
44
+ settles the credits goal is fixed.
45
+ - Backers can't take a pledge back; Backreel staff can refund one.
46
+ - Where it lists: every published crowdfunding post is on the Crowdfunding
47
+ page (`/crowdfunding`, filterable by Raising / Ended). It is **not** on the
48
+ Slate unless the creator opts in: `wji br set <postId> --on-slate on`.
29
49
  - Amounts are whole numbers. Every tier must be ≥ the minimum (and ≥ 1);
30
50
  ≤10 tiers; `--limit` caps the slots (`--limit 0` on `tier set` = unlimited).
31
- - The deadline stops donations at that moment; `--deadline none` removes it.
32
- - A single donation is capped at 100,000.
51
+
52
+ ## Cash campaigns (verified fundraisers only)
53
+
54
+ ```bash
55
+ wji br fund set <postId> --mode cash --goal 500000 --cash-currency hkd --deadline 2026-12-31
56
+ wji br fund supporter add <postId> --name "ABC Films" --amount 50000
57
+ wji br fund set <postId> --offline-raised 50000
58
+ ```
59
+
60
+ Without Cash fundraiser verification the server refuses `cash` / `both`;
61
+ apply on the Backreel profile (Settings → verification). A post that already
62
+ had cash keeps it even if the verification later lapses.
33
63
 
34
64
  ## Designing tiers
35
65
 
@@ -58,25 +88,26 @@ Adding a supporter does **not** change the raised amount — update
58
88
  `--offline-raised` yourself (keep the two consistent). Avatars are square
59
89
  images (`br image logo.png --slot photo`).
60
90
 
61
- ## Donation records
91
+ ## Backers and donation records
62
92
 
63
93
  ```bash
64
- wji br fund donations <postId>
94
+ wji br fund pledges <postId> # credits backers: time, backer, credits, tier, status, message
95
+ wji br fund donations <postId> # legacy Stripe donations, balance, payouts
65
96
  ```
66
97
 
67
- Shows raised / paid out / balance, then each donation: time, donor (anonymous
68
- donors stay hidden even from the creator), amount, tier, status (`paid`,
69
- `partially_refunded`, `refunded`, `failed`, `cancelled`; a red *disputed*
70
- marks a card chargeback) and the donor's message. Payouts and
71
- refunds are handled by Backreel staff — the creator asks them, the CLI cannot
72
- do it.
98
+ Pledge status: `held` (campaign running, or funded and waiting for its
99
+ contract), `released` (a showcase tip, already the creator's), `refunded`,
100
+ `transferred` (moved into the production contract). Anonymous backers stay
101
+ hidden even from the creator. Legacy donations: refunds and payouts are
102
+ handled by Backreel staff — the CLI cannot do it.
73
103
 
74
- ## Showcase posts can take donations too
104
+ ## Showcase posts take credits support too
75
105
 
76
- A showcase post (episodes) can turn donations on to sell `supporter` access
77
- to later episodes — there is just no goal or progress bar:
106
+ A showcase post (episodes) can take credits support to sell `supporter`
107
+ access to later episodes — there is no goal or progress bar, and the credits
108
+ go straight to the creator:
78
109
 
79
110
  ```bash
80
- wji br fund set <postId> --donations on --currency hkd --min 30
111
+ wji br fund set <postId> --donations on --currency credits --min 100
81
112
  wji br episode tiers <postId> --public-to 2 --rest supporter
82
113
  ```
@@ -8,18 +8,22 @@
8
8
  | count | one | ≤30 |
9
9
  | length | no limit | no limit |
10
10
  | size | ≤500 MB | ≤500 MB |
11
- | storage | WJI asset (public URL) | BytePlus VOD (signed URL per play, ~5 min) |
11
+ | storage | BytePlus VOD (signed URL per play, ~5 min); older trailers may be a WJI asset | BytePlus VOD (signed URL per play, ~5 min) |
12
12
  | gated | no — always public | per episode: public / member / supporter / partner |
13
13
 
14
14
  ### Trailer
15
15
 
16
16
  ```bash
17
- wji asset upload trailer.mp4 --json # ≤500 MB, any length
18
- wji br set <postId> --trailer <assetId>
17
+ wji br set <postId> --trailer trailer.mp4 # uploads to VOD, ≤500 MB, any length
18
+ wji br set <postId> --trailer vod:<Vid> # reuse a video already in VOD
19
+ wji br set <postId> --trailer none # remove it
19
20
  ```
20
21
 
21
- There is no duration cap. The upload is rejected only when the file is over
22
- 500 MB.
22
+ The file goes straight to Backreel's VOD space (the same flow as episodes), so
23
+ viewers get adaptive streaming and the quality menu. There is no duration cap;
24
+ only files over 500 MB are refused. It plays after transcoding finishes,
25
+ usually within a minute or two (`br get` shows `processing` → `ready`).
26
+ Passing a WJI asset id still works but is the old way.
23
27
 
24
28
  ## Uploading episodes
25
29