@officexapp/vidfarm-devcli 0.21.54 → 0.21.56
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/.agents/skills/vidfarm/SKILL.md +65 -0
- package/.agents/skills/vidfarm/harnesses/README.md +26 -0
- package/.agents/skills/vidfarm/recipes/bulk-scripting-with-a-harness.md +2 -1
- package/.agents/skills/vidfarm/references/automation-and-local-dev.md +2 -2
- package/SKILL.director.md +69 -3
- package/SKILL.md +76 -4
- package/clipper.md +40 -6
- package/dist/src/cli.js +104 -11
- package/dist/src/devcli/clipper-run.js +941 -0
- package/dist/src/devcli/doctor.js +23 -1
- package/dist/src/devcli/harness.js +60 -15
- package/dist/src/devcli/marketplace-gigs.js +545 -39
- package/dist/src/devcli/skill-docs.js +2 -1
- package/dist/src/devcli/update-check.js +340 -0
- package/experimental/animated-sticker-story.md +2252 -0
- package/experimental/flash-harness.md +549 -0
- package/experimental/google-news-to-video.md +9 -0
- package/experimental/unique-product-explainers.md +8 -0
- package/marketplace.md +549 -14
- package/package.json +6 -2
- package/public/serve-shells/editor.html +27 -0
- package/public/serve-shells/library-files.html +28 -1
- package/public/serve-shells/library-raws.html +27 -0
- package/public/serve-shells/tools-clipper.html +27 -0
- package/public/serve-shells/tools-image.html +27 -0
- package/public/serve-shells/tools-video.html +27 -0
- package/update.md +12 -2
|
@@ -24,6 +24,7 @@ const BOLD = "\x1b[1m";
|
|
|
24
24
|
const DIM = "\x1b[2m";
|
|
25
25
|
const GREEN = "\x1b[32m";
|
|
26
26
|
const YELLOW = "\x1b[33m";
|
|
27
|
+
const RED = "\x1b[31m";
|
|
27
28
|
const RESET = "\x1b[0m";
|
|
28
29
|
/** The shared vidfarm feed: the registry + notification stream every client's machines are listed in. */
|
|
29
30
|
const VIDFARM_FEED_ID = "FEED_01M0BCTVTKSDFZQVEY1RXA4NJK";
|
|
@@ -58,25 +59,56 @@ CLIENT (paid vidfarm plan — the key is read from your vidfarm account)
|
|
|
58
59
|
gigs ring-bell --title "<t>" [--subtext "<s>"] [--machine inbound_offers]
|
|
59
60
|
Broadcast to every agent on the vidfarm feed
|
|
60
61
|
|
|
61
|
-
GIGWORKER (free — bring your own key: export DOLLARPLATOON_API_KEY=…)
|
|
62
|
-
gigs
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
62
|
+
GIGWORKER — "help me earn on vidfarm" (free — bring your own key: export DOLLARPLATOON_API_KEY=…)
|
|
63
|
+
gigs earn START HERE. The whole first session in one command:
|
|
64
|
+
joins the feed if needed, reads who is asking for videos
|
|
65
|
+
RIGHT NOW, checks every one of those machines for MONEY,
|
|
66
|
+
and prints them best-payer-first with the join command.
|
|
67
|
+
--limit <n> --name "<you>" (alias: gigs start)
|
|
68
|
+
|
|
69
|
+
the same four moves, one at a time:
|
|
70
|
+
gigs feed ① Who is asking for videos right now (notifications)
|
|
71
|
+
--funded …and whether each one can actually pay
|
|
72
|
+
--registry Every machine listed in the feed, not just today's bells
|
|
73
|
+
gigs check <gig-id|join-url> ② CAN THIS MACHINE PAY ME? available_funds + a verdict.
|
|
74
|
+
--price <usd> Score against the TASK price, not the gig default
|
|
75
|
+
Do this BEFORE you commit an hour. (alias: gigs funds)
|
|
76
|
+
gigs join <gig-id|invite-url> ③ Join it. --wallet 0x… --invite <token> --name "<you>"
|
|
77
|
+
SET --wallet ON THE FIRST JOIN: a rollup pays the address
|
|
78
|
+
snapshotted when it was created.
|
|
79
|
+
gigs work ④ Work waiting across every machine you joined (paged)
|
|
80
|
+
gigs claim <gig-id> [--count 1] Claim task(s) off a FIFO queue machine. ONE by default.
|
|
81
|
+
Inbound Offers hands out NO tasks — you pitch, unprompted.
|
|
82
|
+
|
|
83
|
+
delivering, and getting paid:
|
|
84
|
+
gigs upload <file> [--prefix p] Put a file on DollarPlatoon's own s3, get a durable url
|
|
69
85
|
gigs submit <gig-id> --task <task-id> --proof <url> [--proof <url>…]
|
|
70
|
-
--
|
|
86
|
+
--clean-master <file> Upload it and seal it in the private note. THE WATERMARKED
|
|
87
|
+
cut goes in --proof; the clean master goes here, and the
|
|
88
|
+
buyer cannot read it until a rollup has actually paid you.
|
|
89
|
+
--private-note "<text>" Anything else that follows the master (ProRes, project
|
|
90
|
+
files, music licence id). 8000 chars, cannot be added later.
|
|
91
|
+
--mailbox <id> --tags a,b --note "<text>" (--note is the PUBLIC feedback field)
|
|
92
|
+
Watch the cut and approve it BEFORE this call — a rejection scores on your
|
|
93
|
+
wallet and clients read it as a trust score on your public profile.
|
|
94
|
+
gigs proof <gig-id> <proof-id> Read your own proof back: paid_out_at + the private note
|
|
95
|
+
gigs earnings Your rollups + totals. (aliases: rollups, paid)
|
|
71
96
|
gigs mine Your mailboxes across every gig
|
|
97
|
+
gigs join-feed [--name "<you>"] Join the shared vidfarm feed (gigs earn does this for you)
|
|
72
98
|
|
|
73
|
-
Common: --dp-key <key> · --json · --host <vidfarm url> (for the key lookup)
|
|
99
|
+
Common: --dp-key <key> · --json · --limit <n> · --host <vidfarm url> (for the key lookup)
|
|
74
100
|
|
|
75
|
-
e.g. vidfarm gigs
|
|
101
|
+
e.g. DOLLARPLATOON_API_KEY=… vidfarm gigs earn
|
|
102
|
+
vidfarm gigs check "https://dollarplatoon.com/gig/GIG_01H…/join?invite=abc"
|
|
103
|
+
vidfarm gigs submit GIG_01H… --task TASK_01H… \\
|
|
104
|
+
--proof https://…/final-watermarked.mp4 --clean-master ./final-clean.mp4
|
|
105
|
+
vidfarm gigs add-task --task "60s explainer for acme.com" --price 0.25 \\
|
|
76
106
|
--upload-link "https://vidfarm.cc/directory/preview/dsh_…/files/acme"
|
|
77
107
|
vidfarm gigs proofs --status pending
|
|
78
108
|
vidfarm gigs approve PRF_01H… --feedback "Great hook, keeping it."
|
|
79
|
-
|
|
109
|
+
|
|
110
|
+
Building the video is a separate job: https://vidfarm.cc/agentic-clipper.md is the loop,
|
|
111
|
+
and https://vidfarm.cc/flash-harness.md is how to build it on a weak model.`;
|
|
80
112
|
/**
|
|
81
113
|
* "You have no key yet, here is how to get one" — the single most common first
|
|
82
114
|
* run for a gigworker, and a deliberate instruction rather than a bug. cli.ts
|
|
@@ -193,6 +225,130 @@ async function dp(auth, path, init = {}) {
|
|
|
193
225
|
}
|
|
194
226
|
return json;
|
|
195
227
|
}
|
|
228
|
+
/**
|
|
229
|
+
* Every list route here is cursored, and a FILTERED page can come back empty
|
|
230
|
+
* while later pages still hold work — `/work/available?only_with_work=true` is
|
|
231
|
+
* the one that bites. Reading page one only is how a clipper gets told "nothing
|
|
232
|
+
* waiting" over a feed full of paid work. Page to the end, with a hard stop so a
|
|
233
|
+
* server that never nulls its cursor cannot spin the loop forever.
|
|
234
|
+
*/
|
|
235
|
+
async function pageAll(auth, path, pick, query = {}, maxPages = 20) {
|
|
236
|
+
const rows = [];
|
|
237
|
+
let cursor;
|
|
238
|
+
for (let page = 0; page < maxPages; page++) {
|
|
239
|
+
const body = await dp(auth, path, { query: { ...query, cursor } });
|
|
240
|
+
rows.push(...pick(body));
|
|
241
|
+
const next = body.next_cursor;
|
|
242
|
+
cursor = typeof next === "string" && next ? next : undefined;
|
|
243
|
+
if (!cursor)
|
|
244
|
+
return { rows, truncated: false };
|
|
245
|
+
}
|
|
246
|
+
return { rows, truncated: true };
|
|
247
|
+
}
|
|
248
|
+
/** Run `work` with at most `limit` in flight — these are N gig reads per loop. */
|
|
249
|
+
async function mapLimit(items, limit, work) {
|
|
250
|
+
const results = new Array(items.length);
|
|
251
|
+
let next = 0;
|
|
252
|
+
const runners = Array.from({ length: Math.min(limit, items.length) }, async () => {
|
|
253
|
+
for (let i = next++; i < items.length; i = next++)
|
|
254
|
+
results[i] = await work(items[i]);
|
|
255
|
+
});
|
|
256
|
+
await Promise.all(runners);
|
|
257
|
+
return results;
|
|
258
|
+
}
|
|
259
|
+
// ── funding — the check that decides whether work gets paid ──────────────────
|
|
260
|
+
/**
|
|
261
|
+
* `GET /gigs/:id` has been seen answering both `{ gig: {...} }` and a flat
|
|
262
|
+
* object — cmdAddTask already reads `webhook` both ways. Unwrap defensively,
|
|
263
|
+
* because a funds check that silently reads `null` is worse than none: it turns
|
|
264
|
+
* an EMPTY machine into an "unknown" one, and a clipper works unknowns.
|
|
265
|
+
*/
|
|
266
|
+
function unwrapGig(body) {
|
|
267
|
+
return body?.gig && typeof body.gig === "object" ? body.gig : (body ?? {});
|
|
268
|
+
}
|
|
269
|
+
function num(value) {
|
|
270
|
+
const parsed = typeof value === "string" ? Number(value) : typeof value === "number" ? value : NaN;
|
|
271
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
272
|
+
}
|
|
273
|
+
async function readGigFunds(auth, gigId) {
|
|
274
|
+
const gig = unwrapGig(await dp(auth, `/gigs/${encodeURIComponent(gigId)}`));
|
|
275
|
+
return {
|
|
276
|
+
gigId,
|
|
277
|
+
title: String(gig.title ?? gigId),
|
|
278
|
+
available_funds: num(gig.available_funds),
|
|
279
|
+
reserved_funds: num(gig.reserved_funds),
|
|
280
|
+
price: num(gig.price),
|
|
281
|
+
review_timeout: gig.review_timeout ?? null,
|
|
282
|
+
distribution: gig.distribution ? String(gig.distribution) : null
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* A $1.00 payout draws $1.10 out of the machine — the 10% platform fee is
|
|
287
|
+
* charged on top of the gig balance, not deducted from the worker. So a gig
|
|
288
|
+
* holding $5.00 pays four $1.00 videos, not five, and "can it pay me" has to be
|
|
289
|
+
* asked against price × 1.1.
|
|
290
|
+
*/
|
|
291
|
+
const PLATFORM_FEE_MULTIPLIER = 1.1;
|
|
292
|
+
const VERDICT_LABEL = {
|
|
293
|
+
empty: "SKIP",
|
|
294
|
+
partial: "PARTIAL",
|
|
295
|
+
thin: "THIN",
|
|
296
|
+
healthy: "HEALTHY",
|
|
297
|
+
price_tbd: "PRICE TBD",
|
|
298
|
+
unknown: "UNKNOWN"
|
|
299
|
+
};
|
|
300
|
+
/** Same thresholds as https://vidfarm.cc/agentic-clipper.md § ④. Keep them in sync. */
|
|
301
|
+
function fundsVerdict(funds, taskPrice) {
|
|
302
|
+
const available = funds.available_funds;
|
|
303
|
+
const price = taskPrice ?? funds.price;
|
|
304
|
+
if (available === null) {
|
|
305
|
+
return { verdict: "unknown", pays: null, note: "the gig did not report available_funds — check it on dollarplatoon.com before you commit an hour" };
|
|
306
|
+
}
|
|
307
|
+
if (available <= 0) {
|
|
308
|
+
return { verdict: "empty", pays: 0, note: "empty coin box. Approval does not create money — do not work this gig yet" };
|
|
309
|
+
}
|
|
310
|
+
if (price === null || price <= 0) {
|
|
311
|
+
return { verdict: "price_tbd", pays: null, note: `$${available.toFixed(2)} in the machine, but the price is TBD — the client names the amount at approval` };
|
|
312
|
+
}
|
|
313
|
+
const pays = Math.floor(available / (price * PLATFORM_FEE_MULTIPLIER));
|
|
314
|
+
if (pays < 1) {
|
|
315
|
+
return { verdict: "partial", pays, note: `$${available.toFixed(2)} cannot cover one $${price.toFixed(2)} payout + the 10% fee. Submitting still succeeds — that is not a promise of payment` };
|
|
316
|
+
}
|
|
317
|
+
if (pays >= 3) {
|
|
318
|
+
return { verdict: "healthy", pays, note: `$${available.toFixed(2)} available — about ${pays} payouts at $${price.toFixed(2)} after the 10% fee` };
|
|
319
|
+
}
|
|
320
|
+
return { verdict: "thin", pays, note: `$${available.toFixed(2)} available — only about ${pays} payout(s) at $${price.toFixed(2)}. Someone else may take it first` };
|
|
321
|
+
}
|
|
322
|
+
function paintVerdict(verdict) {
|
|
323
|
+
const label = VERDICT_LABEL[verdict];
|
|
324
|
+
if (verdict === "healthy")
|
|
325
|
+
return `${GREEN}${label}${RESET}`;
|
|
326
|
+
if (verdict === "empty" || verdict === "partial")
|
|
327
|
+
return `${RED}${label}${RESET}`;
|
|
328
|
+
return `${YELLOW}${label}${RESET}`;
|
|
329
|
+
}
|
|
330
|
+
/** Rank for the shortlist: the machine most likely to actually pay comes first. */
|
|
331
|
+
const VERDICT_RANK = {
|
|
332
|
+
healthy: 0, thin: 1, price_tbd: 2, unknown: 3, partial: 4, empty: 5
|
|
333
|
+
};
|
|
334
|
+
/**
|
|
335
|
+
* A gig id, or a join / invite URL pasted straight out of a notification's
|
|
336
|
+
* `destination_url`. Both halves come out of the URL, which is the whole point:
|
|
337
|
+
* a clipper should never have to hand-parse a link to check a machine.
|
|
338
|
+
*/
|
|
339
|
+
function parseGigRef(raw, fallbackInvite = "") {
|
|
340
|
+
const value = (raw ?? "").trim();
|
|
341
|
+
if (!/^https?:\/\//i.test(value))
|
|
342
|
+
return { gigId: value, invite: fallbackInvite };
|
|
343
|
+
try {
|
|
344
|
+
const url = new URL(value);
|
|
345
|
+
const found = url.pathname.split("/").find((segment) => /^GIG_/i.test(segment));
|
|
346
|
+
return { gigId: found ?? value, invite: fallbackInvite || url.searchParams.get("invite") || "" };
|
|
347
|
+
}
|
|
348
|
+
catch {
|
|
349
|
+
return { gigId: value, invite: fallbackInvite };
|
|
350
|
+
}
|
|
351
|
+
}
|
|
196
352
|
// ── machine resolution ───────────────────────────────────────────────────────
|
|
197
353
|
const MACHINE_ALIASES = {
|
|
198
354
|
custom: "custom_requests",
|
|
@@ -479,21 +635,262 @@ async function cmdRingBell(auth, values) {
|
|
|
479
635
|
}
|
|
480
636
|
// ── gigworker commands ───────────────────────────────────────────────────────
|
|
481
637
|
async function cmdWork(auth, values) {
|
|
482
|
-
const body = await dp(auth, "/work/available", { query: { only_with_work: "true", limit: values.limit } });
|
|
483
638
|
// `/work/available` answers `{ items: [...] }` — reading `mailboxes` first
|
|
484
|
-
// printed "nothing waiting" over a page full of paid work.
|
|
485
|
-
|
|
486
|
-
|
|
639
|
+
// printed "nothing waiting" over a page full of paid work. And it MUST be
|
|
640
|
+
// paged: with only_with_work=true the filter runs inside a page, so several
|
|
641
|
+
// pages in a row can be empty while later ones hold paid work.
|
|
642
|
+
const { rows, truncated } = await pageAll(auth, "/work/available", (body) => body.items ?? body.mailboxes ?? body.work ?? body.data ?? [], { only_with_work: "true", limit: values.limit ?? "100" });
|
|
643
|
+
out(Boolean(values.json), { items: rows, truncated }, () => {
|
|
487
644
|
if (!rows.length) {
|
|
488
|
-
console.log(`${DIM}Nothing waiting in the machines you joined. Find new clients: vidfarm gigs
|
|
645
|
+
console.log(`${DIM}Nothing waiting in the machines you joined. Find new clients: vidfarm gigs earn${RESET}`);
|
|
489
646
|
return;
|
|
490
647
|
}
|
|
491
648
|
for (const row of rows) {
|
|
492
|
-
const
|
|
493
|
-
|
|
649
|
+
const price = num(row.price);
|
|
650
|
+
// `tasks_in_mailbox` = already delivered to you. `poll_in_gig` = unclaimed
|
|
651
|
+
// work sitting in the shared queue. `poll_in_gig: false` is definitive.
|
|
652
|
+
const state = row.tasks_in_mailbox ? "in your mailbox" : row.poll_in_gig ? "claimable — poll it" : "nothing to poll";
|
|
653
|
+
console.log(` ${BOLD}${short(row.gig_title ?? row.gig_id, 55)}${RESET} ${DIM}${row.gig_id ?? ""}${RESET}`);
|
|
654
|
+
console.log(` ${DIM}${state}${price === null ? " · price TBD" : ` · $${price.toFixed(2)}/video`}${RESET}`);
|
|
655
|
+
}
|
|
656
|
+
console.log("");
|
|
657
|
+
if (truncated)
|
|
658
|
+
console.log(`${YELLOW}More pages exist${RESET}${DIM} — raise --limit or page the API directly.${RESET}`);
|
|
659
|
+
console.log(`${DIM}The gig price is only a DEFAULT — each task can carry its own, and price TBD means the client names it at approval.${RESET}`);
|
|
660
|
+
console.log(`${DIM}Check it can pay: vidfarm gigs check <gig-id> · Claim one: vidfarm gigs claim <gig-id>${RESET}`);
|
|
661
|
+
});
|
|
662
|
+
}
|
|
663
|
+
/**
|
|
664
|
+
* MOVE 2 of the earning loop — "can this machine actually pay me?"
|
|
665
|
+
*
|
|
666
|
+
* This is the check that decides whether an hour of editing turns into money,
|
|
667
|
+
* and it was the one step of https://vidfarm.cc/marketplace.md with no CLI verb
|
|
668
|
+
* behind it. Takes a gig id OR a join link pasted straight out of a
|
|
669
|
+
* notification, so nothing has to be hand-parsed.
|
|
670
|
+
*/
|
|
671
|
+
async function cmdCheck(auth, values, positionals) {
|
|
672
|
+
const raw = positionals[0];
|
|
673
|
+
if (!raw)
|
|
674
|
+
throw new Error("gigs check needs a gig id or a join link: vidfarm gigs check GIG_01H… (or paste the notification's URL)");
|
|
675
|
+
const { gigId, invite } = parseGigRef(raw);
|
|
676
|
+
const funds = await readGigFunds(auth, gigId);
|
|
677
|
+
const taskPrice = values.price === undefined ? null : num(values.price);
|
|
678
|
+
const verdict = fundsVerdict(funds, taskPrice);
|
|
679
|
+
out(Boolean(values.json), { ...funds, verdict: verdict.verdict, pays_about: verdict.pays, note: verdict.note }, () => {
|
|
680
|
+
console.log(` ${BOLD}${funds.title}${RESET} ${DIM}${funds.gigId}${funds.distribution ? " · " + funds.distribution : ""}${RESET}`);
|
|
681
|
+
console.log(` ${paintVerdict(verdict.verdict)} ${verdict.note}`);
|
|
682
|
+
const reserved = funds.reserved_funds;
|
|
683
|
+
if (reserved !== null && reserved > 0) {
|
|
684
|
+
console.log(` ${DIM}$${reserved.toFixed(2)} is RESERVED for proofs ahead of you. You are paid from available_funds, not the sum.${RESET}`);
|
|
685
|
+
}
|
|
686
|
+
// A negative timeout is "never" — Inbound Offers machines do not auto-approve,
|
|
687
|
+
// so printing the raw -1 reads like a broken field.
|
|
688
|
+
const timeout = num(funds.review_timeout);
|
|
689
|
+
if (timeout !== null) {
|
|
690
|
+
console.log(` ${DIM}review: ${timeout < 0 ? "no auto-approval — the buyer must act" : `auto-approves after ${timeout}${RESET}`}${RESET}`);
|
|
691
|
+
}
|
|
692
|
+
console.log("");
|
|
693
|
+
if (verdict.verdict === "empty" || verdict.verdict === "partial") {
|
|
694
|
+
console.log(`${DIM}Funds are per gig and cannot move between gigs — a client's other machine tells you nothing about this one.${RESET}`);
|
|
695
|
+
}
|
|
696
|
+
else if (invite) {
|
|
697
|
+
console.log(`${DIM}Join it: vidfarm gigs join "${raw}" --wallet 0xYOUR_BASE_ADDRESS${RESET}`);
|
|
698
|
+
}
|
|
699
|
+
else {
|
|
700
|
+
console.log(`${DIM}Join it: vidfarm gigs join ${funds.gigId} --invite <token> --wallet 0xYOUR_BASE_ADDRESS${RESET}`);
|
|
701
|
+
console.log(`${DIM}No token to hand? An invite-only gig refuses a tokenless join — find the tokened link: vidfarm gigs feed --registry${RESET}`);
|
|
702
|
+
}
|
|
703
|
+
});
|
|
704
|
+
}
|
|
705
|
+
/**
|
|
706
|
+
* "help me earn on vidfarm" in one command — moves 1 and 2 of
|
|
707
|
+
* https://vidfarm.cc/marketplace.md, then a ranked shortlist.
|
|
708
|
+
*
|
|
709
|
+
* Reads the notification feed for who is asking for videos RIGHT NOW, digs the
|
|
710
|
+
* gig id out of each bell's join link, checks every one of them for money, and
|
|
711
|
+
* prints them best-payer-first with the exact join command. Joins the feed for
|
|
712
|
+
* you if you are not on it — a non-member 404s on every feed route, which reads
|
|
713
|
+
* like an empty feed rather than a missing membership.
|
|
714
|
+
*/
|
|
715
|
+
async function cmdEarn(auth, values) {
|
|
716
|
+
const limit = values.limit ?? "50";
|
|
717
|
+
const json = Boolean(values.json);
|
|
718
|
+
const notificationsPath = `/feeds/${VIDFARM_FEED_ID}/notifications`;
|
|
719
|
+
const pickNotifications = (body) => body.notifications ?? body.items ?? body.data ?? [];
|
|
720
|
+
let notifications;
|
|
721
|
+
try {
|
|
722
|
+
notifications = (await pageAll(auth, notificationsPath, pickNotifications, { limit })).rows;
|
|
723
|
+
}
|
|
724
|
+
catch (error) {
|
|
725
|
+
if (!(error instanceof Error) || !/40[134]/.test(error.message))
|
|
726
|
+
throw error;
|
|
727
|
+
if (!json)
|
|
728
|
+
console.log(`${DIM}Not on the vidfarm feed yet — joining it…${RESET}`);
|
|
729
|
+
await dp(auth, `/feeds/${VIDFARM_FEED_ID}/join`, {
|
|
730
|
+
method: "POST",
|
|
731
|
+
body: { invite: VIDFARM_FEED_READ_INVITE, ...(values.name ? { display_name: String(values.name) } : {}) }
|
|
732
|
+
});
|
|
733
|
+
notifications = (await pageAll(auth, notificationsPath, pickNotifications, { limit })).rows;
|
|
734
|
+
}
|
|
735
|
+
// One row per MACHINE, newest bell wins. A client who rang three times this
|
|
736
|
+
// week is one opportunity, not three.
|
|
737
|
+
const byGig = new Map();
|
|
738
|
+
const unjoinable = [];
|
|
739
|
+
for (const row of notifications) {
|
|
740
|
+
const url = String(row.destination_url ?? row.invite_url ?? "");
|
|
741
|
+
const { gigId, invite } = parseGigRef(url);
|
|
742
|
+
if (!/^GIG_/i.test(gigId))
|
|
743
|
+
continue;
|
|
744
|
+
if (!invite)
|
|
745
|
+
unjoinable.push({ gigId, title: row.title });
|
|
746
|
+
if (byGig.has(gigId))
|
|
747
|
+
continue;
|
|
748
|
+
byGig.set(gigId, {
|
|
749
|
+
gigId,
|
|
750
|
+
invite,
|
|
751
|
+
title: String(row.title ?? gigId),
|
|
752
|
+
subtext: String(row.subtext ?? ""),
|
|
753
|
+
created_at: String(row.created_at ?? ""),
|
|
754
|
+
url
|
|
755
|
+
});
|
|
756
|
+
}
|
|
757
|
+
const candidates = [...byGig.values()];
|
|
758
|
+
const scored = await mapLimit(candidates, 5, async (candidate) => {
|
|
759
|
+
try {
|
|
760
|
+
const funds = await readGigFunds(auth, candidate.gigId);
|
|
761
|
+
const verdict = fundsVerdict(funds);
|
|
762
|
+
return { ...candidate, funds, ...verdict };
|
|
763
|
+
}
|
|
764
|
+
catch {
|
|
765
|
+
// A gig you cannot read yet is not a dead gig — most commonly you are not
|
|
766
|
+
// a member. Keep it on the list as unknown rather than dropping it.
|
|
767
|
+
const funds = { gigId: candidate.gigId, title: candidate.title, available_funds: null, reserved_funds: null, price: null, review_timeout: null, distribution: null };
|
|
768
|
+
return { ...candidate, funds, verdict: "unknown", pays: null, note: "could not read this gig — you may need to join it first" };
|
|
769
|
+
}
|
|
770
|
+
});
|
|
771
|
+
scored.sort((a, b) => VERDICT_RANK[a.verdict] - VERDICT_RANK[b.verdict]);
|
|
772
|
+
const joined = await pageAll(auth, "/work/available", (body) => body.items ?? body.mailboxes ?? body.work ?? body.data ?? [], { only_with_work: "true", limit: "100" }).catch(() => ({ rows: [], truncated: false }));
|
|
773
|
+
out(json, { opportunities: scored, work_waiting: joined.rows }, () => {
|
|
774
|
+
console.log(`${BOLD}① Who is asking for videos right now${RESET} ${DIM}(${notifications.length} bells, ${scored.length} machines)${RESET}`);
|
|
775
|
+
if (!scored.length) {
|
|
776
|
+
console.log(` ${DIM}No bells on the feed yet. The registry lists every machine that exists: vidfarm gigs feed --registry${RESET}`);
|
|
777
|
+
}
|
|
778
|
+
console.log("");
|
|
779
|
+
console.log(`${BOLD}② Which of them can actually pay${RESET}`);
|
|
780
|
+
for (const row of scored) {
|
|
781
|
+
console.log(` ${paintVerdict(row.verdict)} ${BOLD}${short(row.title, 58)}${RESET} ${DIM}${row.gigId}${RESET}`);
|
|
782
|
+
console.log(` ${DIM}${row.note}${RESET}`);
|
|
783
|
+
if (row.subtext)
|
|
784
|
+
console.log(` ${DIM}${short(row.subtext, 100)}${RESET}`);
|
|
785
|
+
if (row.verdict !== "empty" && row.verdict !== "partial") {
|
|
786
|
+
// No token means the bare join URL is a dead end — an invite-only gig
|
|
787
|
+
// refuses a tokenless join — so never print a command that cannot work.
|
|
788
|
+
console.log(row.invite
|
|
789
|
+
? ` ${DIM}→ vidfarm gigs join ${row.url} --wallet 0xYOUR_BASE_ADDRESS${RESET}`
|
|
790
|
+
: ` ${YELLOW}→ no ?invite= token on that bell${RESET}${DIM} — get the tokened link: vidfarm gigs feed --registry${RESET}`);
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
if (unjoinable.length) {
|
|
794
|
+
console.log("");
|
|
795
|
+
console.log(`${YELLOW}${unjoinable.length} bell(s) carry a link with no ?invite= token${RESET}${DIM} — an old build rang those. Find the gig in the registry, where the invite_url IS tokened: vidfarm gigs feed --registry${RESET}`);
|
|
796
|
+
}
|
|
797
|
+
console.log("");
|
|
798
|
+
console.log(`${BOLD}③ Work already waiting in machines you joined${RESET}`);
|
|
799
|
+
if (!joined.rows.length) {
|
|
800
|
+
console.log(` ${DIM}None. Join a funded machine above first.${RESET}`);
|
|
801
|
+
}
|
|
802
|
+
else {
|
|
803
|
+
for (const row of joined.rows) {
|
|
804
|
+
const price = num(row.price);
|
|
805
|
+
console.log(` ${BOLD}${short(row.gig_title ?? row.gig_id, 55)}${RESET} ${DIM}${row.gig_id ?? ""}${price === null ? " · price TBD" : ` · $${price.toFixed(2)}`}${row.poll_in_gig ? " · claimable" : ""}${RESET}`);
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
console.log("");
|
|
809
|
+
console.log(`${DIM}Then: vidfarm gigs claim <gig-id> → build it → vidfarm gigs submit <gig-id> --task <task-id> --proof <url>${RESET}`);
|
|
810
|
+
console.log(`${DIM}Inbound Offers machines hand out NO tasks — you pitch a finished video unprompted, and a pass costs you no reputation.${RESET}`);
|
|
811
|
+
console.log(`${DIM}Weak model? Build from a decomposed template: https://vidfarm.cc/flash-harness.md${RESET}`);
|
|
812
|
+
});
|
|
813
|
+
}
|
|
814
|
+
/**
|
|
815
|
+
* "have I been paid?" — `approved` is not `paid`. A rollup that reads `failed`
|
|
816
|
+
* means NOT SETTLED YET, never lost: the cron retries the same rollup, checking
|
|
817
|
+
* the chain first, until it settles. Never ask for one to be re-sent.
|
|
818
|
+
*/
|
|
819
|
+
async function cmdEarnings(auth, values) {
|
|
820
|
+
const { rows } = await pageAll(auth, "/rollups/mine", (body) => body.rollups ?? body.items ?? body.data ?? [], { limit: values.limit ?? "100" });
|
|
821
|
+
const totals = new Map();
|
|
822
|
+
for (const row of rows) {
|
|
823
|
+
const status = String(row.status ?? "unknown");
|
|
824
|
+
const entry = totals.get(status) ?? { count: 0, amount: 0 };
|
|
825
|
+
entry.count += 1;
|
|
826
|
+
entry.amount += num(row.net_amount) ?? 0;
|
|
827
|
+
totals.set(status, entry);
|
|
828
|
+
}
|
|
829
|
+
out(Boolean(values.json), { rollups: rows, totals: Object.fromEntries(totals) }, () => {
|
|
830
|
+
if (!rows.length) {
|
|
831
|
+
console.log(`${DIM}No rollups yet. Submit a proof first: vidfarm gigs earn${RESET}`);
|
|
832
|
+
return;
|
|
833
|
+
}
|
|
834
|
+
for (const row of rows.slice(0, 25)) {
|
|
835
|
+
const amount = num(row.net_amount);
|
|
836
|
+
const settled = row.status === "settled" || row.status === "paid" || Boolean(row.tx_hash);
|
|
837
|
+
console.log(` ${settled ? GREEN + "✓" : YELLOW + "·"}${RESET} ${BOLD}${amount === null ? "?" : "$" + amount.toFixed(2)}${RESET} ${DIM}${row.status ?? ""}${row.tx_hash ? " · " + short(row.tx_hash, 22) : ""}${RESET}`);
|
|
838
|
+
}
|
|
839
|
+
console.log("");
|
|
840
|
+
for (const [status, entry] of totals) {
|
|
841
|
+
console.log(` ${BOLD}${status}${RESET} ${DIM}${entry.count} rollup(s) · $${entry.amount.toFixed(2)}${RESET}`);
|
|
494
842
|
}
|
|
495
843
|
console.log("");
|
|
496
|
-
console.log(`${DIM}
|
|
844
|
+
console.log(`${DIM}A 'failed' rollup means NOT SETTLED YET, never lost — the cron retries the same one until it settles. Never ask for a re-send.${RESET}`);
|
|
845
|
+
console.log(`${DIM}Per proof, the only field that means the USDC moved is paid_out_at — approved is not paid.${RESET}`);
|
|
846
|
+
});
|
|
847
|
+
}
|
|
848
|
+
/** Guess a content type so the presigned PUT is not stored as octet-stream. */
|
|
849
|
+
function guessContentType(filename) {
|
|
850
|
+
const ext = filename.toLowerCase().split(".").pop() ?? "";
|
|
851
|
+
const map = {
|
|
852
|
+
mp4: "video/mp4", mov: "video/quicktime", webm: "video/webm", m4v: "video/x-m4v",
|
|
853
|
+
mp3: "audio/mpeg", wav: "audio/wav", m4a: "audio/mp4",
|
|
854
|
+
png: "image/png", jpg: "image/jpeg", jpeg: "image/jpeg", webp: "image/webp", gif: "image/gif",
|
|
855
|
+
zip: "application/zip", json: "application/json", md: "text/markdown", txt: "text/plain"
|
|
856
|
+
};
|
|
857
|
+
return map[ext] ?? "application/octet-stream";
|
|
858
|
+
}
|
|
859
|
+
/**
|
|
860
|
+
* Put a file on Dollar Platoon's OWN s3 and return the durable url.
|
|
861
|
+
*
|
|
862
|
+
* This is where the clean master goes. It has to be their bucket, not a link you
|
|
863
|
+
* host: the private note presigns that url at read time and the key it hands you
|
|
864
|
+
* is random, which is exactly what keeps the master unreachable while the note is
|
|
865
|
+
* still locked. The presigned PUT expires in an hour, so presign immediately
|
|
866
|
+
* before pushing the bytes.
|
|
867
|
+
*/
|
|
868
|
+
async function uploadToDollarPlatoon(auth, file, prefix = "proofs") {
|
|
869
|
+
const bytes = readFileSync(file);
|
|
870
|
+
const filename = file.split(/[\\/]/).pop() || "upload.bin";
|
|
871
|
+
const contentType = guessContentType(filename);
|
|
872
|
+
const presign = await dp(auth, "/upload/presign", {
|
|
873
|
+
method: "POST",
|
|
874
|
+
body: { filename, content_type: contentType, prefix }
|
|
875
|
+
});
|
|
876
|
+
const putUrl = presign.presigned_url ?? presign.url_put ?? "";
|
|
877
|
+
const publicUrl = presign.url ?? presign.public_url ?? "";
|
|
878
|
+
if (!putUrl || !publicUrl)
|
|
879
|
+
throw new Error("upload presign did not return both presigned_url and url — check the key and the host.");
|
|
880
|
+
const put = await fetch(putUrl, { method: "PUT", headers: { "content-type": contentType }, body: new Uint8Array(bytes) });
|
|
881
|
+
if (!put.ok)
|
|
882
|
+
throw new Error(`upload PUT failed: HTTP ${put.status}`);
|
|
883
|
+
return String(publicUrl);
|
|
884
|
+
}
|
|
885
|
+
async function cmdUpload(auth, values, positionals) {
|
|
886
|
+
const file = positionals[0];
|
|
887
|
+
if (!file)
|
|
888
|
+
throw new Error("gigs upload needs a file: vidfarm gigs upload ./final-clean.mp4");
|
|
889
|
+
const url = await uploadToDollarPlatoon(auth, file, values.prefix ?? "proofs");
|
|
890
|
+
out(Boolean(values.json), { url }, () => {
|
|
891
|
+
console.log(`${GREEN}✓${RESET} Uploaded ${BOLD}${file}${RESET}`);
|
|
892
|
+
console.log(` ${url}`);
|
|
893
|
+
console.log(` ${DIM}Put THIS in the proof's private note, not in proofs[]: vidfarm gigs submit <gig> --task <id> --proof <watermarked url> --private-note "Clean master: ${short(url, 40)}"${RESET}`);
|
|
497
894
|
});
|
|
498
895
|
}
|
|
499
896
|
async function cmdFeed(auth, values) {
|
|
@@ -513,21 +910,44 @@ async function cmdFeed(auth, values) {
|
|
|
513
910
|
// only one key gives a silent zero on the other route — the exact bug that had
|
|
514
911
|
// `--registry` printing "Nothing on the feed right now" over a full page.
|
|
515
912
|
const rows = body.notifications ?? body.items ?? body.entries ?? body.registry ?? body.data ?? [];
|
|
913
|
+
// `--funded` answers the question the feed alone cannot: which of these
|
|
914
|
+
// machines has money in it. One gig read per row, capped at 5 in flight.
|
|
915
|
+
const funded = new Map();
|
|
916
|
+
if (values.funded) {
|
|
917
|
+
const gigIds = [...new Set(rows
|
|
918
|
+
.map((row) => parseGigRef(String(row.destination_url ?? row.invite_url ?? "")).gigId)
|
|
919
|
+
.filter((id) => /^GIG_/i.test(id)))];
|
|
920
|
+
await mapLimit(gigIds, 5, async (gigId) => {
|
|
921
|
+
const gig = await readGigFunds(auth, gigId).catch(() => null);
|
|
922
|
+
if (gig)
|
|
923
|
+
funded.set(gigId, fundsVerdict(gig));
|
|
924
|
+
});
|
|
925
|
+
}
|
|
516
926
|
out(Boolean(values.json), body, () => {
|
|
517
927
|
if (!rows.length) {
|
|
518
928
|
console.log(`${DIM}Nothing on the feed right now.${RESET}`);
|
|
519
929
|
return;
|
|
520
930
|
}
|
|
521
931
|
for (const row of rows) {
|
|
522
|
-
|
|
932
|
+
const link = row.destination_url ?? row.invite_url ?? "";
|
|
933
|
+
const check = funded.get(parseGigRef(String(link)).gigId);
|
|
934
|
+
console.log(` ${check ? paintVerdict(check.verdict) + " " : ""}${BOLD}${short(row.title ?? row.gig_title ?? row.note, 70)}${RESET}`);
|
|
935
|
+
if (check)
|
|
936
|
+
console.log(` ${DIM}${check.note}${RESET}`);
|
|
523
937
|
if (row.subtext)
|
|
524
938
|
console.log(` ${DIM}${short(row.subtext, 100)}${RESET}`);
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
939
|
+
if (link) {
|
|
940
|
+
// An invite-only gig refuses a tokenless join, so a link with no
|
|
941
|
+
// `?invite=` is a dead end, not an opportunity. Say so on the row.
|
|
942
|
+
const joinable = hasInviteToken(link);
|
|
943
|
+
console.log(` ${link}${joinable ? "" : ` ${YELLOW}← no ?invite= token: cannot join this link${RESET}`}`);
|
|
944
|
+
}
|
|
528
945
|
}
|
|
529
946
|
console.log("");
|
|
530
|
-
|
|
947
|
+
if (!values.funded) {
|
|
948
|
+
console.log(`${DIM}A bell tells you who is asking, not who can pay. Add --funded, or run: vidfarm gigs earn${RESET}`);
|
|
949
|
+
}
|
|
950
|
+
console.log(`${DIM}${registry ? "The registry is every machine that EXISTS; the notifications are the ones whose owner is awake. Pick today's work from the bells." : "Check one before you commit an hour: vidfarm gigs check <the link above>"}${RESET}`);
|
|
531
951
|
});
|
|
532
952
|
}
|
|
533
953
|
async function cmdJoinFeed(auth, values) {
|
|
@@ -544,15 +964,12 @@ async function cmdJoin(auth, values, positionals) {
|
|
|
544
964
|
if (!raw)
|
|
545
965
|
throw new Error("gigs join needs a gig id or an invite URL: vidfarm gigs join GIG_01H… --invite <token>");
|
|
546
966
|
// An invite URL carries both halves: .../gig/<gigId>/join?invite=<token>
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
gigId = found;
|
|
554
|
-
invite = invite || url.searchParams.get("invite") || "";
|
|
555
|
-
}
|
|
967
|
+
const { gigId, invite } = parseGigRef(raw, values.invite ? String(values.invite) : "");
|
|
968
|
+
// Joining is free and reversible, so this never blocks — but a clipper who
|
|
969
|
+
// joins an empty machine and only discovers it after an hour of editing has
|
|
970
|
+
// already lost the afternoon. Surface the coin box at the moment of joining.
|
|
971
|
+
const funds = await readGigFunds(auth, gigId).catch(() => null);
|
|
972
|
+
const verdict = funds ? fundsVerdict(funds) : null;
|
|
556
973
|
const result = await dp(auth, `/gigs/${encodeURIComponent(gigId)}/mailboxes`, {
|
|
557
974
|
method: "POST",
|
|
558
975
|
body: {
|
|
@@ -561,9 +978,17 @@ async function cmdJoin(auth, values, positionals) {
|
|
|
561
978
|
...(values.wallet ? { wallet_address: String(values.wallet) } : {})
|
|
562
979
|
}
|
|
563
980
|
});
|
|
564
|
-
out(Boolean(values.json), result, () => {
|
|
981
|
+
out(Boolean(values.json), { ...result, funds, verdict: verdict?.verdict ?? null }, () => {
|
|
565
982
|
const mailbox = result.mailbox?.id ?? result.id ?? "";
|
|
566
983
|
console.log(`${GREEN}✓${RESET} Joined ${BOLD}${gigId}${RESET}${mailbox ? ` ${DIM}mailbox ${mailbox}${RESET}` : ""}`);
|
|
984
|
+
if (verdict)
|
|
985
|
+
console.log(` ${paintVerdict(verdict.verdict)} ${DIM}${verdict.note}${RESET}`);
|
|
986
|
+
if (!values.wallet) {
|
|
987
|
+
// A rollup pays the address snapshotted when it was created, so a wallet
|
|
988
|
+
// added after the first submit does not retroactively redirect anything.
|
|
989
|
+
console.log(` ${YELLOW}No --wallet given${RESET}${DIM} — a managed hot wallet is auto-provisioned and you move the money yourself later.${RESET}`);
|
|
990
|
+
console.log(` ${DIM}Set it BEFORE your first proof: a rollup pays the address snapshotted when it was created.${RESET}`);
|
|
991
|
+
}
|
|
567
992
|
console.log(` ${DIM}Now: vidfarm gigs work · vidfarm gigs claim ${gigId}${RESET}`);
|
|
568
993
|
});
|
|
569
994
|
}
|
|
@@ -571,7 +996,12 @@ async function cmdClaim(auth, values, positionals) {
|
|
|
571
996
|
const gigId = positionals[0];
|
|
572
997
|
if (!gigId)
|
|
573
998
|
throw new Error("gigs claim needs a gig id: vidfarm gigs claim GIG_01H… [--count 2]");
|
|
574
|
-
|
|
999
|
+
// ONE by default. Claiming takes a task off everybody else's queue and the
|
|
1000
|
+
// client is now waiting on you specifically, so a default of 2 quietly put a
|
|
1001
|
+
// clipper two tasks deep before it had finished one — and `unresponsive` is a
|
|
1002
|
+
// 2x reputation hit. The clipper harness's anti-runaway rail is one task in
|
|
1003
|
+
// flight; ask for more only when you mean it.
|
|
1004
|
+
const count = values.count === undefined ? 1 : Math.trunc(Number(values.count));
|
|
575
1005
|
const result = await dp(auth, `/gigs/${encodeURIComponent(gigId)}/queue/poll`, { method: "POST", body: { count } });
|
|
576
1006
|
const tasks = result.tasks ?? [];
|
|
577
1007
|
out(Boolean(values.json), result, () => {
|
|
@@ -601,6 +1031,21 @@ async function cmdSubmit(auth, values, positionals) {
|
|
|
601
1031
|
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.");
|
|
602
1032
|
}
|
|
603
1033
|
const tags = values.tags ? String(values.tags).split(",").map((t) => t.trim()).filter(Boolean) : [];
|
|
1034
|
+
// The escrowed half of the delivery. `proofs[]` is a PUBLIC playable url — the
|
|
1035
|
+
// buyer can download it and so can anyone they forward it to — so the
|
|
1036
|
+
// watermarked cut goes there and the clean master goes in `private_note`,
|
|
1037
|
+
// which the platform unseals only once a rollup has stamped paid_out_at.
|
|
1038
|
+
// --clean-master uploads the file to DollarPlatoon's own s3 first, because the
|
|
1039
|
+
// note presigns that url at read time off a random key; a link you host is
|
|
1040
|
+
// reachable the moment anyone guesses it.
|
|
1041
|
+
const noteParts = [];
|
|
1042
|
+
if (values["clean-master"]) {
|
|
1043
|
+
const url = await uploadToDollarPlatoon(auth, String(values["clean-master"]));
|
|
1044
|
+
noteParts.push(`Clean master, no watermark: ${url}`);
|
|
1045
|
+
}
|
|
1046
|
+
if (values["private-note"])
|
|
1047
|
+
noteParts.push(String(values["private-note"]));
|
|
1048
|
+
const privateNote = noteParts.join("\n");
|
|
604
1049
|
const result = await dp(auth, `/gigs/${encodeURIComponent(gigId)}/proofs`, {
|
|
605
1050
|
method: "POST",
|
|
606
1051
|
body: {
|
|
@@ -608,12 +1053,60 @@ async function cmdSubmit(auth, values, positionals) {
|
|
|
608
1053
|
proofs,
|
|
609
1054
|
...(values.mailbox ? { mailbox_id: String(values.mailbox) } : {}),
|
|
610
1055
|
...(tags.length ? { tags } : {}),
|
|
611
|
-
...(values.note ? { feedback: String(values.note) } : {})
|
|
1056
|
+
...(values.note ? { feedback: String(values.note) } : {}),
|
|
1057
|
+
...(privateNote ? { private_note: privateNote } : {})
|
|
612
1058
|
}
|
|
613
1059
|
});
|
|
1060
|
+
const proof = result.proof ?? result;
|
|
614
1061
|
out(Boolean(values.json), result, () => {
|
|
615
|
-
console.log(`${GREEN}✓${RESET} Submitted to ${BOLD}${gigId}${RESET} ${DIM}${
|
|
1062
|
+
console.log(`${GREEN}✓${RESET} Submitted to ${BOLD}${gigId}${RESET} ${DIM}${proof.id ?? result.proof_id ?? ""}${RESET}`);
|
|
1063
|
+
if (privateNote) {
|
|
1064
|
+
// The response field is the ONLY confirmation the note was stored, and a
|
|
1065
|
+
// note cannot be patched onto a proof afterwards — so a silent miss here
|
|
1066
|
+
// means the master was never handed over at all.
|
|
1067
|
+
if (proof.private_note_locked) {
|
|
1068
|
+
console.log(` ${GREEN}✓${RESET} ${DIM}private note stored and sealed — it opens when a rollup stamps paid_out_at, not on approval.${RESET}`);
|
|
1069
|
+
}
|
|
1070
|
+
else {
|
|
1071
|
+
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}`);
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
else {
|
|
1076
|
+
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
|
+
}
|
|
616
1078
|
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}`);
|
|
1080
|
+
});
|
|
1081
|
+
}
|
|
1082
|
+
/**
|
|
1083
|
+
* Read one of YOUR OWN proofs back — the "have I been paid, and did my note
|
|
1084
|
+
* save" check. The submitting worker can always read their own note, at any
|
|
1085
|
+
* status. Note this GET returns the proof FLAT: `.paid_out_at`, never
|
|
1086
|
+
* `.proof.paid_out_at`.
|
|
1087
|
+
*/
|
|
1088
|
+
async function cmdProof(auth, values, positionals) {
|
|
1089
|
+
const [gigRef, proofId] = positionals;
|
|
1090
|
+
if (!gigRef || !proofId)
|
|
1091
|
+
throw new Error("gigs proof needs a gig id and a proof id: vidfarm gigs proof GIG_01H… PROOF_01H…");
|
|
1092
|
+
const { gigId } = parseGigRef(gigRef);
|
|
1093
|
+
const body = await dp(auth, `/gigs/${encodeURIComponent(gigId)}/proofs/${encodeURIComponent(proofId)}`);
|
|
1094
|
+
const proof = body.proof ?? body;
|
|
1095
|
+
out(Boolean(values.json), body, () => {
|
|
1096
|
+
const paid = Boolean(proof.paid_out_at);
|
|
1097
|
+
console.log(` ${BOLD}${proof.id ?? proofId}${RESET} ${DIM}${proof.status ?? ""}${RESET}`);
|
|
1098
|
+
console.log(` ${paid ? GREEN + "PAID" : YELLOW + "NOT PAID"}${RESET} ${DIM}${paid ? String(proof.paid_out_at) : "approved is not paid — paid_out_at is the only field that means the USDC moved"}${RESET}`);
|
|
1099
|
+
if (proof.private_note_locked) {
|
|
1100
|
+
console.log(` ${DIM}private note: stored and sealed (opens on payout)${RESET}`);
|
|
1101
|
+
}
|
|
1102
|
+
else if (typeof proof.private_note === "string" && proof.private_note) {
|
|
1103
|
+
console.log(` ${DIM}private note:${RESET} ${short(proof.private_note, 160)}`);
|
|
1104
|
+
}
|
|
1105
|
+
else {
|
|
1106
|
+
console.log(` ${DIM}private note: none was attached${RESET}`);
|
|
1107
|
+
}
|
|
1108
|
+
console.log("");
|
|
1109
|
+
console.log(`${DIM}A link inside the note is presigned at read time and expires in an hour — re-fetch each time you want the file.${RESET}`);
|
|
617
1110
|
});
|
|
618
1111
|
}
|
|
619
1112
|
async function cmdMine(auth, values) {
|
|
@@ -658,12 +1151,16 @@ function options() {
|
|
|
658
1151
|
title: { type: "string" },
|
|
659
1152
|
subtext: { type: "string" },
|
|
660
1153
|
registry: { type: "boolean" },
|
|
1154
|
+
funded: { type: "boolean" },
|
|
661
1155
|
name: { type: "string" },
|
|
662
1156
|
invite: { type: "string" },
|
|
663
1157
|
wallet: { type: "string" },
|
|
664
1158
|
count: { type: "string" },
|
|
665
1159
|
mailbox: { type: "string" },
|
|
666
|
-
note: { type: "string" }
|
|
1160
|
+
note: { type: "string" },
|
|
1161
|
+
"private-note": { type: "string" },
|
|
1162
|
+
"clean-master": { type: "string" },
|
|
1163
|
+
prefix: { type: "string" }
|
|
667
1164
|
};
|
|
668
1165
|
}
|
|
669
1166
|
export async function runGigsCommand(argv) {
|
|
@@ -691,14 +1188,23 @@ export async function runGigsCommand(argv) {
|
|
|
691
1188
|
case "pass": return cmdReview(auth, values, rest, "reject");
|
|
692
1189
|
case "ring-bell":
|
|
693
1190
|
case "bell": return cmdRingBell(auth, values);
|
|
1191
|
+
case "earn":
|
|
1192
|
+
case "start": return cmdEarn(auth, values);
|
|
694
1193
|
case "work":
|
|
695
1194
|
case "available": return cmdWork(auth, values);
|
|
1195
|
+
case "check":
|
|
1196
|
+
case "funds": return cmdCheck(auth, values, rest);
|
|
696
1197
|
case "feed": return cmdFeed(auth, values);
|
|
697
1198
|
case "join-feed": return cmdJoinFeed(auth, values);
|
|
698
1199
|
case "join": return cmdJoin(auth, values, rest);
|
|
699
1200
|
case "claim":
|
|
700
1201
|
case "poll": return cmdClaim(auth, values, rest);
|
|
701
1202
|
case "submit": return cmdSubmit(auth, values, rest);
|
|
1203
|
+
case "proof": return cmdProof(auth, values, rest);
|
|
1204
|
+
case "upload": return cmdUpload(auth, values, rest);
|
|
1205
|
+
case "earnings":
|
|
1206
|
+
case "rollups":
|
|
1207
|
+
case "paid": return cmdEarnings(auth, values);
|
|
702
1208
|
case "mine":
|
|
703
1209
|
case "mailboxes": return cmdMine(auth, values);
|
|
704
1210
|
default:
|