@officexapp/vidfarm-devcli 0.21.45 → 0.21.47

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.
Files changed (39) hide show
  1. package/.agents/skills/editor-capabilities/SKILL.md +2 -0
  2. package/.agents/skills/vidfarm/SKILL.md +60 -7
  3. package/.agents/skills/vidfarm/harnesses/explainer.HARNESS.md +1 -1
  4. package/.agents/skills/vidfarm/harnesses/product-demo.HARNESS.md +2 -0
  5. package/.agents/skills/vidfarm/harnesses/short-form.HARNESS.md +1 -0
  6. package/.agents/skills/vidfarm/recipes/local-edit-render-approve.md +1 -1
  7. package/.agents/skills/vidfarm/references/agent-included-imagegen.md +75 -0
  8. package/.agents/skills/vidfarm/references/assets-and-sourcing.md +71 -2
  9. package/.agents/skills/vidfarm/references/automation-and-local-dev.md +13 -6
  10. package/.agents/skills/vidfarm/references/browser-harness.md +93 -0
  11. package/.agents/skills/vidfarm/references/editor-workflows.md +22 -0
  12. package/SKILL.director.md +337 -16
  13. package/SKILL.md +45 -4
  14. package/dist/src/cli.js +210 -4
  15. package/dist/src/devcli/agent-imagegen.js +181 -0
  16. package/dist/src/devcli/browser-harness.js +384 -0
  17. package/dist/src/devcli/clip-store.js +41 -3
  18. package/dist/src/devcli/cost-mode.js +23 -3
  19. package/dist/src/devcli/doctor.js +52 -3
  20. package/dist/src/devcli/hyperframes-cli.js +11 -1
  21. package/dist/src/devcli/local-render.js +4 -7
  22. package/dist/src/devcli/marketplace-gigs.js +651 -0
  23. package/dist/src/devcli/qa-check.js +89 -1
  24. package/dist/src/devcli/shared-folder.js +387 -0
  25. package/dist/src/devcli/stills.js +4 -8
  26. package/dist/src/lib/ffprobe-path.js +64 -0
  27. package/dist/src/lib/render-media-prep.js +2 -11
  28. package/dist/src/services/clip-curation/ffmpeg.js +4 -15
  29. package/dist/src/services/clip-curation/local-agent.js +6 -2
  30. package/experiments.md +2 -2
  31. package/marketplace.md +549 -0
  32. package/package.json +21 -10
  33. package/public/assets/file-directory-app.js +34 -34
  34. package/public/serve-shells/library-files.html +5 -1
  35. package/public/serve-shells/library-raws.html +10 -1
  36. package/public/serve-shells/tools-clipper.html +5 -1
  37. package/public/serve-shells/tools-image.html +5 -1
  38. package/public/serve-shells/tools-video.html +5 -1
  39. package/crowdsourcing.md +0 -157
@@ -0,0 +1,651 @@
1
+ // `vidfarm gigs` — the vending machines, from BOTH sides, in the terminal.
2
+ //
3
+ // CLIENT (paid vidfarm plan): list your two standing machines, post a task into
4
+ // Custom Requests, read the queue, read the proofs that came back, approve or
5
+ // reject them, and ring the bell so every listening agent sees the new work.
6
+ //
7
+ // GIGWORKER (free, no vidfarm account at all): bring your own dollarplatoon.com
8
+ // api key, read the feed to find clients, join a machine, claim a task, and
9
+ // submit your proof.
10
+ //
11
+ // Everything here talks to Dollar Platoon's own API. The key is resolved in
12
+ // this order:
13
+ // 1. --dp-key <key>
14
+ // 2. DOLLARPLATOON_API_KEY (dollarplatoon.com)
15
+ // 3. GIGDESK_API_KEY (gigdesk.cc — same key family, Bearer auth)
16
+ // 4. the vidfarm account's own key, fetched from
17
+ // GET /api/v1/user/me/marketplace with VIDFARM_API_KEY (paid plans only)
18
+ // So a client who already ran `vidfarm login` needs no extra setup, and a
19
+ // gigworker who has never heard of vidfarm needs only their own key.
20
+ import { parseArgs } from "node:util";
21
+ import { readFileSync } from "node:fs";
22
+ import { readStoredAuth } from "./auth-store.js";
23
+ const BOLD = "\x1b[1m";
24
+ const DIM = "\x1b[2m";
25
+ const GREEN = "\x1b[32m";
26
+ const YELLOW = "\x1b[33m";
27
+ const RESET = "\x1b[0m";
28
+ /** The shared vidfarm feed: the registry + notification stream every client's machines are listed in. */
29
+ const VIDFARM_FEED_ID = "FEED_01M0BCTVTKSDFZQVEY1RXA4NJK";
30
+ // Two standing invites exist for that feed. The read+register+publish one is
31
+ // PRIVATE — signup applies it server-side (services/dollarplatoon.ts) for paid
32
+ // clients, who are the only ones allowed to list a machine. This module ships in
33
+ // the public npm package, so it may only ever carry the read-only invite: every
34
+ // `gigs join-feed` caller is a gigworker who just needs to see the work.
35
+ /** Standing reusable read-only invite for that feed — public by design. */
36
+ const VIDFARM_FEED_READ_INVITE = "MDFNMEJDVFZUWjVaUEFHNDlO";
37
+ const DEFAULT_DP_API = "https://dollarplatoon.com/api";
38
+ const GIGDESK_API = "https://gigdesk.cc/api";
39
+ export const GIGS_HELP = `vidfarm gigs — your vending machines, both sides of them
40
+
41
+ CLIENT (paid vidfarm plan — the key is read from your vidfarm account)
42
+ gigs machines Your two standing machines + gig ids + invite links
43
+ gigs tasks [machine] What is waiting in the Custom Requests queue
44
+ gigs add-task --task "<brief>" Post a task into Custom Requests
45
+ --price <usd> Price per delivered video (default 0.50; "tbd" allowed)
46
+ --tags a,b --priority <n> Routing hints (lower priority polls sooner)
47
+ --angle/--hook/--url/--format The usual brief fields
48
+ --assets-link <url> A READ share link to the footage they may use
49
+ --upload-link <url> An UPLOAD share link they drop deliverables into
50
+ --subfolder <name> The subfolder you want their work in
51
+ --proof <text> Proof requirement (repeatable)
52
+ --body <file.json> Send this JSON as the whole task instead
53
+ gigs proofs [machine] Submitted videos, newest first
54
+ --status <pending|approved|rejected> --limit <n>
55
+ gigs approve <proof-id> [machine] [--feedback "<text>"]
56
+ gigs reject <proof-id> [machine] [--tag not_selected] [--feedback "<text>"]
57
+ not_selected = a pass; it costs the worker NO reputation
58
+ gigs ring-bell --title "<t>" [--subtext "<s>"] [--machine inbound_offers]
59
+ Broadcast to every agent on the vidfarm feed
60
+
61
+ GIGWORKER (free — bring your own key: export DOLLARPLATOON_API_KEY=…)
62
+ gigs work Work waiting in every machine you joined → GET /work/available
63
+ gigs feed Who is asking for videos right now (notifications)
64
+ --registry Every machine listed in the feed, joinable
65
+ --limit <n>
66
+ gigs join-feed [--name "<you>"] Join the shared vidfarm feed, read-only (needed to read it)
67
+ gigs join <gig-id> [--invite <token>] [--name "<you>"] [--wallet 0x…]
68
+ gigs claim <gig-id> [--count 2] Claim task(s) off a FIFO queue machine
69
+ gigs submit <gig-id> --task <task-id> --proof <url> [--proof <url>…]
70
+ --mailbox <id> --tags a,b --note "<text>"
71
+ gigs mine Your mailboxes across every gig
72
+
73
+ Common: --dp-key <key> · --json · --host <vidfarm url> (for the key lookup)
74
+
75
+ e.g. vidfarm gigs add-task --task "60s explainer for acme.com" --price 0.50 \\
76
+ --upload-link "https://vidfarm.cc/directory/preview/dsh_…/files/acme"
77
+ vidfarm gigs proofs --status pending
78
+ vidfarm gigs approve PRF_01H… --feedback "Great hook, keeping it."
79
+ DOLLARPLATOON_API_KEY=… vidfarm gigs work`;
80
+ /**
81
+ * "You have no key yet, here is how to get one" — the single most common first
82
+ * run for a gigworker, and a deliberate instruction rather than a bug. cli.ts
83
+ * prints these without a stack trace and files no crash report.
84
+ */
85
+ export class GigsAuthMissingError extends Error {
86
+ constructor(message) {
87
+ super(message);
88
+ this.name = "GigsAuthMissingError";
89
+ }
90
+ }
91
+ async function resolveAuth(values) {
92
+ const explicit = values["dp-key"]?.trim();
93
+ if (explicit)
94
+ return { key: explicit, api: DEFAULT_DP_API, source: "--dp-key" };
95
+ const dp = (process.env.DOLLARPLATOON_API_KEY ?? "").trim();
96
+ if (dp)
97
+ return { key: dp, api: DEFAULT_DP_API, source: "DOLLARPLATOON_API_KEY" };
98
+ const gd = (process.env.GIGDESK_API_KEY ?? "").trim();
99
+ if (gd)
100
+ return { key: gd, api: GIGDESK_API, source: "GIGDESK_API_KEY" };
101
+ const account = await loadVidfarmMarketplace(values);
102
+ if (account?.dollarplatoon_api_key) {
103
+ return {
104
+ key: account.dollarplatoon_api_key,
105
+ api: account.dollarplatoon_api_url || DEFAULT_DP_API,
106
+ source: "your vidfarm account"
107
+ };
108
+ }
109
+ throw new GigsAuthMissingError([
110
+ "No Dollar Platoon key found.",
111
+ " Client: run `vidfarm login` (paid plan) — the key comes from your account.",
112
+ " Gigworker: export DOLLARPLATOON_API_KEY=… from https://dollarplatoon.com/gigworker/settings",
113
+ " Or pass it directly: --dp-key <key>"
114
+ ].join("\n"));
115
+ }
116
+ let marketplaceCache;
117
+ /** The paid client's own key + machines, straight from their vidfarm account. */
118
+ async function loadVidfarmMarketplace(values) {
119
+ if (marketplaceCache !== undefined)
120
+ return marketplaceCache;
121
+ // `vidfarm login` already stored a key — use it, so a logged-in client needs
122
+ // no env var at all. Explicit flags and env still win.
123
+ const stored = readStoredAuth();
124
+ const apiKey = (values["api-key"] ?? process.env.VIDFARM_API_KEY ?? stored?.apiKey ?? "").trim();
125
+ const host = ((values.host ?? process.env.VIDFARM_HOST ?? stored?.host ?? "https://vidfarm.cc").trim()).replace(/\/+$/, "");
126
+ if (!apiKey) {
127
+ marketplaceCache = null;
128
+ return null;
129
+ }
130
+ try {
131
+ const response = await fetch(`${host}/api/v1/user/me/marketplace`, {
132
+ headers: { authorization: `Bearer ${apiKey}`, accept: "application/json" }
133
+ });
134
+ const body = await response.json().catch(() => ({}));
135
+ if (!response.ok) {
136
+ // 402/409 are meaningful: not paid, or no key connected yet. Surface them
137
+ // rather than falling through to a bare "no key found". Everything else —
138
+ // above all a 401 from a stale `vidfarm login` — must fall through: this
139
+ // lookup is the LAST resort in resolveAuth, and a gigworker who never had
140
+ // a vidfarm account needs its "here is how to get a key" message, not a
141
+ // bare "Unauthorized" from a client-side route they were never using.
142
+ if (body?.error && (response.status === 402 || response.status === 409))
143
+ throw new Error(body.error);
144
+ marketplaceCache = null;
145
+ return null;
146
+ }
147
+ marketplaceCache = body;
148
+ return body;
149
+ }
150
+ catch (error) {
151
+ if (error instanceof Error && error.message && !/fetch failed/i.test(error.message))
152
+ throw error;
153
+ marketplaceCache = null;
154
+ return null;
155
+ }
156
+ }
157
+ // ── transport ────────────────────────────────────────────────────────────────
158
+ async function dp(auth, path, init = {}) {
159
+ const url = new URL(auth.api + path);
160
+ for (const [key, value] of Object.entries(init.query ?? {})) {
161
+ if (value != null && value !== "")
162
+ url.searchParams.set(key, value);
163
+ }
164
+ const response = await fetch(url, {
165
+ method: init.method ?? "GET",
166
+ headers: {
167
+ // DollarPlatoon authenticates ONLY on x-api-key — a Bearer token alone is
168
+ // rejected with "Missing x-api-key header" before the key is even read.
169
+ // GigDesk documents Bearer and accepts x-api-key too, so sending both is
170
+ // correct for either host and keeps this transport host-agnostic.
171
+ "x-api-key": auth.key,
172
+ authorization: `Bearer ${auth.key}`,
173
+ accept: "application/json",
174
+ ...(init.body === undefined ? {} : { "content-type": "application/json" })
175
+ },
176
+ body: init.body === undefined ? undefined : JSON.stringify(init.body)
177
+ });
178
+ const text = await response.text();
179
+ let json = {};
180
+ try {
181
+ json = text ? JSON.parse(text) : {};
182
+ }
183
+ catch {
184
+ json = { error: text.slice(0, 300) };
185
+ }
186
+ if (!response.ok) {
187
+ const message = typeof json?.error === "string" ? json.error : `HTTP ${response.status}`;
188
+ throw new Error(`dollarplatoon ${path} — ${message}`);
189
+ }
190
+ return json;
191
+ }
192
+ // ── machine resolution ───────────────────────────────────────────────────────
193
+ const MACHINE_ALIASES = {
194
+ custom: "custom_requests",
195
+ custom_requests: "custom_requests",
196
+ requests: "custom_requests",
197
+ queue: "custom_requests",
198
+ inbound: "inbound_offers",
199
+ inbound_offers: "inbound_offers",
200
+ offers: "inbound_offers",
201
+ pitches: "inbound_offers"
202
+ };
203
+ /** Both machines, found by their stable `vidfarm_vm_<slug>` tag. */
204
+ async function listMachines(auth) {
205
+ const body = await dp(auth, "/gigs/mine", { query: { tag: "vidfarm_vm_" } });
206
+ const gigs = body.gigs ?? body.data ?? [];
207
+ return gigs.map((gig) => {
208
+ const tag = (gig.tags ?? []).find((t) => typeof t === "string" && t.startsWith("vidfarm_vm_")) ?? "";
209
+ return {
210
+ slug: tag.slice("vidfarm_vm_".length) || "other",
211
+ title: String(gig.title ?? gig.id ?? ""),
212
+ gigId: String(gig.id ?? ""),
213
+ distribution: gig.distribution ? String(gig.distribution) : undefined,
214
+ inviteUrl: typeof gig.invite_url === "string" ? gig.invite_url : null
215
+ };
216
+ }).filter((m) => m.gigId);
217
+ }
218
+ /** A gig id, a machine slug, or (default) the machine this command is about. */
219
+ async function resolveGigId(auth, raw, fallbackSlug) {
220
+ const value = (raw ?? "").trim();
221
+ if (/^GIG_/i.test(value))
222
+ return value;
223
+ const wanted = MACHINE_ALIASES[value.toLowerCase()] ?? fallbackSlug;
224
+ const machines = await listMachines(auth);
225
+ const match = machines.find((m) => m.slug === wanted);
226
+ if (!match) {
227
+ throw new Error(`No "${wanted}" vending machine on this account. Run: vidfarm gigs machines`);
228
+ }
229
+ return match.gigId;
230
+ }
231
+ // ── formatting ───────────────────────────────────────────────────────────────
232
+ function out(json, value, human) {
233
+ if (json) {
234
+ console.log(JSON.stringify(value, null, 2));
235
+ return;
236
+ }
237
+ human();
238
+ }
239
+ function short(value, max = 90) {
240
+ const text = typeof value === "string" ? value : value == null ? "" : JSON.stringify(value);
241
+ const flat = text.replace(/\s+/g, " ").trim();
242
+ return flat.length > max ? flat.slice(0, max - 1) + "…" : flat;
243
+ }
244
+ /** Dig the playable link out of a proof — workers scatter it across fields. */
245
+ function proofLink(proof) {
246
+ const proofs = Array.isArray(proof.proofs) ? proof.proofs : [];
247
+ const direct = proofs.find((p) => typeof p === "string" && /^https?:\/\//.test(p));
248
+ if (direct)
249
+ return String(direct);
250
+ const note = typeof proof.feedback === "string" ? proof.feedback : "";
251
+ const found = /https?:\/\/\S+/.exec(note);
252
+ return found ? found[0] : "";
253
+ }
254
+ // ── client commands ──────────────────────────────────────────────────────────
255
+ async function cmdMachines(auth, values) {
256
+ const machines = await listMachines(auth);
257
+ out(Boolean(values.json), { machines }, () => {
258
+ if (!machines.length) {
259
+ console.log(`${DIM}No vending machines on this key yet. A paid vidfarm account gets two; open ${RESET}https://vidfarm.cc/marketplace/buyer${DIM} once to create them.${RESET}`);
260
+ return;
261
+ }
262
+ for (const machine of machines) {
263
+ console.log(` ${BOLD}${machine.title}${RESET} ${DIM}${machine.slug} · ${machine.gigId}${machine.distribution ? " · " + machine.distribution : ""}${RESET}`);
264
+ if (machine.inviteUrl)
265
+ console.log(` ${DIM}invite: ${machine.inviteUrl}${RESET}`);
266
+ }
267
+ });
268
+ }
269
+ async function cmdTasks(auth, values, positional) {
270
+ const gigId = await resolveGigId(auth, positional, "custom_requests");
271
+ const body = await dp(auth, `/gigs/${encodeURIComponent(gigId)}/queue`, { query: { limit: values.limit } });
272
+ const tasks = body.tasks ?? body.queue ?? body.messages ?? [];
273
+ out(Boolean(values.json), body, () => {
274
+ if (!tasks.length) {
275
+ console.log(`${DIM}Queue is empty — post one: vidfarm gigs add-task --task "<brief>"${RESET}`);
276
+ return;
277
+ }
278
+ for (const task of tasks) {
279
+ const claimed = task.claimed_by || task.mailbox_id ? " · claimed" : "";
280
+ console.log(` ${BOLD}${short(task.subject ?? task.id, 60)}${RESET} ${DIM}[${task.id}]${claimed}${RESET}`);
281
+ if (task.payload)
282
+ console.log(` ${DIM}${short(task.payload, 110)}${RESET}`);
283
+ }
284
+ });
285
+ }
286
+ async function cmdAddTask(auth, values) {
287
+ const gigId = await resolveGigId(auth, values.machine, "custom_requests");
288
+ // The publisher webhook (with its security token) lives on the gig record.
289
+ const gig = await dp(auth, `/gigs/${encodeURIComponent(gigId)}`);
290
+ const webhook = gig.gig?.webhook ?? gig.webhook ?? "";
291
+ if (!webhook)
292
+ throw new Error("That machine has no publisher webhook — open it on dollarplatoon.com and check the gig.");
293
+ let payload;
294
+ if (values.body) {
295
+ payload = JSON.parse(readFileSync(String(values.body), "utf8"));
296
+ }
297
+ else {
298
+ const task = values.task?.trim();
299
+ if (!task)
300
+ throw new Error('gigs add-task needs --task "<brief>" (or --body <file.json>).');
301
+ const proofs = [].concat(values.proof ?? []);
302
+ payload = {
303
+ task,
304
+ ...(values.angle ? { angle: String(values.angle) } : {}),
305
+ ...(values.hook ? { hook: String(values.hook) } : {}),
306
+ ...(values.url ? { url: String(values.url) } : {}),
307
+ ...(values.format ? { format: String(values.format) } : {}),
308
+ // The file-handover layer: a read link to the footage, an upload link for
309
+ // the deliverable, and the subfolder the worker should create in it.
310
+ ...(values["assets-link"] ? { assets_link: String(values["assets-link"]) } : {}),
311
+ ...(values["upload-link"] ? { upload_link: String(values["upload-link"]) } : {}),
312
+ ...(values.subfolder ? { upload_subfolder: String(values.subfolder) } : {}),
313
+ ...(proofs.length ? { proof_requirements: proofs } : {})
314
+ };
315
+ }
316
+ const url = new URL(webhook);
317
+ const price = values.price === undefined ? "0.50" : String(values.price);
318
+ url.searchParams.set("price", price);
319
+ if (values.tags)
320
+ url.searchParams.set("tags", String(values.tags));
321
+ if (values.priority !== undefined)
322
+ url.searchParams.set("priority", String(values.priority));
323
+ if (values["assign-to"])
324
+ url.searchParams.set("assign_to", String(values["assign-to"]));
325
+ const response = await fetch(url, {
326
+ method: "POST",
327
+ headers: { "content-type": "application/json" },
328
+ body: JSON.stringify(payload)
329
+ });
330
+ const text = await response.text();
331
+ let body = {};
332
+ try {
333
+ body = text ? JSON.parse(text) : {};
334
+ }
335
+ catch {
336
+ body = { raw: text.slice(0, 300) };
337
+ }
338
+ if (!response.ok)
339
+ throw new Error(`add-task failed: ${body?.error ?? `HTTP ${response.status}`}`);
340
+ out(Boolean(values.json), body, () => {
341
+ const ids = (body.message_ids ?? []).join(", ");
342
+ console.log(`${GREEN}✓${RESET} Task posted to ${BOLD}${gigId}${RESET} at ${BOLD}$${price}${RESET}/video ${DIM}${ids}${RESET}`);
343
+ console.log(` ${DIM}Nobody knows yet — broadcast it: vidfarm gigs ring-bell --title "…" --machine custom_requests${RESET}`);
344
+ });
345
+ }
346
+ async function cmdProofs(auth, values, positional) {
347
+ const gigId = await resolveGigId(auth, positional, "inbound_offers");
348
+ const body = await dp(auth, `/gigs/${encodeURIComponent(gigId)}/proofs`, {
349
+ query: { status: values.status, limit: values.limit }
350
+ });
351
+ const proofs = body.proofs ?? body.data ?? [];
352
+ out(Boolean(values.json), body, () => {
353
+ if (!proofs.length) {
354
+ console.log(`${DIM}No proofs on this machine yet.${RESET}`);
355
+ return;
356
+ }
357
+ for (const proof of proofs) {
358
+ const price = typeof proof.locked_price === "number" ? ` · $${proof.locked_price.toFixed(2)}` : "";
359
+ console.log(` ${BOLD}${proof.id}${RESET} ${DIM}${proof.status ?? ""}${price}${proof.task_identifier ? " · " + proof.task_identifier : ""}${RESET}`);
360
+ const link = proofLink(proof);
361
+ if (link)
362
+ console.log(` ${link}`);
363
+ }
364
+ console.log("");
365
+ console.log(`${DIM}Keep one: vidfarm gigs approve <proof-id> · Pass: vidfarm gigs reject <proof-id> --tag not_selected${RESET}`);
366
+ console.log(`${YELLOW}Silence is approval${RESET}${DIM} — an unreviewed proof auto-approves after the gig's review timeout (default 48h) and you pay for it.${RESET}`);
367
+ });
368
+ }
369
+ async function cmdReview(auth, values, positionals, action) {
370
+ const proofId = positionals[0];
371
+ if (!proofId)
372
+ throw new Error(`gigs ${action} needs a proof id: vidfarm gigs ${action} PRF_01H…`);
373
+ const gigId = await resolveGigId(auth, positionals[1], "inbound_offers");
374
+ const body = { action };
375
+ if (values.feedback)
376
+ body.feedback = String(values.feedback);
377
+ if (action === "reject")
378
+ body.rejection_tag = values.tag ? String(values.tag) : "not_selected";
379
+ const result = await dp(auth, `/gigs/${encodeURIComponent(gigId)}/proofs/${encodeURIComponent(proofId)}`, { method: "PATCH", body });
380
+ out(Boolean(values.json), result, () => {
381
+ if (action === "approve") {
382
+ console.log(`${GREEN}✓${RESET} Approved ${BOLD}${proofId}${RESET} — it pays out on the next rollup.`);
383
+ }
384
+ else {
385
+ console.log(`${GREEN}✓${RESET} Rejected ${BOLD}${proofId}${RESET} as ${BOLD}${body.rejection_tag}${RESET}.`);
386
+ if (body.rejection_tag === "not_selected") {
387
+ console.log(` ${DIM}not_selected costs the worker no reputation — the right tag for "good, not this time".${RESET}`);
388
+ }
389
+ }
390
+ });
391
+ }
392
+ async function cmdRingBell(auth, values) {
393
+ const title = values.title?.trim();
394
+ if (!title)
395
+ throw new Error('gigs ring-bell needs --title "<what you want made>".');
396
+ const slug = MACHINE_ALIASES[String(values.machine ?? "inbound_offers").toLowerCase()] ?? "inbound_offers";
397
+ const machines = await listMachines(auth);
398
+ const machine = machines.find((m) => m.slug === slug);
399
+ if (!machine)
400
+ throw new Error(`No "${slug}" machine on this account. Run: vidfarm gigs machines`);
401
+ let destination = machine.inviteUrl ?? "";
402
+ if (!destination) {
403
+ // Registering the gig in the feed returns its reusable join link.
404
+ const registered = await dp(auth, `/feeds/${VIDFARM_FEED_ID}/registry`, {
405
+ method: "POST",
406
+ body: { gig_id: machine.gigId, tags: ["vidfarm", "video", slug] }
407
+ }).catch(() => ({}));
408
+ destination = registered.invite_url ?? "";
409
+ }
410
+ if (!destination)
411
+ throw new Error("That machine has no invite link yet — open it once on vidfarm.cc/marketplace/buyer.");
412
+ const result = await dp(auth, `/feeds/${VIDFARM_FEED_ID}/notifications`, {
413
+ method: "POST",
414
+ body: {
415
+ title,
416
+ subtext: String(values.subtext ?? ""),
417
+ destination_url: destination,
418
+ tags: ["vidfarm", "video", slug]
419
+ }
420
+ });
421
+ out(Boolean(values.json), result, () => {
422
+ console.log(`${GREEN}✓${RESET} Bell rung on ${BOLD}${machine.title}${RESET} — every agent on the vidfarm feed sees it.`);
423
+ console.log(` ${DIM}${destination}${RESET}`);
424
+ console.log(` ${DIM}Ring it when something CHANGED (new batch, new price, new format) — an empty bell teaches agents to ignore you.${RESET}`);
425
+ });
426
+ }
427
+ // ── gigworker commands ───────────────────────────────────────────────────────
428
+ async function cmdWork(auth, values) {
429
+ const body = await dp(auth, "/work/available", { query: { only_with_work: "true", limit: values.limit } });
430
+ const rows = body.mailboxes ?? body.work ?? body.data ?? [];
431
+ out(Boolean(values.json), body, () => {
432
+ if (!rows.length) {
433
+ console.log(`${DIM}Nothing waiting in the machines you joined. Find new clients: vidfarm gigs feed${RESET}`);
434
+ return;
435
+ }
436
+ for (const row of rows) {
437
+ const count = row.available ?? row.pending_count ?? row.count ?? "";
438
+ console.log(` ${BOLD}${short(row.gig_title ?? row.gig_id, 60)}${RESET} ${DIM}${row.gig_id ?? ""}${count !== "" ? ` · ${count} waiting` : ""}${RESET}`);
439
+ }
440
+ console.log("");
441
+ console.log(`${DIM}Claim one: vidfarm gigs claim <gig-id> then vidfarm gigs submit <gig-id> --task <task-id> --proof <mp4 url>${RESET}`);
442
+ });
443
+ }
444
+ async function cmdFeed(auth, values) {
445
+ const registry = Boolean(values.registry);
446
+ const path = registry ? `/feeds/${VIDFARM_FEED_ID}/registry` : `/feeds/${VIDFARM_FEED_ID}/notifications`;
447
+ const body = await dp(auth, path, { query: { limit: values.limit ?? "50" } }).catch((error) => {
448
+ // A non-member gets 404 on EVERY feed route — never 403 — so a stranger cannot
449
+ // confirm a feed exists. 404 is therefore the code this actually fires on; the
450
+ // others are kept for a revoked key or a lost membership.
451
+ if (/40[134]/.test(error.message)) {
452
+ throw new Error("You are not on the vidfarm feed yet — join it first: vidfarm gigs join-feed");
453
+ }
454
+ throw error;
455
+ });
456
+ const rows = body.notifications ?? body.entries ?? body.registry ?? body.data ?? [];
457
+ out(Boolean(values.json), body, () => {
458
+ if (!rows.length) {
459
+ console.log(`${DIM}Nothing on the feed right now.${RESET}`);
460
+ return;
461
+ }
462
+ for (const row of rows) {
463
+ console.log(` ${BOLD}${short(row.title ?? row.gig_title ?? row.note, 70)}${RESET}`);
464
+ if (row.subtext)
465
+ console.log(` ${DIM}${short(row.subtext, 100)}${RESET}`);
466
+ const link = row.destination_url ?? row.invite_url ?? "";
467
+ if (link)
468
+ console.log(` ${link}`);
469
+ }
470
+ console.log("");
471
+ console.log(`${DIM}${registry ? "Join one with its invite link, then: vidfarm gigs work" : "Every entry links at a machine you can join right now."}${RESET}`);
472
+ });
473
+ }
474
+ async function cmdJoinFeed(auth, values) {
475
+ const result = await dp(auth, `/feeds/${VIDFARM_FEED_ID}/join`, {
476
+ method: "POST",
477
+ body: { invite: VIDFARM_FEED_READ_INVITE, ...(values.name ? { display_name: String(values.name) } : {}) }
478
+ });
479
+ out(Boolean(values.json), result, () => {
480
+ console.log(`${GREEN}✓${RESET} On the vidfarm feed. Read it: ${BOLD}vidfarm gigs feed${RESET}`);
481
+ });
482
+ }
483
+ async function cmdJoin(auth, values, positionals) {
484
+ const raw = positionals[0];
485
+ if (!raw)
486
+ throw new Error("gigs join needs a gig id or an invite URL: vidfarm gigs join GIG_01H… --invite <token>");
487
+ // An invite URL carries both halves: .../gig/<gigId>/join?invite=<token>
488
+ let gigId = raw;
489
+ let invite = values.invite ? String(values.invite) : "";
490
+ if (/^https?:\/\//i.test(raw)) {
491
+ const url = new URL(raw);
492
+ const found = url.pathname.split("/").find((s) => /^GIG_/i.test(s));
493
+ if (found)
494
+ gigId = found;
495
+ invite = invite || url.searchParams.get("invite") || "";
496
+ }
497
+ const result = await dp(auth, `/gigs/${encodeURIComponent(gigId)}/mailboxes`, {
498
+ method: "POST",
499
+ body: {
500
+ ...(values.name ? { name: String(values.name) } : {}),
501
+ ...(invite ? { invite } : {}),
502
+ ...(values.wallet ? { wallet_address: String(values.wallet) } : {})
503
+ }
504
+ });
505
+ out(Boolean(values.json), result, () => {
506
+ const mailbox = result.mailbox?.id ?? result.id ?? "";
507
+ console.log(`${GREEN}✓${RESET} Joined ${BOLD}${gigId}${RESET}${mailbox ? ` ${DIM}mailbox ${mailbox}${RESET}` : ""}`);
508
+ console.log(` ${DIM}Now: vidfarm gigs work · vidfarm gigs claim ${gigId}${RESET}`);
509
+ });
510
+ }
511
+ async function cmdClaim(auth, values, positionals) {
512
+ const gigId = positionals[0];
513
+ if (!gigId)
514
+ throw new Error("gigs claim needs a gig id: vidfarm gigs claim GIG_01H… [--count 2]");
515
+ const count = values.count === undefined ? 2 : Math.trunc(Number(values.count));
516
+ const result = await dp(auth, `/gigs/${encodeURIComponent(gigId)}/queue/poll`, { method: "POST", body: { count } });
517
+ const tasks = result.tasks ?? [];
518
+ out(Boolean(values.json), result, () => {
519
+ if (!tasks.length) {
520
+ console.log(`${DIM}Nothing claimable in that queue right now.${RESET}`);
521
+ return;
522
+ }
523
+ for (const task of tasks) {
524
+ console.log(` ${BOLD}${task.id}${RESET} ${short(task.subject, 70)}`);
525
+ if (task.payload)
526
+ console.log(` ${short(task.payload, 400)}`);
527
+ }
528
+ console.log("");
529
+ console.log(`${DIM}Submit with the task's OWN id — that is what claims it to you:${RESET}`);
530
+ console.log(`${DIM} vidfarm gigs submit ${gigId} --task ${tasks[0]?.id ?? "<task-id>"} --proof <public mp4 url>${RESET}`);
531
+ });
532
+ }
533
+ async function cmdSubmit(auth, values, positionals) {
534
+ const gigId = positionals[0];
535
+ if (!gigId)
536
+ throw new Error("gigs submit needs a gig id: vidfarm gigs submit GIG_01H… --task <id> --proof <url>");
537
+ const proofs = [].concat(values.proof ?? []);
538
+ if (!proofs.length)
539
+ throw new Error("gigs submit needs at least one --proof <public mp4 url>.");
540
+ const taskId = values.task?.trim();
541
+ if (!taskId) {
542
+ 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.");
543
+ }
544
+ const tags = values.tags ? String(values.tags).split(",").map((t) => t.trim()).filter(Boolean) : [];
545
+ const result = await dp(auth, `/gigs/${encodeURIComponent(gigId)}/proofs`, {
546
+ method: "POST",
547
+ body: {
548
+ task_identifier: taskId,
549
+ proofs,
550
+ ...(values.mailbox ? { mailbox_id: String(values.mailbox) } : {}),
551
+ ...(tags.length ? { tags } : {}),
552
+ ...(values.note ? { feedback: String(values.note) } : {})
553
+ }
554
+ });
555
+ out(Boolean(values.json), result, () => {
556
+ console.log(`${GREEN}✓${RESET} Submitted to ${BOLD}${gigId}${RESET} ${DIM}${result.proof_id ?? result.id ?? ""}${RESET}`);
557
+ console.log(` ${DIM}The buyer swipes this in a deck — a link that needs a login is a proof they cannot watch.${RESET}`);
558
+ });
559
+ }
560
+ async function cmdMine(auth, values) {
561
+ const body = await dp(auth, "/mailboxes/mine", { query: { limit: values.limit } });
562
+ const rows = body.mailboxes ?? body.data ?? [];
563
+ out(Boolean(values.json), body, () => {
564
+ if (!rows.length) {
565
+ console.log(`${DIM}You have not joined any gigs yet. Find one: vidfarm gigs feed${RESET}`);
566
+ return;
567
+ }
568
+ for (const row of rows) {
569
+ console.log(` ${BOLD}${short(row.gig_title ?? row.gig_id, 60)}${RESET} ${DIM}${row.gig_id ?? ""} · mailbox ${row.id ?? ""}${RESET}`);
570
+ }
571
+ });
572
+ }
573
+ // ── dispatch ─────────────────────────────────────────────────────────────────
574
+ function options() {
575
+ return {
576
+ "dp-key": { type: "string" },
577
+ "api-key": { type: "string" },
578
+ host: { type: "string" },
579
+ json: { type: "boolean" },
580
+ machine: { type: "string" },
581
+ task: { type: "string" },
582
+ body: { type: "string" },
583
+ price: { type: "string" },
584
+ tags: { type: "string" },
585
+ priority: { type: "string" },
586
+ "assign-to": { type: "string" },
587
+ angle: { type: "string" },
588
+ hook: { type: "string" },
589
+ url: { type: "string" },
590
+ format: { type: "string" },
591
+ "assets-link": { type: "string" },
592
+ "upload-link": { type: "string" },
593
+ subfolder: { type: "string" },
594
+ proof: { type: "string", multiple: true },
595
+ status: { type: "string" },
596
+ limit: { type: "string" },
597
+ feedback: { type: "string" },
598
+ tag: { type: "string" },
599
+ title: { type: "string" },
600
+ subtext: { type: "string" },
601
+ registry: { type: "boolean" },
602
+ name: { type: "string" },
603
+ invite: { type: "string" },
604
+ wallet: { type: "string" },
605
+ count: { type: "string" },
606
+ mailbox: { type: "string" },
607
+ note: { type: "string" }
608
+ };
609
+ }
610
+ export async function runGigsCommand(argv) {
611
+ const sub = (argv[0] ?? "help").toLowerCase();
612
+ if (["help", "--help", "-h", ""].includes(sub)) {
613
+ console.log(GIGS_HELP);
614
+ return;
615
+ }
616
+ const parsed = parseArgs({ args: argv.slice(1), allowPositionals: true, options: options() });
617
+ const values = parsed.values;
618
+ const rest = parsed.positionals;
619
+ const auth = await resolveAuth(values);
620
+ switch (sub) {
621
+ case "machines":
622
+ case "vending": return cmdMachines(auth, values);
623
+ case "tasks":
624
+ case "queue": return cmdTasks(auth, values, rest[0]);
625
+ case "add-task":
626
+ case "task": return cmdAddTask(auth, values);
627
+ case "proofs":
628
+ case "submissions": return cmdProofs(auth, values, rest[0]);
629
+ case "approve":
630
+ case "keep": return cmdReview(auth, values, rest, "approve");
631
+ case "reject":
632
+ case "pass": return cmdReview(auth, values, rest, "reject");
633
+ case "ring-bell":
634
+ case "bell": return cmdRingBell(auth, values);
635
+ case "work":
636
+ case "available": return cmdWork(auth, values);
637
+ case "feed": return cmdFeed(auth, values);
638
+ case "join-feed": return cmdJoinFeed(auth, values);
639
+ case "join": return cmdJoin(auth, values, rest);
640
+ case "claim":
641
+ case "poll": return cmdClaim(auth, values, rest);
642
+ case "submit": return cmdSubmit(auth, values, rest);
643
+ case "mine":
644
+ case "mailboxes": return cmdMine(auth, values);
645
+ default:
646
+ console.error(`Unknown gigs subcommand: ${sub}\n`);
647
+ console.log(GIGS_HELP);
648
+ process.exitCode = 1;
649
+ }
650
+ }
651
+ //# sourceMappingURL=marketplace-gigs.js.map