@wji.ai/cli 0.9.2 → 0.9.4
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 +106 -28
- package/package.json +1 -1
- package/skill/backreel/SKILL.md +28 -14
- package/skill/backreel/reference/crowdfunding.md +61 -30
package/dist/index.js
CHANGED
|
@@ -6955,7 +6955,11 @@ function enquiryCommand(globals2) {
|
|
|
6955
6955
|
|
|
6956
6956
|
// src/commands/backreel-fund.ts
|
|
6957
6957
|
import { Command as Command22 } from "commander";
|
|
6958
|
-
var
|
|
6958
|
+
var CASH_CURRENCIES = ["hkd", "usd", "twd", "sgd", "eur", "gbp"];
|
|
6959
|
+
var CURRENCIES = ["credits", ...CASH_CURRENCIES];
|
|
6960
|
+
var MODES = ["credits", "cash", "both"];
|
|
6961
|
+
var hasCredits = (mode) => mode === "credits" || mode === "both";
|
|
6962
|
+
var fmt = (n) => Math.round(Number(n) || 0).toLocaleString("en-US");
|
|
6959
6963
|
var MAX_TIERS = 10;
|
|
6960
6964
|
var num = (value, flag, min = 0) => {
|
|
6961
6965
|
const n = Number(value);
|
|
@@ -6975,6 +6979,10 @@ function itemIndex(n, count, what) {
|
|
|
6975
6979
|
}
|
|
6976
6980
|
function campaignDto(c) {
|
|
6977
6981
|
return {
|
|
6982
|
+
// fundingMode / currency / creditGoal: the server keeps the stored value when a field is left out
|
|
6983
|
+
fundingMode: c?.fundingMode ?? void 0,
|
|
6984
|
+
currency: c?.fundingMode && c.fundingMode !== "credits" ? c?.currency ?? void 0 : void 0,
|
|
6985
|
+
creditGoal: c?.creditGoal ?? void 0,
|
|
6978
6986
|
goalAmount: c?.goalAmount ?? 0,
|
|
6979
6987
|
offlineRaisedAmount: c?.offlineRaisedAmount ?? 0,
|
|
6980
6988
|
deadline: c?.deadline ?? null,
|
|
@@ -7004,22 +7012,40 @@ var put = (client, id, body) => client.put(`${POSTS}/list/${id}`, body);
|
|
|
7004
7012
|
function printFund(post) {
|
|
7005
7013
|
const c = post.campaign;
|
|
7006
7014
|
const d = post.donation || {};
|
|
7007
|
-
const
|
|
7015
|
+
const mode = c?.fundingMode || "cash";
|
|
7016
|
+
const cur = (c?.currency || d.currency !== "credits" && d.currency || "hkd").toUpperCase();
|
|
7008
7017
|
if (c) {
|
|
7009
|
-
const
|
|
7010
|
-
|
|
7011
|
-
|
|
7012
|
-
|
|
7013
|
-
|
|
7014
|
-
|
|
7015
|
-
|
|
7018
|
+
const rows = [["funding", mode]];
|
|
7019
|
+
if (hasCredits(mode)) {
|
|
7020
|
+
const pct = c.creditGoal ? ` (${Math.round((c.creditsRaised || 0) / c.creditGoal * 100)}%)` : "";
|
|
7021
|
+
rows.push(
|
|
7022
|
+
["credits goal", `${fmt(c.creditGoal)} credits`],
|
|
7023
|
+
["credits raised", `${fmt(c.creditsRaised)} credits${pct} from ${c.creditBackerCount ?? 0} backer(s)`],
|
|
7024
|
+
["outcome", c.outcome === "funded" ? color.green("funded") : c.outcome === "failed" ? color.red("failed \u2014 refunded") : "raising"]
|
|
7025
|
+
);
|
|
7026
|
+
}
|
|
7027
|
+
if (mode !== "credits") {
|
|
7028
|
+
const pct = c.goalAmount ? ` (${Math.round((c.raisedAmount || 0) / c.goalAmount * 100)}%)` : "";
|
|
7029
|
+
rows.push(
|
|
7030
|
+
["cash goal", `${c.goalAmount} ${cur}`],
|
|
7031
|
+
["cash raised", `${c.raisedAmount} ${cur}${pct} = offline ${c.offlineRaisedAmount ?? 0} + online ${(c.raisedAmount || 0) - (c.offlineRaisedAmount || 0)}`]
|
|
7032
|
+
);
|
|
7033
|
+
}
|
|
7034
|
+
if (c.production) {
|
|
7035
|
+
rows.push([
|
|
7036
|
+
"production",
|
|
7037
|
+
`${c.production.status} \u2014 ${c.production.milestonesApproved}/${c.production.milestonesTotal} milestones (contract: \`wji br contract get ${c.production.contractJobId}\`)`
|
|
7038
|
+
]);
|
|
7039
|
+
}
|
|
7040
|
+
rows.push(["supporters", c.supporterCount], ["deadline", c.deadline || color.dim("none")]);
|
|
7041
|
+
detail(rows);
|
|
7016
7042
|
process.stdout.write("\n");
|
|
7017
7043
|
} else {
|
|
7018
7044
|
info(color.dim(`${post.kind} post \u2014 no campaign (goal / offline supporters are crowdfunding-only); donations still work`));
|
|
7019
7045
|
}
|
|
7020
7046
|
detail([
|
|
7021
7047
|
["donations", d.enabled ? color.green("on") : "off"],
|
|
7022
|
-
["currency", (d.currency || "hkd").toUpperCase()],
|
|
7048
|
+
["currency", d.currency === "credits" ? "WJI credits" : (d.currency || "hkd").toUpperCase()],
|
|
7023
7049
|
["minimum", d.minAmount ?? 0]
|
|
7024
7050
|
]);
|
|
7025
7051
|
if ((d.tiers || []).length) {
|
|
@@ -7048,13 +7074,17 @@ function fundCommand(globals2) {
|
|
|
7048
7074
|
const post = await loadPost(client, postId);
|
|
7049
7075
|
render({ campaign: post.campaign, donation: post.donation, kind: post.kind }, () => printFund(post));
|
|
7050
7076
|
});
|
|
7051
|
-
fund.command("set <postId>").description("
|
|
7077
|
+
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
7078
|
const client = backreelClient(globals2);
|
|
7053
7079
|
const post = await loadPost(client, postId);
|
|
7054
7080
|
const body = {};
|
|
7055
|
-
|
|
7081
|
+
const campaignFlags = [opts.goal, opts.offlineRaised, opts.deadline, opts.mode, opts.creditGoal, opts.cashCurrency];
|
|
7082
|
+
if (campaignFlags.some((v) => v !== void 0)) {
|
|
7056
7083
|
requireCampaign(post);
|
|
7057
7084
|
const campaign = campaignDto(post.campaign);
|
|
7085
|
+
if (opts.mode !== void 0) campaign.fundingMode = oneOf2(String(opts.mode).toLowerCase(), MODES, "--mode");
|
|
7086
|
+
if (opts.creditGoal !== void 0) campaign.creditGoal = Math.round(num(opts.creditGoal, "--credit-goal", 1));
|
|
7087
|
+
if (opts.cashCurrency !== void 0) campaign.currency = oneOf2(String(opts.cashCurrency).toLowerCase(), CASH_CURRENCIES, "--cash-currency");
|
|
7058
7088
|
if (opts.goal !== void 0) campaign.goalAmount = num(opts.goal, "--goal");
|
|
7059
7089
|
if (opts.offlineRaised !== void 0) campaign.offlineRaisedAmount = num(opts.offlineRaised, "--offline-raised");
|
|
7060
7090
|
if (opts.deadline !== void 0) {
|
|
@@ -7066,9 +7096,12 @@ function fundCommand(globals2) {
|
|
|
7066
7096
|
}
|
|
7067
7097
|
}
|
|
7068
7098
|
body.campaign = campaign;
|
|
7099
|
+
if (hasCredits(campaign.fundingMode) && opts.currency === void 0 && opts.donations === void 0) {
|
|
7100
|
+
body.donation = { ...donationDto(post.donation), enabled: true, currency: "credits" };
|
|
7101
|
+
}
|
|
7069
7102
|
}
|
|
7070
7103
|
if (opts.donations !== void 0 || opts.currency !== void 0 || opts.min !== void 0) {
|
|
7071
|
-
const donation = donationDto(post.donation);
|
|
7104
|
+
const donation = body.donation || donationDto(post.donation);
|
|
7072
7105
|
if (opts.donations !== void 0) donation.enabled = oneOf2(String(opts.donations).toLowerCase(), ["on", "off"], "--donations") === "on";
|
|
7073
7106
|
if (opts.currency !== void 0) donation.currency = oneOf2(String(opts.currency).toLowerCase(), CURRENCIES, "--currency");
|
|
7074
7107
|
if (opts.min !== void 0) donation.minAmount = Math.round(num(opts.min, "--min"));
|
|
@@ -7185,6 +7218,46 @@ function fundCommand(globals2) {
|
|
|
7185
7218
|
if ((r.payouts || []).length) info(color.dim(`${r.payouts.length} payout(s) \u2014 payouts and refunds are handled by Backreel staff`));
|
|
7186
7219
|
});
|
|
7187
7220
|
});
|
|
7221
|
+
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) => {
|
|
7222
|
+
const client = backreelClient(globals2);
|
|
7223
|
+
const r = await client.get(`/backreel/pledge/post/${postId}/manage`, { limit: Number(opts.limit) });
|
|
7224
|
+
render(r, () => {
|
|
7225
|
+
detail([
|
|
7226
|
+
["credits", fmt(r.stats?.credits ?? 0)],
|
|
7227
|
+
["backers", r.stats?.backerCount ?? 0],
|
|
7228
|
+
["pledges", r.stats?.pledgeCount ?? 0]
|
|
7229
|
+
]);
|
|
7230
|
+
process.stdout.write("\n");
|
|
7231
|
+
table(r.data || [], [
|
|
7232
|
+
{ header: "when", value: (p) => relativeTime(p.createdAt) },
|
|
7233
|
+
{ header: "backer", value: (p) => p.isAnonymous ? color.dim("anonymous") : truncate(p.backer?.name, 20) },
|
|
7234
|
+
{ header: "credits", value: (p) => fmt(p.credits) },
|
|
7235
|
+
{ header: "tier", value: (p) => truncate(p.tier?.title, 16) },
|
|
7236
|
+
{ header: "status", value: (p) => p.status },
|
|
7237
|
+
{ header: "message", value: (p) => truncate(p.message, 30) }
|
|
7238
|
+
]);
|
|
7239
|
+
});
|
|
7240
|
+
});
|
|
7241
|
+
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) => {
|
|
7242
|
+
const client = backreelClient(globals2);
|
|
7243
|
+
const post = await client.get(`${POSTS}/list/${postId}`);
|
|
7244
|
+
if (post.donation?.currency !== "credits" || !post.donation?.enabled) {
|
|
7245
|
+
throw new Error(`"${post.title}" doesn't take credits support`);
|
|
7246
|
+
}
|
|
7247
|
+
const tiers = post.donation.tiers || [];
|
|
7248
|
+
const tier2 = opts.tier ? tiers[itemIndex(opts.tier, tiers.length, "Tier")] : null;
|
|
7249
|
+
const amount = Math.round(num(credits, "<credits>", 1));
|
|
7250
|
+
const where = post.kind === "crowdfunding" ? "held by the campaign until its deadline" : "sent to the creator";
|
|
7251
|
+
if (!await confirm(`Back "${post.title}" with ${fmt(amount)} credits (${where})?`, opts.yes)) return info("Aborted");
|
|
7252
|
+
const pledge = await client.post("/backreel/pledge", {
|
|
7253
|
+
post: postId,
|
|
7254
|
+
credits: amount,
|
|
7255
|
+
tierId: tier2?.id,
|
|
7256
|
+
message: opts.message,
|
|
7257
|
+
isAnonymous: !!opts.anonymous
|
|
7258
|
+
});
|
|
7259
|
+
render(pledge, () => success(`Backed "${post.title}" with ${fmt(pledge.credits)} credits \u2014 ${pledge.status}`));
|
|
7260
|
+
});
|
|
7188
7261
|
return fund;
|
|
7189
7262
|
}
|
|
7190
7263
|
|
|
@@ -7211,12 +7284,12 @@ function parseDay(value, flag) {
|
|
|
7211
7284
|
return d.toISOString();
|
|
7212
7285
|
}
|
|
7213
7286
|
var usd = (n) => n == null ? "" : `US$${Number(n).toLocaleString("en-US", { maximumFractionDigits: 2 })}`;
|
|
7214
|
-
var
|
|
7287
|
+
var fmt2 = (n) => n == null ? "" : Math.round(Number(n)).toLocaleString("en-US");
|
|
7215
7288
|
function budgetText(j) {
|
|
7216
7289
|
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()} ${
|
|
7290
|
+
const cur = j.currency === "usd" || !j.currency ? usd : (n) => `${String(j.currency).toUpperCase()} ${fmt2(n)}`;
|
|
7218
7291
|
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 ? `${
|
|
7292
|
+
return [cash, j.budgetCredits ? `${fmt2(j.budgetCredits)} credits` : ""].filter(Boolean).join(" + ");
|
|
7220
7293
|
}
|
|
7221
7294
|
function jobOptions2(cmd) {
|
|
7222
7295
|
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 +7367,8 @@ function printBids(rows) {
|
|
|
7294
7367
|
{ header: "job", value: (b) => truncate(b.job?.title, 24) },
|
|
7295
7368
|
{ header: "bidder", value: (b) => truncate(b.organization?.name || b.bidder?.name, 18) },
|
|
7296
7369
|
{ 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()} ${
|
|
7298
|
-
{ header: "credits", value: (b) =>
|
|
7370
|
+
{ header: "price", value: (b) => b.price == null ? "" : b.currency === "usd" || !b.currency ? usd(b.price) : `${b.currency.toUpperCase()} ${fmt2(b.price)}` },
|
|
7371
|
+
{ header: "credits", value: (b) => fmt2(b.creditsPrice) },
|
|
7299
7372
|
{ header: "days", value: (b) => b.timelineDays ?? "" },
|
|
7300
7373
|
{ header: "pitch", value: (b) => truncate(b.pitch, 30) }
|
|
7301
7374
|
]);
|
|
@@ -7463,7 +7536,7 @@ var KIND_NAMES = {
|
|
|
7463
7536
|
draft: "Draft"
|
|
7464
7537
|
};
|
|
7465
7538
|
var usd2 = (n) => n == null ? "" : `US$${Number(n).toLocaleString("en-US", { maximumFractionDigits: 2 })}`;
|
|
7466
|
-
var
|
|
7539
|
+
var fmt3 = (n) => n == null ? "" : Math.round(Number(n)).toLocaleString("en-US");
|
|
7467
7540
|
var msName = (m, i) => m.title || KIND_NAMES[m.kind] || `Payment ${i + 1}`;
|
|
7468
7541
|
async function loadContract(client, ref) {
|
|
7469
7542
|
try {
|
|
@@ -7506,12 +7579,12 @@ function printContract(c) {
|
|
|
7506
7579
|
["you are", c.mySide],
|
|
7507
7580
|
["poster", c.posterOrg?.name ? `${c.posterOrg.name} (org)` : c.poster?.name],
|
|
7508
7581
|
["creator", c.creatorOrg?.name ? `${c.creatorOrg.name} (org)` : c.creator?.name],
|
|
7509
|
-
["total", `${usd2(c.totalUsd)} = cash ${usd2(c.cashUsd)} + ${
|
|
7582
|
+
["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
7583
|
["credits release", c.creditsRelease === "milestones" ? "with each payment" : "all at once"],
|
|
7511
7584
|
["plan", c.status === "planning" ? `v${c.planVersion}${c.proposedBy ? ` proposed by ${c.proposedBy}` : ""}` : void 0],
|
|
7512
7585
|
[
|
|
7513
7586
|
"project credits",
|
|
7514
|
-
e ? e.settledAt ? `settled: ${e.settledTo === "split" ? `${
|
|
7587
|
+
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
7588
|
],
|
|
7516
7589
|
["workspace", e?.workspaceId],
|
|
7517
7590
|
["cancel request", c.cancelRequest ? `${c.cancelRequest.side}${c.cancelRequest.reason ? `: ${c.cancelRequest.reason}` : ""}` : void 0],
|
|
@@ -7524,7 +7597,7 @@ function printContract(c) {
|
|
|
7524
7597
|
{ header: "payment", value: (m) => truncate(msName(m, c.milestones.indexOf(m)), 22) },
|
|
7525
7598
|
{ header: "due", value: (m) => new Date(m.dueDate).toLocaleDateString() },
|
|
7526
7599
|
{ header: "cash", value: (m) => usd2(m.cashUsd) },
|
|
7527
|
-
...c.creditsRelease === "milestones" ? [{ header: "credits", value: (m) =>
|
|
7600
|
+
...c.creditsRelease === "milestones" ? [{ header: "credits", value: (m) => fmt3(m.credits) }] : [],
|
|
7528
7601
|
{ header: "status", value: (m) => m.status },
|
|
7529
7602
|
{ header: "revisions", value: (m) => m.revisionsUsed ? `${m.revisionsUsed}/${c.revisionAllowance}` : "" },
|
|
7530
7603
|
{
|
|
@@ -7535,14 +7608,14 @@ function printContract(c) {
|
|
|
7535
7608
|
const pending = (c.changeRequests || []).find((r) => r.status === "pending");
|
|
7536
7609
|
if (pending) {
|
|
7537
7610
|
info("");
|
|
7538
|
-
info(`${color.yellow("Change request waiting")} ${pending.id}: +${
|
|
7611
|
+
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
7612
|
}
|
|
7540
7613
|
const d = c.dispute;
|
|
7541
7614
|
if (d && d.status !== "withdrawn") {
|
|
7542
7615
|
info("");
|
|
7543
7616
|
info(`${d.status === "open" ? color.yellow("Dispute open") : "Dispute decided"} \u2014 raised by ${d.side}: ${truncate(d.reason, 80)}`);
|
|
7544
7617
|
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" ? ` (${
|
|
7618
|
+
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
7619
|
}
|
|
7547
7620
|
}
|
|
7548
7621
|
function parsePayment(spec, perMilestone) {
|
|
@@ -7645,9 +7718,9 @@ function contractCommand(globals2) {
|
|
|
7645
7718
|
const client = backreelClient(globals2);
|
|
7646
7719
|
const c = await loadContract(client, ref);
|
|
7647
7720
|
if (c.myCredits != null && c.myCredits < c.credits) {
|
|
7648
|
-
throw new Error(`You have ${
|
|
7721
|
+
throw new Error(`You have ${fmt3(c.myCredits)} general credits; this contract needs ${fmt3(c.credits)}. Top up on WJI first.`);
|
|
7649
7722
|
}
|
|
7650
|
-
if (!await confirm(`Add ${
|
|
7723
|
+
if (!await confirm(`Add ${fmt3(c.credits)} credits to "${c.job?.title}" and start production?`, opts.yes)) return info("Aborted");
|
|
7651
7724
|
await run(c.id, "/fund", void 0, (u) => `Funded \u2014 project workspace ${u.escrow?.workspaceId}`);
|
|
7652
7725
|
});
|
|
7653
7726
|
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 +7768,7 @@ function contractCommand(globals2) {
|
|
|
7695
7768
|
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
7769
|
const n = Math.floor(Number(credits));
|
|
7697
7770
|
if (!(n > 0)) throw new Error("credits must be a positive number");
|
|
7698
|
-
await run(ref, "/topup", { credits: n }, (u) => `Topped up ${
|
|
7771
|
+
await run(ref, "/topup", { credits: n }, (u) => `Topped up ${fmt3(n)} \u2014 ${fmt3(Math.max(0, u.escrow?.left ?? 0))} project credits left`);
|
|
7699
7772
|
});
|
|
7700
7773
|
const change = ct.command("change").description("Change requests: ask for more credits / cash when scope changes or extra revisions");
|
|
7701
7774
|
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 +7921,7 @@ function backreelCommand(globals2) {
|
|
|
7848
7921
|
{ header: "publish", value: (p2) => p2.publishStatus },
|
|
7849
7922
|
{ header: "project", value: (p2) => p2.projectStatus },
|
|
7850
7923
|
{ header: "credits", value: (p2) => (p2.credits || []).length },
|
|
7924
|
+
{ header: "interested", value: (p2) => p2.interestCount ?? 0 },
|
|
7851
7925
|
{ header: "updated", value: (p2) => relativeTime(p2.updatedAt) }
|
|
7852
7926
|
]);
|
|
7853
7927
|
const p = r?.pagination;
|
|
@@ -8069,7 +8143,7 @@ function backreelCommand(globals2) {
|
|
|
8069
8143
|
return br;
|
|
8070
8144
|
}
|
|
8071
8145
|
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");
|
|
8146
|
+
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("--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");
|
|
8073
8147
|
}
|
|
8074
8148
|
async function postBody(opts) {
|
|
8075
8149
|
const body = { ...await readJson(opts.body) || {} };
|
|
@@ -8081,6 +8155,7 @@ async function postBody(opts) {
|
|
|
8081
8155
|
}
|
|
8082
8156
|
if (opts.category) body.category = opts.category;
|
|
8083
8157
|
if (opts.org !== void 0) body.organization = opts.org === "none" ? null : opts.org;
|
|
8158
|
+
if (opts.onSlate !== void 0) body.showOnSlate = oneOf2(opts.onSlate, ["on", "off"], "--on-slate") === "on";
|
|
8084
8159
|
if (opts.projectStatus) body.projectStatus = oneOf2(opts.projectStatus, PROJECT_STATUSES, "--project-status");
|
|
8085
8160
|
if (opts.tags !== void 0) body.tags = String(opts.tags).split(",").map((t) => t.trim()).filter(Boolean);
|
|
8086
8161
|
if (opts.description !== void 0) body.description = toHtml(await readValue(opts.description));
|
|
@@ -8123,10 +8198,13 @@ function printPost(p) {
|
|
|
8123
8198
|
["media", (p.assets || []).length ? `${p.assets.length} item(s)` : void 0],
|
|
8124
8199
|
["episodes", (p.episodes || []).length ? `${p.episodes.length} (\`backreel episode list\`)` : void 0],
|
|
8125
8200
|
["campaign", p.campaign ? `${p.campaign.raisedAmount}/${p.campaign.goalAmount} ${(p.campaign.currency || "").toUpperCase()}` : void 0],
|
|
8201
|
+
["deadline", p.campaign?.deadline ? String(p.campaign.deadline).slice(0, 10) : void 0],
|
|
8202
|
+
["on slate", p.kind === "crowdfunding" ? p.showOnSlate ? "yes" : "no (Crowdfunding page only)" : void 0],
|
|
8126
8203
|
["donations", p.donation?.enabled ? "on" : void 0],
|
|
8127
8204
|
["trailer", p.trailer ? `${assetLabel(p.trailer)}${p.trailerDurationSec ? ` (${p.trailerDurationSec}s)` : ""}` : void 0],
|
|
8128
8205
|
["creator", p.creator?.name],
|
|
8129
8206
|
["likes", p.likeCount],
|
|
8207
|
+
["interested", p.interestCount ? `${p.interestCount} (members get new-episode and campaign-result notices)` : void 0],
|
|
8130
8208
|
["updated", relativeTime(p.updatedAt)]
|
|
8131
8209
|
]);
|
|
8132
8210
|
process.stdout.write("\n");
|
package/package.json
CHANGED
package/skill/backreel/SKILL.md
CHANGED
|
@@ -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
|
|
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
|
|
@@ -29,8 +29,9 @@ wji br list # your posts, drafts included
|
|
|
29
29
|
| trailer | a video asset, ≤500 MB, any length | `wji asset upload` → `br set --trailer` |
|
|
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 +
|
|
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
|
|
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
|
|
119
|
-
|
|
120
|
-
wji br fund
|
|
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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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,
|
|
1
|
+
# Crowdfunding, credits support and supporters
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## Funding types
|
|
4
4
|
|
|
5
|
-
| |
|
|
5
|
+
| | Credits | Cash |
|
|
6
6
|
| --- | --- | --- |
|
|
7
|
-
|
|
|
8
|
-
|
|
|
9
|
-
|
|
|
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
|
-
|
|
12
|
-
|
|
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
|
-
|
|
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
|
|
19
|
-
wji br fund
|
|
20
|
-
wji br fund tier add <postId> --title "
|
|
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
|
-
-
|
|
27
|
-
-
|
|
28
|
-
|
|
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
|
-
|
|
32
|
-
|
|
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
|
-
##
|
|
91
|
+
## Backers and donation records
|
|
62
92
|
|
|
63
93
|
```bash
|
|
64
|
-
wji br fund
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
`
|
|
70
|
-
|
|
71
|
-
|
|
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
|
|
104
|
+
## Showcase posts take credits support too
|
|
75
105
|
|
|
76
|
-
A showcase post (episodes) can
|
|
77
|
-
to later episodes — there is
|
|
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
|
|
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
|
```
|