@officexapp/vidfarm-devcli 0.21.58 → 0.21.60

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.
@@ -20,6 +20,7 @@
20
20
  import { parseArgs } from "node:util";
21
21
  import { readFileSync } from "node:fs";
22
22
  import { readStoredAuth } from "./auth-store.js";
23
+ import { verifyProof } from "./proof-verify.js";
23
24
  const BOLD = "\x1b[1m";
24
25
  const DIM = "\x1b[2m";
25
26
  const GREEN = "\x1b[32m";
@@ -82,15 +83,47 @@ GIGWORKER — "help me earn on vidfarm" (free — bring your own key: export DOL
82
83
 
83
84
  delivering, and getting paid:
84
85
  gigs upload <file> [--prefix p] Put a file on DollarPlatoon's own s3, get a durable url
86
+ gigs verify-proof --proof <url> [--proof <url>…] RUN THIS BEFORE YOU SUBMIT. No key needed.
87
+ Loads every url the way a stranger's browser would (no cookie,
88
+ no login) and refuses a dead link, a 403 bucket, an HTML error
89
+ body, or a presigned url whose read access expires. Also checks
90
+ you did not swap the two halves of the delivery.
91
+ --clean-master <file> …the master you are about to seal (checked on disk)
92
+ --private-note "<text>" …the note you are about to send
93
+ --clean-in-proof "the proof url IS my final cut and I meant that" — silences it
94
+ --strict warnings fail too Exits non-zero on any failure.
85
95
  gigs submit <gig-id> --task <task-id> --proof <url> [--proof <url>…]
96
+ Runs verify-proof first and REFUSES to send a broken proof
97
+ (--skip-verify overrides, at your own risk).
86
98
  --clean-master <file> Upload it and seal it in the private note. THE WATERMARKED
87
99
  cut goes in --proof; the clean master goes here, and the
88
100
  buyer cannot read it until a rollup has actually paid you.
89
101
  --private-note "<text>" Anything else that follows the master (ProRes, project
90
- files, music licence id). 8000 chars, cannot be added later.
102
+ files, music licence id). 8000 chars, and on a SUBMITTED
103
+ proof it cannot be changed — withdraw it first, or draft it.
91
104
  --mailbox <id> --tags a,b --note "<text>" (--note is the PUBLIC feedback field)
105
+ --ask <usd> Quote your own price (machines with price offers on). An
106
+ ask is a QUOTE — the buyer must approve at that number.
107
+ --draft Save it WITHOUT sending. No review clock, no webhook, the
108
+ buyer sees nothing. (same as: gigs draft <gig-id> …)
92
109
  Watch the cut and approve it BEFORE this call — a rejection scores on your
93
110
  wallet and clients read it as a trust score on your public profile.
111
+
112
+ drafts, and the undo (a proof is NOT one-way while it is still pending):
113
+ gigs draft <gig-id> --task <id> --proof <url> Save without sending. Claims the task.
114
+ gigs drafts <gig-id> Your unsent drafts on that machine
115
+ gigs edit-draft <gig-id> <proof-id> [--proof <url>…] [--private-note "<t>"] [--ask <usd>]
116
+ Change an unsent draft. --private-note "" clears it.
117
+ gigs send <gig-id> <proof-id> Send a draft. The price is snapshotted NOW, not when you
118
+ saved it, and the review clock starts here.
119
+ gigs withdraw <gig-id> <proof-id> THE UNDO. Pulls a pending proof back to draft, stops the
120
+ review clock, and hides it from the buyer again.
121
+ (aliases: undo, unsend)
122
+ Works ONLY while the proof is pending. Once the buyer has
123
+ approved or rejected it the verdict is theirs, and anything
124
+ inside a rollup or already paid is final.
125
+ gigs discard <gig-id> <proof-id> Delete a draft and free its task. Drafts only — a
126
+ submitted proof stays on the record. (alias: delete-draft)
94
127
  gigs proof <gig-id> <proof-id> Read your own proof back: paid_out_at + the private note
95
128
  gigs earnings Your rollups + totals. (aliases: rollups, paid)
96
129
  gigs mine Your mailboxes across every gig
@@ -100,12 +133,14 @@ GIGWORKER — "help me earn on vidfarm" (free — bring your own key: export DOL
100
133
 
101
134
  e.g. DOLLARPLATOON_API_KEY=… vidfarm gigs earn
102
135
  vidfarm gigs check "https://dollarplatoon.com/gig/GIG_01H…/join?invite=abc"
136
+ vidfarm gigs verify-proof --proof https://…/final-watermarked.mp4 --clean-master ./final-clean.mp4
103
137
  vidfarm gigs submit GIG_01H… --task TASK_01H… \\
104
138
  --proof https://…/final-watermarked.mp4 --clean-master ./final-clean.mp4
105
139
  vidfarm gigs add-task --task "60s explainer for acme.com" --price 0.25 \\
106
140
  --upload-link "https://vidfarm.cc/directory/preview/dsh_…/files/acme"
107
141
  vidfarm gigs proofs --status pending
108
142
  vidfarm gigs approve PRF_01H… --feedback "Great hook, keeping it."
143
+ vidfarm gigs withdraw GIG_01H… PRF_01H… # sent the wrong cut — take it back
109
144
 
110
145
  Building the video is a separate job: https://vidfarm.cc/agentic-clipper.md is the loop,
111
146
  and https://vidfarm.cc/flash-harness.md is how to build it on a weak model.`;
@@ -1019,6 +1054,55 @@ async function cmdClaim(auth, values, positionals) {
1019
1054
  console.log(`${DIM} vidfarm gigs submit ${gigId} --task ${tasks[0]?.id ?? "<task-id>"} --proof <public mp4 url>${RESET}`);
1020
1055
  });
1021
1056
  }
1057
+ // ── the pre-submit sanity check ──────────────────────────────────────────────
1058
+ /** Print one verify result the same way from `verify-proof` and from `submit`. */
1059
+ function printVerify(result, strict) {
1060
+ const paint = (check) => {
1061
+ const badge = check.level === "fail" ? `${RED}✗ FAIL${RESET}` : check.level === "warn" ? `${YELLOW}! WARN${RESET}` : `${GREEN}✓ OK ${RESET}`;
1062
+ console.log(` ${badge} ${BOLD}${check.code}${RESET} ${DIM}${short(check.target, 70)}${RESET}`);
1063
+ console.log(` ${check.message}`);
1064
+ if (check.hint)
1065
+ console.log(` ${DIM}${check.hint}${RESET}`);
1066
+ };
1067
+ for (const check of result.checks)
1068
+ paint(check);
1069
+ console.log("");
1070
+ if (result.failures.length) {
1071
+ console.log(`${RED}DO NOT SUBMIT${RESET} — ${result.failures.length} failure${result.failures.length === 1 ? "" : "s"}. A buyer who cannot play it passes on it, and once they have ruled you cannot take it back.`);
1072
+ }
1073
+ else if (result.warnings.length && strict) {
1074
+ console.log(`${YELLOW}HOLD${RESET} — --strict, so ${result.warnings.length} warning${result.warnings.length === 1 ? "" : "s"} count as failures.`);
1075
+ }
1076
+ else if (result.warnings.length) {
1077
+ console.log(`${GREEN}✓ The links load.${RESET} ${DIM}Read the ${result.warnings.length} warning${result.warnings.length === 1 ? "" : "s"} above before you send it.${RESET}`);
1078
+ }
1079
+ else {
1080
+ console.log(`${GREEN}✓ CLEARED${RESET} ${DIM}— every link plays for a stranger and the two halves of the delivery are the right way round.${RESET}`);
1081
+ }
1082
+ }
1083
+ /**
1084
+ * `vidfarm gigs verify-proof` — needs NO api key on purpose. A share-link worker
1085
+ * with no account, and an agent that has not logged in yet, both have to be able
1086
+ * to check a proof before it is sent. Exits 2 on any failure so it chains:
1087
+ * `verify-proof … && gigs submit …`.
1088
+ */
1089
+ export async function cmdVerifyProof(values) {
1090
+ const proofs = [].concat(values.proof ?? []);
1091
+ if (!proofs.length) {
1092
+ throw new Error("gigs verify-proof needs at least one --proof <url>: vidfarm gigs verify-proof --proof https://…/final-watermarked.mp4");
1093
+ }
1094
+ const strict = Boolean(values.strict);
1095
+ const result = await verifyProof({
1096
+ proofs,
1097
+ privateNote: values["private-note"] ? String(values["private-note"]) : undefined,
1098
+ cleanMaster: values["clean-master"] ? String(values["clean-master"]) : undefined,
1099
+ cleanInProof: Boolean(values["clean-in-proof"])
1100
+ }, { probe: values["no-network"] !== true });
1101
+ const blocked = !result.ok || (strict && result.warnings.length > 0);
1102
+ out(Boolean(values.json), { ...result, may_submit: !blocked, strict }, () => printVerify(result, strict));
1103
+ if (blocked)
1104
+ process.exitCode = 2;
1105
+ }
1022
1106
  async function cmdSubmit(auth, values, positionals) {
1023
1107
  const gigId = positionals[0];
1024
1108
  if (!gigId)
@@ -1031,6 +1115,55 @@ async function cmdSubmit(auth, values, positionals) {
1031
1115
  throw new Error("gigs submit needs --task <task-id>. On a queue machine that is the POLLED task's id (never the subject line) — it is what claims the task to you.");
1032
1116
  }
1033
1117
  const tags = values.tags ? String(values.tags).split(",").map((t) => t.trim()).filter(Boolean) : [];
1118
+ // `gigs draft` is this same command with the flag forced on. A draft is stored
1119
+ // and claims its task, but nothing is delivered: no review clock, no webhook,
1120
+ // and the buyer's dashboard and proof list both filter it out.
1121
+ const asDraft = Boolean(values.draft);
1122
+ // THE SANITY CHECK, BEFORE THE BUYER SEES IT. A pending proof CAN be pulled
1123
+ // back (`gigs withdraw`), but only until the buyer rules on it — and the two
1124
+ // ways a finished video loses the payout are both mechanical and both faster
1125
+ // than a rejection: the link does not load for the buyer, or the public half
1126
+ // and the escrowed half were swapped. Both are checkable, so neither is left
1127
+ // to memory. This runs BEFORE the clean-master upload so a broken proof costs
1128
+ // no bytes. A draft delivers nothing, so there it only warns.
1129
+ if (!values["skip-verify"] && !asDraft) {
1130
+ const check = await verifyProof({
1131
+ proofs,
1132
+ privateNote: values["private-note"] ? String(values["private-note"]) : undefined,
1133
+ cleanMaster: values["clean-master"] ? String(values["clean-master"]) : undefined,
1134
+ cleanInProof: Boolean(values["clean-in-proof"])
1135
+ });
1136
+ if (!check.ok) {
1137
+ if (values.json)
1138
+ console.log(JSON.stringify({ ok: false, submitted: false, verify: check }, null, 2));
1139
+ else
1140
+ printVerify(check, false);
1141
+ throw new Error("Refusing to submit: the pre-submit check failed (see above). Fix it, or re-run with --skip-verify if you are certain the buyer can play that url.");
1142
+ }
1143
+ if (check.warnings.length && !values.json) {
1144
+ for (const warning of check.warnings) {
1145
+ console.log(`${YELLOW}!${RESET} ${BOLD}${warning.code}${RESET} ${warning.message}`);
1146
+ if (warning.hint)
1147
+ console.log(` ${DIM}${warning.hint}${RESET}`);
1148
+ }
1149
+ }
1150
+ }
1151
+ else if (asDraft && !values["skip-verify"] && !values.json) {
1152
+ // A draft is the one place a broken link is not a mistake — it is usually a
1153
+ // placeholder for a render that has not finished. So the check still runs
1154
+ // and still prints, but it never refuses. `gigs send` is the gate.
1155
+ const check = await verifyProof({
1156
+ proofs,
1157
+ privateNote: values["private-note"] ? String(values["private-note"]) : undefined,
1158
+ cleanMaster: values["clean-master"] ? String(values["clean-master"]) : undefined,
1159
+ cleanInProof: Boolean(values["clean-in-proof"])
1160
+ });
1161
+ for (const problem of [...check.failures, ...check.warnings]) {
1162
+ console.log(`${YELLOW}!${RESET} ${BOLD}${problem.code}${RESET} ${problem.message}`);
1163
+ }
1164
+ if (check.failures.length)
1165
+ console.log(`${DIM}Draft saved anyway — fix these before ${BOLD}gigs send${RESET}${DIM}, which does refuse.${RESET}`);
1166
+ }
1034
1167
  // The escrowed half of the delivery. `proofs[]` is a PUBLIC playable url — the
1035
1168
  // buyer can download it and so can anyone they forward it to — so the
1036
1169
  // watermarked cut goes there and the clean master goes in `private_note`,
@@ -1054,11 +1187,21 @@ async function cmdSubmit(auth, values, positionals) {
1054
1187
  ...(values.mailbox ? { mailbox_id: String(values.mailbox) } : {}),
1055
1188
  ...(tags.length ? { tags } : {}),
1056
1189
  ...(values.note ? { feedback: String(values.note) } : {}),
1057
- ...(privateNote ? { private_note: privateNote } : {})
1190
+ ...(privateNote ? { private_note: privateNote } : {}),
1191
+ ...(values.ask ? { asking_price: Number(values.ask) } : {}),
1192
+ ...(asDraft ? { draft: true } : {})
1058
1193
  }
1059
1194
  });
1060
1195
  const proof = result.proof ?? result;
1061
1196
  out(Boolean(values.json), result, () => {
1197
+ if (asDraft) {
1198
+ console.log(`${GREEN}✓${RESET} Draft saved on ${BOLD}${gigId}${RESET} ${DIM}${proof.id ?? result.proof_id ?? ""}${RESET}`);
1199
+ console.log(` ${DIM}The buyer sees nothing. No review clock, no webhook, and it counts as no submission.${RESET}`);
1200
+ console.log(` ${DIM}It DOES hold the task against your open-task cap until you send or discard it.${RESET}`);
1201
+ console.log(` ${DIM}Edit it: vidfarm gigs edit-draft ${gigId} ${proof.id ?? "<proof-id>"} --proof <url>${RESET}`);
1202
+ console.log(` ${DIM}Send it: vidfarm gigs send ${gigId} ${proof.id ?? "<proof-id>"} ${RESET}${DIM}— the price is snapshotted then, not now.${RESET}`);
1203
+ return;
1204
+ }
1062
1205
  console.log(`${GREEN}✓${RESET} Submitted to ${BOLD}${gigId}${RESET} ${DIM}${proof.id ?? result.proof_id ?? ""}${RESET}`);
1063
1206
  if (privateNote) {
1064
1207
  // The response field is the ONLY confirmation the note was stored, and a
@@ -1069,14 +1212,151 @@ async function cmdSubmit(auth, values, positionals) {
1069
1212
  }
1070
1213
  else {
1071
1214
  console.log(` ${RED}!${RESET} ${BOLD}private_note_locked was not returned — the note may not have saved.${RESET}`);
1072
- console.log(` ${DIM}You cannot patch a note onto a proof afterwards. Check it now: vidfarm gigs proof ${gigId} ${proof.id ?? "<proof-id>"}${RESET}`);
1215
+ console.log(` ${DIM}A SUBMITTED proof's note cannot be patched. Check it now: vidfarm gigs proof ${gigId} ${proof.id ?? "<proof-id>"}${RESET}`);
1216
+ console.log(` ${DIM}If it is missing, withdraw and re-send: vidfarm gigs withdraw ${gigId} ${proof.id ?? "<proof-id>"}${RESET}`);
1073
1217
  }
1074
1218
  }
1075
1219
  else {
1076
1220
  console.log(` ${DIM}No private note sent. If that url is your CLEAN master, the buyer now has it before paying — see --clean-master.${RESET}`);
1077
1221
  }
1078
1222
  console.log(` ${DIM}The buyer swipes this in a deck — a link that needs a login is a proof they cannot watch.${RESET}`);
1079
- console.log(` ${DIM}A proof cannot be unsent. Agents: only submit a cut the gigworker approved, unless they asked for auto-submit.${RESET}`);
1223
+ console.log(` ${DIM}Wrong cut? vidfarm gigs withdraw ${gigId} ${proof.id ?? "<proof-id>"} takes it back but only until the buyer rules on it.${RESET}`);
1224
+ console.log(` ${DIM}Agents: only submit a cut the gigworker approved, unless they asked for auto-submit. The undo is a repair, not a licence.${RESET}`);
1225
+ });
1226
+ }
1227
+ // ── drafts and the undo ──────────────────────────────────────────────────────
1228
+ //
1229
+ // A submitted proof is NOT one-way. Dollar Platoon models delivery in two
1230
+ // halves, and vidfarm used to expose only the second one:
1231
+ //
1232
+ // draft ──send──▶ pending ──buyer rules──▶ approved / rejected
1233
+ // ▲ │
1234
+ // └────withdraw──────┘ (pending only, and never once a rollup carries it)
1235
+ //
1236
+ // The line the worker actually needs is where the undo STOPS: the buyer's
1237
+ // verdict is theirs, so a rejection cannot be withdrawn — that would erase it
1238
+ // from the reputation record it belongs to. Everything before that is revisable.
1239
+ /** Shared arg shape for the four proof-level draft commands. */
1240
+ function draftTarget(positionals, verb) {
1241
+ const [gigRef, proofId] = positionals;
1242
+ if (!gigRef || !proofId) {
1243
+ throw new Error(`gigs ${verb} needs a gig id and a proof id: vidfarm gigs ${verb} GIG_01H… PRF_01H…`);
1244
+ }
1245
+ return { gigId: parseGigRef(gigRef).gigId, proofId };
1246
+ }
1247
+ /**
1248
+ * `gigs withdraw` — the undo. The web app labels this button "Undo" so it does
1249
+ * not read as a withdrawal of money, and this command is the same endpoint.
1250
+ */
1251
+ async function cmdWithdraw(auth, values, positionals) {
1252
+ const { gigId, proofId } = draftTarget(positionals, "withdraw");
1253
+ const body = await dp(auth, `/gigs/${encodeURIComponent(gigId)}/proofs/${encodeURIComponent(proofId)}/withdraw`, { method: "POST" });
1254
+ out(Boolean(values.json), body, () => {
1255
+ if (body.already_draft) {
1256
+ console.log(`${GREEN}✓${RESET} ${BOLD}${proofId}${RESET} was already a draft — nothing was sent, so there was nothing to take back.`);
1257
+ }
1258
+ else {
1259
+ console.log(`${GREEN}✓${RESET} Withdrew ${BOLD}${proofId}${RESET} back to ${BOLD}draft${RESET}.`);
1260
+ console.log(` ${DIM}The review clock stopped and the buyer can no longer see it.${RESET}`);
1261
+ console.log(` ${DIM}Re-sending restarts that clock from zero — the buyer gets a full window on the new cut.${RESET}`);
1262
+ }
1263
+ console.log(` ${DIM}Fix it: vidfarm gigs edit-draft ${gigId} ${proofId} --proof <url>${RESET}`);
1264
+ console.log(` ${DIM}Send it: vidfarm gigs send ${gigId} ${proofId}${RESET}`);
1265
+ });
1266
+ }
1267
+ /** `gigs edit-draft` — change an unsent draft. Submitted proofs are frozen. */
1268
+ async function cmdEditDraft(auth, values, positionals) {
1269
+ const { gigId, proofId } = draftTarget(positionals, "edit-draft");
1270
+ const proofs = [].concat(values.proof ?? []);
1271
+ const body = {};
1272
+ if (proofs.length)
1273
+ body.proofs = proofs;
1274
+ // An empty string is the documented way to CLEAR either field, so the test is
1275
+ // "was the flag present", never "is the value truthy".
1276
+ if (values["private-note"] !== undefined)
1277
+ body.private_note = String(values["private-note"]) || null;
1278
+ if (values.ask !== undefined)
1279
+ body.asking_price = String(values.ask) === "" ? null : Number(values.ask);
1280
+ if (!Object.keys(body).length) {
1281
+ throw new Error("gigs edit-draft needs something to change: --proof <url>, --private-note \"<text>\", or --ask <usd>. Pass an empty string to clear a note or an ask.");
1282
+ }
1283
+ const result = await dp(auth, `/gigs/${encodeURIComponent(gigId)}/proofs/${encodeURIComponent(proofId)}/draft`, { method: "PATCH", body });
1284
+ out(Boolean(values.json), result, () => {
1285
+ console.log(`${GREEN}✓${RESET} Updated draft ${BOLD}${proofId}${RESET} ${DIM}${(result.updated ?? Object.keys(body)).join(", ")}${RESET}`);
1286
+ console.log(` ${DIM}The task it fulfils cannot be changed. To move it, discard this draft and start again.${RESET}`);
1287
+ console.log(` ${DIM}Send it: vidfarm gigs send ${gigId} ${proofId}${RESET}`);
1288
+ });
1289
+ }
1290
+ /**
1291
+ * `gigs send` — put a draft in front of the buyer. This is the gate the draft
1292
+ * itself is not: it runs the same pre-send check as `gigs submit`, because this
1293
+ * is the call that starts the review clock and fires the buyer's webhook.
1294
+ */
1295
+ async function cmdSendDraft(auth, values, positionals) {
1296
+ const { gigId, proofId } = draftTarget(positionals, "send");
1297
+ if (!values["skip-verify"]) {
1298
+ const current = await dp(auth, `/gigs/${encodeURIComponent(gigId)}/proofs/${encodeURIComponent(proofId)}`);
1299
+ const proof = current.proof ?? current;
1300
+ const links = (Array.isArray(proof.proofs) ? proof.proofs : []).filter((p) => typeof p === "string" && /^https?:\/\//i.test(String(p)));
1301
+ if (links.length) {
1302
+ const check = await verifyProof({ proofs: links, cleanInProof: Boolean(values["clean-in-proof"]) });
1303
+ if (!check.ok) {
1304
+ if (values.json)
1305
+ console.log(JSON.stringify({ ok: false, sent: false, verify: check }, null, 2));
1306
+ else
1307
+ printVerify(check, false);
1308
+ throw new Error("Refusing to send: the draft's links failed the pre-send check (see above). Fix them with gigs edit-draft, or re-run with --skip-verify.");
1309
+ }
1310
+ }
1311
+ }
1312
+ const body = await dp(auth, `/gigs/${encodeURIComponent(gigId)}/proofs/${encodeURIComponent(proofId)}/submit`, { method: "POST" });
1313
+ out(Boolean(values.json), body, () => {
1314
+ console.log(`${GREEN}✓${RESET} Sent ${BOLD}${proofId}${RESET} to ${BOLD}${gigId}${RESET}${body.resubmitted ? ` ${DIM}(re-sent)${RESET}` : ""}`);
1315
+ if (typeof body.locked_price === "number") {
1316
+ // Snapshotted HERE, not when the draft was saved — the buyer may have
1317
+ // repriced the task while the draft sat, so this is the number that pays.
1318
+ console.log(` ${DIM}Locked price: $${body.locked_price.toFixed(2)} — snapshotted now, not when you saved the draft.${RESET}`);
1319
+ }
1320
+ else if (body.locked_price === null) {
1321
+ console.log(` ${DIM}The task is TBD, so the amount is set at approval. That is not $0.${RESET}`);
1322
+ }
1323
+ console.log(` ${DIM}Changed your mind? vidfarm gigs withdraw ${gigId} ${proofId} — until the buyer rules on it.${RESET}`);
1324
+ });
1325
+ }
1326
+ /** `gigs discard` — delete a draft and free its task_identifier. */
1327
+ async function cmdDiscard(auth, values, positionals) {
1328
+ const { gigId, proofId } = draftTarget(positionals, "discard");
1329
+ const body = await dp(auth, `/gigs/${encodeURIComponent(gigId)}/proofs/${encodeURIComponent(proofId)}`, { method: "DELETE" });
1330
+ out(Boolean(values.json), body, () => {
1331
+ console.log(`${GREEN}✓${RESET} Deleted draft ${BOLD}${proofId}${RESET}.`);
1332
+ // The duplicate guard counts drafts, so an abandoned one blocks its own task
1333
+ // for good. Deleting it is what unblocks a fresh proof against that task.
1334
+ console.log(` ${DIM}Its task is free again — you can start a new proof against it.${RESET}`);
1335
+ console.log(` ${DIM}Your claim on the task is untouched. Walk away and task_timeout recycles it.${RESET}`);
1336
+ });
1337
+ }
1338
+ /** `gigs drafts <gig-id>` — the unsent work the buyer cannot see. */
1339
+ async function cmdDrafts(auth, values, positionals) {
1340
+ const gigRef = positionals[0];
1341
+ if (!gigRef)
1342
+ throw new Error("gigs drafts needs a gig id: vidfarm gigs drafts GIG_01H…");
1343
+ const { gigId } = parseGigRef(gigRef);
1344
+ const body = await dp(auth, `/gigs/${encodeURIComponent(gigId)}/proofs`, { query: { status: "draft" } });
1345
+ const rows = body.proofs ?? body.items ?? body.data ?? [];
1346
+ out(Boolean(values.json), body, () => {
1347
+ if (!rows.length) {
1348
+ console.log(`${DIM}No drafts on this machine. Save one: vidfarm gigs draft ${gigId} --task <task-id> --proof <url>${RESET}`);
1349
+ return;
1350
+ }
1351
+ for (const row of rows) {
1352
+ console.log(` ${BOLD}${row.id}${RESET} ${DIM}draft${row.task_identifier ? " · " + row.task_identifier : ""}${RESET}`);
1353
+ const link = proofLink(row);
1354
+ if (link)
1355
+ console.log(` ${DIM}${short(link, 100)}${RESET}`);
1356
+ }
1357
+ console.log("");
1358
+ console.log(`${DIM}Each one holds its task against your open-task cap. Send it, or discard it.${RESET}`);
1359
+ console.log(`${DIM} vidfarm gigs send ${gigId} ${rows[0]?.id ?? "<proof-id>"} · vidfarm gigs discard ${gigId} ${rows[0]?.id ?? "<proof-id>"}${RESET}`);
1080
1360
  });
1081
1361
  }
1082
1362
  /**
@@ -1160,6 +1440,12 @@ function options() {
1160
1440
  note: { type: "string" },
1161
1441
  "private-note": { type: "string" },
1162
1442
  "clean-master": { type: "string" },
1443
+ "clean-in-proof": { type: "boolean" },
1444
+ draft: { type: "boolean" },
1445
+ ask: { type: "string" },
1446
+ "skip-verify": { type: "boolean" },
1447
+ "no-network": { type: "boolean" },
1448
+ strict: { type: "boolean" },
1163
1449
  prefix: { type: "string" }
1164
1450
  };
1165
1451
  }
@@ -1172,6 +1458,11 @@ export async function runGigsCommand(argv) {
1172
1458
  const parsed = parseArgs({ args: argv.slice(1), allowPositionals: true, options: options() });
1173
1459
  const values = parsed.values;
1174
1460
  const rest = parsed.positionals;
1461
+ // The pre-submit check talks to nobody's API — it fetches the worker's own
1462
+ // links the way a stranger would. Dispatch it BEFORE the key is resolved, so a
1463
+ // share-link worker with no account can still check a proof before sending it.
1464
+ if (["verify-proof", "check-proof", "preflight", "verify"].includes(sub))
1465
+ return cmdVerifyProof(values);
1175
1466
  const auth = await resolveAuth(values);
1176
1467
  switch (sub) {
1177
1468
  case "machines":
@@ -1200,6 +1491,18 @@ export async function runGigsCommand(argv) {
1200
1491
  case "claim":
1201
1492
  case "poll": return cmdClaim(auth, values, rest);
1202
1493
  case "submit": return cmdSubmit(auth, values, rest);
1494
+ // `draft` is `submit --draft`, so the flag is forced rather than duplicated.
1495
+ case "draft": return cmdSubmit(auth, { ...values, draft: true }, rest);
1496
+ case "drafts": return cmdDrafts(auth, values, rest);
1497
+ case "edit-draft":
1498
+ case "revise": return cmdEditDraft(auth, values, rest);
1499
+ case "send":
1500
+ case "send-draft": return cmdSendDraft(auth, values, rest);
1501
+ case "withdraw":
1502
+ case "undo":
1503
+ case "unsend": return cmdWithdraw(auth, values, rest);
1504
+ case "discard":
1505
+ case "delete-draft": return cmdDiscard(auth, values, rest);
1203
1506
  case "proof": return cmdProof(auth, values, rest);
1204
1507
  case "upload": return cmdUpload(auth, values, rest);
1205
1508
  case "earnings":