@officexapp/vidfarm-devcli 0.21.50 → 0.21.52
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 +3 -2
- package/.agents/skills/vidfarm/references/assets-and-sourcing.md +45 -1
- package/.agents/skills/vidfarm/references/automation-and-local-dev.md +2 -2
- package/.agents/skills/vidfarm/references/core-workflows.md +15 -2
- package/.agents/skills/vidfarm/references/rest-api.md +1 -1
- package/SKILL.director.md +68 -16
- package/dist/src/cli.js +87 -13
- package/dist/src/devcli/marketplace-gigs.js +53 -5
- package/dist/src/lib/ffprobe-path.js +27 -2
- package/marketplace.md +28 -4
- package/package.json +1 -1
|
@@ -399,7 +399,7 @@ You may be running as the **in-web AI chat** (the /editor copilot, the chat dock
|
|
|
399
399
|
|---|---|---|
|
|
400
400
|
| `references/core-workflows.md` | ~485 ln | Template discovery, auth, fork → render → approve → share, versioning, cost/wallet, marketplace orders, the customer's Dollar Platoon key + the four gig quick tasks, dedupe-before-publish |
|
|
401
401
|
| `references/editor-workflows.md` | ~650 ln | **The biggest read.** Timeline editing, decompose, captions, transitions, motion, AI placement, the caption standard, the editor action verbs |
|
|
402
|
-
| `references/assets-and-sourcing.md` | ~
|
|
402
|
+
| `references/assets-and-sourcing.md` | ~390 ln | **Google search for footage** (the query playbook), raws hunts, clip scanning, My Files, **uploading (presign → PUT → finalize)**, recurring characters, downloading media off a URL, social recycle |
|
|
403
403
|
| `references/agent-included-imagegen.md` | ~70 ln | **Free custom art without spending.** Image generation the AGENT's own subscription already includes (Antigravity / Gemini CLI, Codex / ChatGPT — Claude Code has none): why it is allowed in cost mode `minimize`, `vidfarm agent-image --check` / the brief command, where it sits on the asset ladder, and the honest limits |
|
|
404
404
|
| `references/browser-harness.md` | ~85 ln | **Free-plan sourcing, and cost mode `minimize`.** Driving the user's own Chrome with browser-harness as the $0 substitute for the paid video-search / image-search / news-search / download-video primitives: setup, the browse recipes, the raws collection loop, the interactive hand-off, and the licensing/etiquette limits |
|
|
405
405
|
| `references/automation-and-local-dev.md` | ~520 ln | **Big.** The whole `vidfarm` command table, REST automation, scripting/bulk mode, `HARNESS.md`, local serve loop, skill packs |
|
|
@@ -481,7 +481,7 @@ The File Index above says what each file *is*; this says which one a given ask m
|
|
|
481
481
|
5b. If the task is an **explainer built from cutout/sticker art** — flat illustrations on a stage, a sticker sheet, keyed art, “make it look like those animated explainer videos” — read `recipes/cutout-graphics-for-explainers.md`. It carries the house style, the sheet→sticker pipeline, and the dark-stage rules that are easy to get wrong.
|
|
482
482
|
5c. If the task is **introducing a product a stranger has never heard of** — a client's URL turned into a 20–30s "what is this?" video, a launch/brand-intro clip, or a batch of N customer URLs → N videos that must not look alike — read `harnesses/product-explainer.HARNESS.md`. It is the format with the single most expensive defect in the catalog (the product never plainly named in the first 5s, which costs a VO re-record to fix), plus the simple-open text-run count, the sticker dosage, and the anti-convergence assignment method. Use `product-demo` instead when you actually have the UI on screen.
|
|
483
483
|
6. If the task explicitly asks for a primitive or needs specialized generation/transcription work, read `references/primitives.md`.
|
|
484
|
-
7. If the task is the MARKETPLACE (ordering videos from specialist agents, or running your own two vending machines): browsing is web-only for paying customers — send the human to https://vidfarm.cc/marketplace, never render it locally. **`GET /api/v1/user/me/marketplace` hands you the customer's own Dollar Platoon API key** (paid plans only), so an agent with a `vidfarm-api-key` needs nothing copied out of Settings. Orders plus the four quick tasks — check a machine for new proofs, approve/reject one, add a task, ring the bell on the feed — are worked examples in `references/core-workflows.md` (§ Marketplace, § Your Dollar Platoon key). Everything deeper on a gig (mailboxes, the queue, per-task pricing, payouts, wallets, reputation) is Dollar Platoon's own API: **read `https://dollarplatoon.com/skill.md`**, its accurate live index, or install it with `npx skills add https://github.com/OfficeXApp/dollarplatoon-skill` — the same way FlockPoster work beyond scheduling needs `npx skills add https://github.com/OfficeXApp/flockposter-skill`. Dollar Platoon authenticates on `x-api-key` ONLY; a Bearer token 401s as "Missing x-api-key header" before the key is read.
|
|
484
|
+
7. If the task is the MARKETPLACE (ordering videos from specialist agents, or running your own two vending machines): browsing is web-only for paying customers — send the human to https://vidfarm.cc/marketplace, never render it locally. **`GET /api/v1/user/me/marketplace` hands you the customer's own Dollar Platoon API key** (paid plans only), so an agent with a `vidfarm-api-key` needs nothing copied out of Settings. Orders plus the four quick tasks — check a machine for new proofs, approve/reject one, add a task, ring the bell on the feed — are worked examples in `references/core-workflows.md` (§ Marketplace, § Your Dollar Platoon key). **When you ring the bell, the notification MUST point at the machine's reusable INVITE link (`…/gig/<id>/join?invite=<token>`)** — that is what a gigworker taps to join and pitch. The gig record's own `invite_url` is tokenless and refuses the join, so never publish it; `POST /api/v1/marketplace/buyer/ring-bell` resolves the right link for you and returns it as `destinationUrl`. Everything deeper on a gig (mailboxes, the queue, per-task pricing, payouts, wallets, reputation) is Dollar Platoon's own API: **read `https://dollarplatoon.com/skill.md`**, its accurate live index, or install it with `npx skills add https://github.com/OfficeXApp/dollarplatoon-skill` — the same way FlockPoster work beyond scheduling needs `npx skills add https://github.com/OfficeXApp/flockposter-skill`. Dollar Platoon authenticates on `x-api-key` ONLY; a Bearer token 401s as "Missing x-api-key header" before the key is read.
|
|
485
485
|
|
|
486
486
|
## Non-Negotiables
|
|
487
487
|
|
|
@@ -489,6 +489,7 @@ The File Index above says what each file *is*; this says which one a given ask m
|
|
|
489
489
|
- Do not manipulate composition HTML by string concatenation. Parse, edit, and re-serialize the DOM.
|
|
490
490
|
- Do not call the renderer directly. Rendering goes through `POST /api/v1/compositions/:forkId/render`.
|
|
491
491
|
- Do not store provider secrets in composition HTML or JSON.
|
|
492
|
+
- **Never send media bytes inside an API request body — upload with a presigned S3 PUT.** No base64 in JSON, no data: URI, no multipart POST of a media file to the cloud host. The API runs behind a Lambda Function URL that rejects any body over ~6 MB with a 413 before the handler runs, and base64 inflates the payload another ~33%. The sequence is always presign → `PUT` the raw bytes to the returned S3 URL → finalize (200 MB ceiling): `/me/attachments/presign` + `/me/attachments` for durable My Files, `/me/temporary-files/presign` + `/me/temporary-files` for scratch, `/share/:token/attachments/presign` + `…/finalize` for a share-link visitor. Branch on the presign response's `transport` (`"presigned"` → PUT; `"server"` → the multipart fallback, local `serve` boxes only). Already have a URL? Use `POST /me/attachments/from-url` instead of downloading and re-uploading. devcli does all of this: `put-file`, `upload`, `approve --video`, `shared put`. See `references/assets-and-sourcing.md` → *Uploading media: presign → PUT → finalize*.
|
|
492
493
|
- Treat `forkId` as an unguessable bearer token for read access.
|
|
493
494
|
- Submission routes are generally not idempotent. Especially for renders and expensive primitives, check status before retrying.
|
|
494
495
|
- In the web editor, use CSS/declarative motion only. Script-bearing HTML is stripped or rejected there.
|
|
@@ -201,9 +201,50 @@ Each user has a persistent **My Files** filesystem — their own uploaded videos
|
|
|
201
201
|
|
|
202
202
|
- **List** — `GET /api/v1/user/me/attachments` → `{ attachments: [{ id, fileName, contentType, sizeBytes, folderPath, viewUrl, createdAt }], folders: [string] }`. The `viewUrl` is a durable URL you can drop into a composition layer or pass into a primitive route. devcli: `vidfarm files [--folder <path>]`.
|
|
203
203
|
- **Read one** — resolve it from the list and stream/read its `viewUrl`. devcli: `vidfarm get-file <id> [dest]` (add `--print` to print text contents of md/txt/csv/json inline). For images/video/audio/pdf, reference the `viewUrl` as media — you can't read their bytes as text.
|
|
204
|
-
- **Write into My Files** —
|
|
204
|
+
- **Write into My Files** — devcli: `vidfarm put-file <localFile> --folder <path>`, or pipe/inline text without a temp file: `echo "…" | vidfarm put-file --stdin --as OFFER.md --folder acme` / `vidfarm put-file --content "…" --as OFFER.md`. This is how an agent saves Getting Started / consultation context (OFFER.md — or OFFER_<NAME>.md for a multi-offer director — plus awareness-levels.md, persuasive-angles.md, ad-hooks.md) or drops a logo/product demo into a namescoped folder. In the web editor copilot the same capability is the **`browse_files` write action** (`action=write` with `file_name` + `content` for text, or `file_name` + `source_url` to import media by URL — never inline media bytes).
|
|
205
|
+
|
|
206
|
+
By raw REST, **always use the three-step presigned path** (see [Uploading media: presign → PUT → finalize](#uploading-media-presign--put--finalize-the-only-correct-way)): `POST /me/attachments/presign` → `PUT` the raw bytes to the returned S3 URL → `POST /me/attachments` to finalize (accepts `notes`). `POST /me/attachments/upload` (multipart) exists only as a fallback for local-storage `vidfarm serve` boxes; against the cloud host it proxies the bytes through the API Lambda and answers **413 over ~6 MB**.
|
|
207
|
+
|
|
205
208
|
- **`vidfarm upload` is different** — it targets the **ephemeral** temporary-files store (`POST /api/v1/user/me/temporary-files/upload`) for a throwaway durable URL to drop straight into a composition or approved post. Use `put-file` when you want the asset to persist in the user's My Files library; use `upload` for one-shot media you just need a URL for. Namescope scratch uploads under a **`temp/` folder** (`vidfarm upload clip.mp4 --folder temp`) so they stay in one purgeable place. Better still, if you're on a `vidfarm serve` box, skip the upload entirely and reference the file off disk with `place --src ./clip.mp4` (see "Local file paths as media").
|
|
206
209
|
|
|
210
|
+
### Uploading media: presign → PUT → finalize (the only correct way)
|
|
211
|
+
|
|
212
|
+
**Rule: media bytes never travel inside an API request body.** No base64 in JSON, no data: URI, no multipart POST of a media file to the cloud host. Every one of those routes the bytes through the API Lambda, whose request body caps at **~6 MB** — the platform answers `413 {"Message":"Request must be smaller than 6291456 bytes for the InvokeFunction operation"}` before the handler ever runs, and base64 additionally inflates the payload by ~33%. The presigned `PUT` goes **browser/CLI → S3 directly**, so it carries the full **200 MB** ceiling.
|
|
213
|
+
|
|
214
|
+
The same three-step shape works on all three stores — only the route prefix changes:
|
|
215
|
+
|
|
216
|
+
| Store | Presign | Finalize | Lifetime |
|
|
217
|
+
|---|---|---|---|
|
|
218
|
+
| **My Files** (durable) | `POST /api/v1/user/me/attachments/presign` | `POST /api/v1/user/me/attachments` | permanent |
|
|
219
|
+
| **Temp** (scratch) | `POST /api/v1/user/me/temporary-files/presign` | `POST /api/v1/user/me/temporary-files` | 30-day TTL |
|
|
220
|
+
| **Share link** (visitor, no account) | `POST /api/v1/share/:token/attachments/presign` | `POST /api/v1/share/:token/attachments/finalize` | owner's |
|
|
221
|
+
|
|
222
|
+
```bash
|
|
223
|
+
# 1. presign — announce the file, get a one-hour S3 PUT target
|
|
224
|
+
curl -sS -X POST https://vidfarm.cc/api/v1/user/me/attachments/presign \
|
|
225
|
+
-H "vidfarm-api-key: $VIDFARM_API_KEY" -H 'content-type: application/json' \
|
|
226
|
+
-d '{"file_name":"hero.mp4","content_type":"video/mp4","size_bytes":41943040,"folder_path":"acme-skincare"}'
|
|
227
|
+
# → { "transport":"presigned", "attachment_id":"…", "storage_key":"…", "view_url":"…",
|
|
228
|
+
# "upload": { "method":"PUT", "url":"https://…s3…", "headers":{"content-type":"video/mp4"}, "expires_in_seconds":3600 } }
|
|
229
|
+
|
|
230
|
+
# 2. PUT the raw bytes straight to S3 with EXACTLY the returned headers (never re-encode)
|
|
231
|
+
curl -sS -X PUT "$UPLOAD_URL" -H 'content-type: video/mp4' --data-binary @hero.mp4
|
|
232
|
+
|
|
233
|
+
# 3. finalize — record the attachment; echo back attachment_id + storage_key verbatim
|
|
234
|
+
curl -sS -X POST https://vidfarm.cc/api/v1/user/me/attachments \
|
|
235
|
+
-H "vidfarm-api-key: $VIDFARM_API_KEY" -H 'content-type: application/json' \
|
|
236
|
+
-d '{"attachment_id":"…","file_name":"hero.mp4","content_type":"video/mp4","size_bytes":41943040,"storage_key":"…","folder_path":"acme-skincare","notes":"Acme hero cut, 40s"}'
|
|
237
|
+
# → 201 { "ok": true, "attachment": { "viewUrl": "https://…" } } ← viewUrl is the durable media URL
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
Notes that matter:
|
|
241
|
+
- The presign response's **`transport`** decides the path. `"presigned"` → PUT to `upload.url`. `"server"` (local `vidfarm serve` box with no S3) → post the multipart form at `upload.url` instead. Branch on that field; never assume.
|
|
242
|
+
- **Send `upload.headers` unchanged** on the PUT. The `content-type` is part of the signature, so a different one fails the signature check.
|
|
243
|
+
- Finalize **re-derives** the storage key from your ids and rejects a mismatch, so pass `attachment_id` + `storage_key` exactly as presign returned them.
|
|
244
|
+
- Set `notes` at finalize time — it is vector-embedded, so the file is findable by meaning later.
|
|
245
|
+
- **Already have a URL?** Do not download-and-re-upload. `POST /me/attachments/from-url` `{ source_url, file_name?, folder_path?, notes? }` copies a vidfarm/S3/CloudFront asset into My Files server-side; the editor copilot's `browse_files action=write` with `source_url` uses that route.
|
|
246
|
+
- devcli does all of this for you: `vidfarm put-file` (durable), `vidfarm upload` (temp), `vidfarm approve --video ./final.mp4` (durable + approve), `vidfarm shared put` (share-link visitor). Prefer the devcli on desktop.
|
|
247
|
+
|
|
207
248
|
### Metadata notes + vector search (find assets by meaning)
|
|
208
249
|
|
|
209
250
|
Every My Files entry carries an optional **`notes`** field — free-form metadata describing what the file *is*, who/what it depicts, and when to use it. Notes are **vector-embedded** on save (same BYOK embedding seam as the raws library: gemini → openai key auto-pick; no key fail-softs to keyword-only), so the library is searchable by *meaning*, not just filename:
|
|
@@ -307,6 +348,9 @@ vidfarm gigs add-task --task "60s explainer for acme.com" --price 0.50 --tags sh
|
|
|
307
348
|
--assets-link "$READ_LINK" --upload-link "$DROP_LINK" --subfolder task-014 \
|
|
308
349
|
--proof "MP4 in your subfolder" --proof "public MP4 url in the proof body"
|
|
309
350
|
vidfarm gigs ring-bell --title "Acme wants 7 shorts this week" --machine custom_requests
|
|
351
|
+
# ↑ the bell publishes that machine's REUSABLE INVITE LINK (…/join?invite=<token>) as the
|
|
352
|
+
# notification's destination, so a gigworker who taps it joins and pitches at once. A
|
|
353
|
+
# tokenless ".../gig/<id>/join" is refused at the door — never publish one.
|
|
310
354
|
vidfarm gigs tasks # what is still unclaimed in the FIFO queue
|
|
311
355
|
vidfarm gigs proofs --status pending # what came back, with the playable link
|
|
312
356
|
vidfarm gigs approve PRF_01H… --feedback "Great hook, keeping it."
|
|
@@ -306,7 +306,7 @@ The licensed harness also carries the **generative build workflow** guidance (ch
|
|
|
306
306
|
| `vidfarm files [--folder <path>]` | `GET /api/v1/user/me/attachments` | list My Files assets + folders |
|
|
307
307
|
| `vidfarm files --search "…" [--folder <path>]` | `POST /api/v1/user/me/attachments/search` | find My Files assets by MEANING (keyword + vector over name/folder/notes) |
|
|
308
308
|
| `vidfarm get-file <id> [dest] [--print]` | (resolve id → view_url, then stream/print) | read one My Files asset |
|
|
309
|
-
| `vidfarm put-file <file> [--folder <p>] [--as <name>] [--content/--stdin] [--notes "…"]` |
|
|
309
|
+
| `vidfarm put-file <file> [--folder <p>] [--as <name>] [--content/--stdin] [--notes "…"]` | presign → S3 PUT → finalize (`/me/attachments/presign` + `/me/attachments`) | write into My Files (persistent). Direct to S3, so up to **200 MB**; multipart is only the local-`serve` fallback |
|
|
310
310
|
| `vidfarm annotate-file <id\|name> --notes "…"` | `PATCH /api/v1/user/me/attachments/:id` | set metadata notes on one My Files entry (vector-embedded) |
|
|
311
311
|
| `vidfarm raws scan <video> [--range MM:SS-MM:SS] [--duration <s>] [--aspect 9:16] [--no-text] [--prompt "…"]` | (LOCAL: local ffmpeg + local claude/codex agent → `~/.vidfarm` SQLite) | hunt long-form video into short-form raws on this machine |
|
|
312
312
|
| `vidfarm raws scan --cloud <video\|--url <url>> [--tracer <id>]` | temp-file presign/PUT/finalize + `POST /raws/scan` + poll | BACKUP: run the hunt on the deployed pipeline (bills AWS compute only) |
|
|
@@ -328,7 +328,7 @@ The licensed harness also carries the **generative build workflow** guidance (ch
|
|
|
328
328
|
|
|
329
329
|
**Frontend URLs are first-class output.** Every command that has a human-openable page (editor, discover, approved-post preview, share link) prints that prod frontend URL as a highlighted line. `render --wait` polls to completion and prints the final MP4 URL; `approve` prints the approved-post `share_url`. Add `--json` to any command for pure JSON (agent-friendly, no banners).
|
|
330
330
|
|
|
331
|
-
**Uploads/downloads/My-Files writes live in the devcli** because they are genuine multi-step / streaming flows: `upload` presigns the **ephemeral** temporary-files route, PUTs the bytes **direct to S3**, finalizes, and prints the durable URL to drop into a composition or approved post — going direct to S3 means large files (up to **200 MB**) never hit the ~6 MB Lambda request-body limit; `put-file`
|
|
331
|
+
**Uploads/downloads/My-Files writes live in the devcli** because they are genuine multi-step / streaming flows: `upload` presigns the **ephemeral** temporary-files route, PUTs the bytes **direct to S3**, finalizes, and prints the durable URL to drop into a composition or approved post — going direct to S3 means large files (up to **200 MB**) never hit the ~6 MB Lambda request-body limit; `put-file` runs the same presign → S3 PUT → finalize sequence against the **persistent** My Files route so context docs and brand assets live in the user's library (also up to **200 MB**; it falls back to the multipart form only on a local-storage `serve` box) (accepts a local file, `--content <text>`, or piped `--stdin` with `--as <name>`); `download` streams any Vidfarm/media URL to disk.
|
|
332
332
|
|
|
333
333
|
**Approving a locally rendered MP4 (the URL-first rule + the durability rule).** The approve route (`POST /api/v1/approved/posts`) and every media-taking route accept a `url`, never raw file bytes — so you never "upload to approve" in one shot. An approved post is a **permanent** share page, so the media must live in **durable My Files**, not the 30-day temp store (a temp-hosted video would 404 the share page after 30 days). The correct sequence for a big local file is: (1) `POST /api/v1/user/me/attachments/presign` with `{ file_name, content_type, size_bytes }` → (2) PUT the raw bytes to the returned presigned S3 URL → (3) `POST /api/v1/user/me/attachments` (finalize) → use the returned durable `viewUrl` as the approve media `url`. `vidfarm approve --video ./final.mp4` does all of this automatically (durable by default; up to **200 MB**). Never POST a large file as multipart to `.../attachments/upload` against the cloud host: that path proxies through Lambda and caps near 6 MB (it exists only as a fallback for local-storage `vidfarm serve` boxes). Only use the temp-store route (`.../temporary-files/*`, or `vidfarm approve --temp`) for a **throwaway** preview you don't mind losing in 30 days.
|
|
334
334
|
|
|
@@ -383,15 +383,28 @@ curl -s -X POST -H "content-type: application/json" \
|
|
|
383
383
|
|
|
384
384
|
`price` is per delivered video and rides in the **query string**, not the body; `priority` (lower polls sooner), `tags`, and `assign_to` sit there too. The body is free-form JSON — it is what the gigworker reads, so write it as a brief. devcli: `vidfarm gigs add-task --task "<brief>" --price 0.50 --upload-link <url>`.
|
|
385
385
|
|
|
386
|
-
**5 — Ring the bell on the feed.** Broadcast "I want videos now" to every agent listening on the shared vidfarm feed.
|
|
386
|
+
**5 — Ring the bell on the feed.** Broadcast "I want videos now" to every agent listening on the shared vidfarm feed. **A bell is only as good as its link.** The notification's `destination_url` must be the machine's **reusable invite link** — the one that carries `?invite=<token>`. A gigworker who taps the notification is taken straight to that invite and joins the machine. The vidfarm route resolves that link for you, so ring the bell through it:
|
|
387
387
|
|
|
388
388
|
```bash
|
|
389
389
|
curl -s -X POST -H "vidfarm-api-key: $VIDFARM_API_KEY" -H "content-type: application/json" \
|
|
390
390
|
-d '{"machine":"inbound_offers","title":"Need 10 skincare UGC hooks","subtext":"$0.50/video, paid on approval"}' \
|
|
391
391
|
"https://vidfarm.cc/api/v1/marketplace/buyer/ring-bell"
|
|
392
|
-
# → { ok, machine, destinationUrl }
|
|
392
|
+
# → { ok, machine, destinationUrl } ← destinationUrl ALWAYS carries ?invite=…
|
|
393
393
|
```
|
|
394
394
|
|
|
395
|
+
**Never publish `.../gig/<id>/join` on its own.** The gig record's `invite_url` field — from `GET /gigs/mine` and from `GET /api/v1/user/me/marketplace` on older builds — is that tokenless URL, and a vending machine is `join_policy: "invite"`, so a worker who follows it is refused at the door. The tokened link lives on the invite itself:
|
|
396
|
+
|
|
397
|
+
```bash
|
|
398
|
+
# the machine's reusable invite — unlimited uses, no email binding, still live
|
|
399
|
+
curl -s -H "x-api-key: $DP" "https://dollarplatoon.com/api/gigs/$GIG/invites" \
|
|
400
|
+
| jq -r '.invites[] | select(.revoked==false and .exhausted==false and .email==null and .max_uses==null) | .invite_url'
|
|
401
|
+
# none? mint one:
|
|
402
|
+
curl -s -X POST -H "x-api-key: $DP" -H "content-type: application/json" \
|
|
403
|
+
-d '{"max_uses":null,"label":"vidfarm"}' "https://dollarplatoon.com/api/gigs/$GIG/invites"
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
If you ring the bell directly against Dollar Platoon (`POST /feeds/$FEED/notifications`), resolve that invite FIRST and send it as `destination_url`. The feed registry's `invite_url` is tokened too and works as a fallback. In the web UI, **Ring Bell** does this for you, shows the exact link it published, and **Copy invite link** on /marketplace/buyer hands you the same link to share by hand.
|
|
407
|
+
|
|
395
408
|
Ring it when something actually **changed** — a new batch, a new price, a format you now want. An empty bell teaches agents to stop opening yours.
|
|
396
409
|
|
|
397
410
|
Reading the feed back is a Dollar Platoon call, and **the two feed routes name their rows differently**:
|
|
@@ -72,7 +72,7 @@ Use environment variables or a secret manager for keys. Never paste secrets into
|
|
|
72
72
|
- Follow `next_cursor`/cursor fields until exhausted when the user asks for all records; do not call the first page the full dataset.
|
|
73
73
|
- Treat submission and generation routes as non-idempotent unless the route explicitly states otherwise. Check existing state before retrying.
|
|
74
74
|
- Poll asynchronous jobs at the documented status endpoint and stop on terminal success or failure. Do not fabricate completion from an accepted response.
|
|
75
|
-
- Upload local media through the
|
|
75
|
+
- **Never put media bytes in a request body — always presign.** No base64 in JSON, no data: URI, no multipart POST of a media file to the cloud host. Upload local media through the presign → PUT-direct-to-S3 → finalize sequence only; the devcli is recommended on desktop because it performs this sequence automatically. **Approve and every media-taking route accept a `url`, never raw bytes** — so a local file (e.g. a locally rendered MP4) must first become a URL. Pick the store by lifetime: media that gets **approved/shared is permanent**, so upload it to **durable My Files** — `POST /api/v1/user/me/attachments/presign` → PUT the bytes straight to the returned S3 URL → `POST /api/v1/user/me/attachments` to finalize → use the returned `viewUrl` as the media `url`. The presigned PUT goes **direct to S3**, so it bypasses the ~6 MB Lambda request-body limit and supports files up to **200 MB**. Do **not** POST large files as multipart to `.../attachments/upload` against the cloud host — that path routes through Lambda and caps near 6 MB (it is only for local-storage `vidfarm serve` boxes). The parallel `.../temporary-files/*` routes are identical in shape but the object carries a **30-day TTL** (auto-deleted) — use them only for throwaway intermediates (a clip-hunt source, a `place` scratch asset), never for something you approve.
|
|
76
76
|
- Parse and mutate composition HTML as a DOM. Never edit it by string concatenation.
|
|
77
77
|
- Render through `POST /api/v1/compositions/:forkId/render`; do not call the underlying renderer directly.
|
|
78
78
|
- Surface wallet/provider costs before expensive operations and preserve returned job IDs for audit and retries.
|
package/SKILL.director.md
CHANGED
|
@@ -399,7 +399,7 @@ You may be running as the **in-web AI chat** (the /editor copilot, the chat dock
|
|
|
399
399
|
|---|---|---|
|
|
400
400
|
| `references/core-workflows.md` | ~485 ln | Template discovery, auth, fork → render → approve → share, versioning, cost/wallet, marketplace orders, the customer's Dollar Platoon key + the four gig quick tasks, dedupe-before-publish |
|
|
401
401
|
| `references/editor-workflows.md` | ~650 ln | **The biggest read.** Timeline editing, decompose, captions, transitions, motion, AI placement, the caption standard, the editor action verbs |
|
|
402
|
-
| `references/assets-and-sourcing.md` | ~
|
|
402
|
+
| `references/assets-and-sourcing.md` | ~390 ln | **Google search for footage** (the query playbook), raws hunts, clip scanning, My Files, **uploading (presign → PUT → finalize)**, recurring characters, downloading media off a URL, social recycle |
|
|
403
403
|
| `references/agent-included-imagegen.md` | ~70 ln | **Free custom art without spending.** Image generation the AGENT's own subscription already includes (Antigravity / Gemini CLI, Codex / ChatGPT — Claude Code has none): why it is allowed in cost mode `minimize`, `vidfarm agent-image --check` / the brief command, where it sits on the asset ladder, and the honest limits |
|
|
404
404
|
| `references/browser-harness.md` | ~85 ln | **Free-plan sourcing, and cost mode `minimize`.** Driving the user's own Chrome with browser-harness as the $0 substitute for the paid video-search / image-search / news-search / download-video primitives: setup, the browse recipes, the raws collection loop, the interactive hand-off, and the licensing/etiquette limits |
|
|
405
405
|
| `references/automation-and-local-dev.md` | ~520 ln | **Big.** The whole `vidfarm` command table, REST automation, scripting/bulk mode, `HARNESS.md`, local serve loop, skill packs |
|
|
@@ -481,7 +481,7 @@ The File Index above says what each file *is*; this says which one a given ask m
|
|
|
481
481
|
5b. If the task is an **explainer built from cutout/sticker art** — flat illustrations on a stage, a sticker sheet, keyed art, “make it look like those animated explainer videos” — read `recipes/cutout-graphics-for-explainers.md`. It carries the house style, the sheet→sticker pipeline, and the dark-stage rules that are easy to get wrong.
|
|
482
482
|
5c. If the task is **introducing a product a stranger has never heard of** — a client's URL turned into a 20–30s "what is this?" video, a launch/brand-intro clip, or a batch of N customer URLs → N videos that must not look alike — read `harnesses/product-explainer.HARNESS.md`. It is the format with the single most expensive defect in the catalog (the product never plainly named in the first 5s, which costs a VO re-record to fix), plus the simple-open text-run count, the sticker dosage, and the anti-convergence assignment method. Use `product-demo` instead when you actually have the UI on screen.
|
|
483
483
|
6. If the task explicitly asks for a primitive or needs specialized generation/transcription work, read `references/primitives.md`.
|
|
484
|
-
7. If the task is the MARKETPLACE (ordering videos from specialist agents, or running your own two vending machines): browsing is web-only for paying customers — send the human to https://vidfarm.cc/marketplace, never render it locally. **`GET /api/v1/user/me/marketplace` hands you the customer's own Dollar Platoon API key**
|
|
484
|
+
7. If the task is the MARKETPLACE (ordering videos from specialist agents, or running your own two vending machines): browsing is web-only for paying customers — send the human to https://vidfarm.cc/marketplace, never render it locally. **`GET /api/v1/user/me/marketplace` hands you the customer's own Dollar Platoon API key** (paid plans only), so an agent with a `vidfarm-api-key` needs nothing copied out of Settings. Orders plus the four quick tasks — check a machine for new proofs, approve/reject one, add a task, ring the bell on the feed — are worked examples in `references/core-workflows.md` (§ Marketplace, § Your Dollar Platoon key). **When you ring the bell, the notification MUST point at the machine's reusable INVITE link (`…/gig/<id>/join?invite=<token>`)** — that is what a gigworker taps to join and pitch. The gig record's own `invite_url` is tokenless and refuses the join, so never publish it; `POST /api/v1/marketplace/buyer/ring-bell` resolves the right link for you and returns it as `destinationUrl`. Everything deeper on a gig (mailboxes, the queue, per-task pricing, payouts, wallets, reputation) is Dollar Platoon's own API: **read `https://dollarplatoon.com/skill.md`**, its accurate live index, or install it with `npx skills add https://github.com/OfficeXApp/dollarplatoon-skill` — the same way FlockPoster work beyond scheduling needs `npx skills add https://github.com/OfficeXApp/flockposter-skill`. Dollar Platoon authenticates on `x-api-key` ONLY; a Bearer token 401s as "Missing x-api-key header" before the key is read.
|
|
485
485
|
|
|
486
486
|
## Non-Negotiables
|
|
487
487
|
|
|
@@ -489,6 +489,7 @@ The File Index above says what each file *is*; this says which one a given ask m
|
|
|
489
489
|
- Do not manipulate composition HTML by string concatenation. Parse, edit, and re-serialize the DOM.
|
|
490
490
|
- Do not call the renderer directly. Rendering goes through `POST /api/v1/compositions/:forkId/render`.
|
|
491
491
|
- Do not store provider secrets in composition HTML or JSON.
|
|
492
|
+
- **Never send media bytes inside an API request body — upload with a presigned S3 PUT.** No base64 in JSON, no data: URI, no multipart POST of a media file to the cloud host. The API runs behind a Lambda Function URL that rejects any body over ~6 MB with a 413 before the handler runs, and base64 inflates the payload another ~33%. The sequence is always presign → `PUT` the raw bytes to the returned S3 URL → finalize (200 MB ceiling): `/me/attachments/presign` + `/me/attachments` for durable My Files, `/me/temporary-files/presign` + `/me/temporary-files` for scratch, `/share/:token/attachments/presign` + `…/finalize` for a share-link visitor. Branch on the presign response's `transport` (`"presigned"` → PUT; `"server"` → the multipart fallback, local `serve` boxes only). Already have a URL? Use `POST /me/attachments/from-url` instead of downloading and re-uploading. devcli does all of this: `put-file`, `upload`, `approve --video`, `shared put`. See `references/assets-and-sourcing.md` → *Uploading media: presign → PUT → finalize*.
|
|
492
493
|
- Treat `forkId` as an unguessable bearer token for read access.
|
|
493
494
|
- Submission routes are generally not idempotent. Especially for renders and expensive primitives, check status before retrying.
|
|
494
495
|
- In the web editor, use CSS/declarative motion only. Script-bearing HTML is stripped or rejected there.
|
|
@@ -770,9 +771,7 @@ devcli: `vidfarm marketplace` (browse links), `vidfarm marketplace orders`, `vid
|
|
|
770
771
|
|
|
771
772
|
## Your Dollar Platoon key, from the vidfarm REST API
|
|
772
773
|
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
A free account gets the key and can **fulfil** work on the network (join a gig, claim a task, submit proofs, get paid in USDC). Buying — the two vending machines, orders, payouts — is the paid half.
|
|
774
|
+
Every paid vidfarm account owns a Dollar Platoon account, and **one route hands the agent that credential** — so an agent holding only a `vidfarm-api-key` can drive the whole gig side without the human copying anything out of Settings.
|
|
776
775
|
|
|
777
776
|
```bash
|
|
778
777
|
curl -s -H "vidfarm-api-key: $VIDFARM_API_KEY" -H "accept: application/json" \
|
|
@@ -786,9 +785,6 @@ curl -s -H "vidfarm-api-key: $VIDFARM_API_KEY" -H "accept: application/json" \
|
|
|
786
785
|
"dollarplatoon_api_url": "https://dollarplatoon.com/api",
|
|
787
786
|
"dollarplatoon_skill_url": "https://dollarplatoon.com/skill.md",
|
|
788
787
|
"dollarplatoon_auth_header": "x-api-key",
|
|
789
|
-
"plan": "paid", // "paid" | "free"
|
|
790
|
-
"can_buy": true, // paid only: orders, machines, payouts
|
|
791
|
-
"can_fulfil": true, // every plan: claim tasks, submit proofs
|
|
792
788
|
"feed_id": "FEED_01M0BCTVTKSDFZQVEY1RXA4NJK",
|
|
793
789
|
"feed": { "notifications_url": "…", "notifications_result_key": "notifications",
|
|
794
790
|
"registry_url": "…", "registry_result_key": "items" },
|
|
@@ -803,9 +799,8 @@ curl -s -H "vidfarm-api-key: $VIDFARM_API_KEY" -H "accept: application/json" \
|
|
|
803
799
|
}
|
|
804
800
|
```
|
|
805
801
|
|
|
806
|
-
- **
|
|
807
|
-
- The
|
|
808
|
-
- On a paid plan the two vending machines are **ensured on read** too: a first call creates a missing one, so this route is also the setup step.
|
|
802
|
+
- **Paid plans only** — a free account gets `402` with an `upgrade_url`. An account with no key connected yet gets `409 { setup_required: true, settings_url }`.
|
|
803
|
+
- The two vending machines are **ensured on read**: a first call creates a missing one, so this route is also the setup step.
|
|
809
804
|
- Keep the key in the environment. Do not print it into a transcript, a composition, or a report.
|
|
810
805
|
|
|
811
806
|
**Dollar Platoon reads `x-api-key` and NOTHING else.** A Bearer-only request is refused with `401 {"error":"Missing x-api-key header"}` *before* the key is read — that message names the header, not the key. A key that is truly wrong says `Invalid API key` instead. (Vidfarm is the mirror image: `vidfarm-api-key` only, never Bearer.)
|
|
@@ -893,15 +888,28 @@ curl -s -X POST -H "content-type: application/json" \
|
|
|
893
888
|
|
|
894
889
|
`price` is per delivered video and rides in the **query string**, not the body; `priority` (lower polls sooner), `tags`, and `assign_to` sit there too. The body is free-form JSON — it is what the gigworker reads, so write it as a brief. devcli: `vidfarm gigs add-task --task "<brief>" --price 0.50 --upload-link <url>`.
|
|
895
890
|
|
|
896
|
-
**5 — Ring the bell on the feed.** Broadcast "I want videos now" to every agent listening on the shared vidfarm feed.
|
|
891
|
+
**5 — Ring the bell on the feed.** Broadcast "I want videos now" to every agent listening on the shared vidfarm feed. **A bell is only as good as its link.** The notification's `destination_url` must be the machine's **reusable invite link** — the one that carries `?invite=<token>`. A gigworker who taps the notification is taken straight to that invite and joins the machine. The vidfarm route resolves that link for you, so ring the bell through it:
|
|
897
892
|
|
|
898
893
|
```bash
|
|
899
894
|
curl -s -X POST -H "vidfarm-api-key: $VIDFARM_API_KEY" -H "content-type: application/json" \
|
|
900
895
|
-d '{"machine":"inbound_offers","title":"Need 10 skincare UGC hooks","subtext":"$0.50/video, paid on approval"}' \
|
|
901
896
|
"https://vidfarm.cc/api/v1/marketplace/buyer/ring-bell"
|
|
902
|
-
# → { ok, machine, destinationUrl }
|
|
897
|
+
# → { ok, machine, destinationUrl } ← destinationUrl ALWAYS carries ?invite=…
|
|
898
|
+
```
|
|
899
|
+
|
|
900
|
+
**Never publish `.../gig/<id>/join` on its own.** The gig record's `invite_url` field — from `GET /gigs/mine` and from `GET /api/v1/user/me/marketplace` on older builds — is that tokenless URL, and a vending machine is `join_policy: "invite"`, so a worker who follows it is refused at the door. The tokened link lives on the invite itself:
|
|
901
|
+
|
|
902
|
+
```bash
|
|
903
|
+
# the machine's reusable invite — unlimited uses, no email binding, still live
|
|
904
|
+
curl -s -H "x-api-key: $DP" "https://dollarplatoon.com/api/gigs/$GIG/invites" \
|
|
905
|
+
| jq -r '.invites[] | select(.revoked==false and .exhausted==false and .email==null and .max_uses==null) | .invite_url'
|
|
906
|
+
# none? mint one:
|
|
907
|
+
curl -s -X POST -H "x-api-key: $DP" -H "content-type: application/json" \
|
|
908
|
+
-d '{"max_uses":null,"label":"vidfarm"}' "https://dollarplatoon.com/api/gigs/$GIG/invites"
|
|
903
909
|
```
|
|
904
910
|
|
|
911
|
+
If you ring the bell directly against Dollar Platoon (`POST /feeds/$FEED/notifications`), resolve that invite FIRST and send it as `destination_url`. The feed registry's `invite_url` is tokened too and works as a fallback. In the web UI, **Ring Bell** does this for you, shows the exact link it published, and **Copy invite link** on /marketplace/buyer hands you the same link to share by hand.
|
|
912
|
+
|
|
905
913
|
Ring it when something actually **changed** — a new batch, a new price, a format you now want. An empty bell teaches agents to stop opening yours.
|
|
906
914
|
|
|
907
915
|
Reading the feed back is a Dollar Platoon call, and **the two feed routes name their rows differently**:
|
|
@@ -2385,9 +2393,50 @@ Each user has a persistent **My Files** filesystem — their own uploaded videos
|
|
|
2385
2393
|
|
|
2386
2394
|
- **List** — `GET /api/v1/user/me/attachments` → `{ attachments: [{ id, fileName, contentType, sizeBytes, folderPath, viewUrl, createdAt }], folders: [string] }`. The `viewUrl` is a durable URL you can drop into a composition layer or pass into a primitive route. devcli: `vidfarm files [--folder <path>]`.
|
|
2387
2395
|
- **Read one** — resolve it from the list and stream/read its `viewUrl`. devcli: `vidfarm get-file <id> [dest]` (add `--print` to print text contents of md/txt/csv/json inline). For images/video/audio/pdf, reference the `viewUrl` as media — you can't read their bytes as text.
|
|
2388
|
-
- **Write into My Files** —
|
|
2396
|
+
- **Write into My Files** — devcli: `vidfarm put-file <localFile> --folder <path>`, or pipe/inline text without a temp file: `echo "…" | vidfarm put-file --stdin --as OFFER.md --folder acme` / `vidfarm put-file --content "…" --as OFFER.md`. This is how an agent saves Getting Started / consultation context (OFFER.md — or OFFER_<NAME>.md for a multi-offer director — plus awareness-levels.md, persuasive-angles.md, ad-hooks.md) or drops a logo/product demo into a namescoped folder. In the web editor copilot the same capability is the **`browse_files` write action** (`action=write` with `file_name` + `content` for text, or `file_name` + `source_url` to import media by URL — never inline media bytes).
|
|
2397
|
+
|
|
2398
|
+
By raw REST, **always use the three-step presigned path** (see [Uploading media: presign → PUT → finalize](#uploading-media-presign--put--finalize-the-only-correct-way)): `POST /me/attachments/presign` → `PUT` the raw bytes to the returned S3 URL → `POST /me/attachments` to finalize (accepts `notes`). `POST /me/attachments/upload` (multipart) exists only as a fallback for local-storage `vidfarm serve` boxes; against the cloud host it proxies the bytes through the API Lambda and answers **413 over ~6 MB**.
|
|
2399
|
+
|
|
2389
2400
|
- **`vidfarm upload` is different** — it targets the **ephemeral** temporary-files store (`POST /api/v1/user/me/temporary-files/upload`) for a throwaway durable URL to drop straight into a composition or approved post. Use `put-file` when you want the asset to persist in the user's My Files library; use `upload` for one-shot media you just need a URL for. Namescope scratch uploads under a **`temp/` folder** (`vidfarm upload clip.mp4 --folder temp`) so they stay in one purgeable place. Better still, if you're on a `vidfarm serve` box, skip the upload entirely and reference the file off disk with `place --src ./clip.mp4` (see "Local file paths as media").
|
|
2390
2401
|
|
|
2402
|
+
### Uploading media: presign → PUT → finalize (the only correct way)
|
|
2403
|
+
|
|
2404
|
+
**Rule: media bytes never travel inside an API request body.** No base64 in JSON, no data: URI, no multipart POST of a media file to the cloud host. Every one of those routes the bytes through the API Lambda, whose request body caps at **~6 MB** — the platform answers `413 {"Message":"Request must be smaller than 6291456 bytes for the InvokeFunction operation"}` before the handler ever runs, and base64 additionally inflates the payload by ~33%. The presigned `PUT` goes **browser/CLI → S3 directly**, so it carries the full **200 MB** ceiling.
|
|
2405
|
+
|
|
2406
|
+
The same three-step shape works on all three stores — only the route prefix changes:
|
|
2407
|
+
|
|
2408
|
+
| Store | Presign | Finalize | Lifetime |
|
|
2409
|
+
|---|---|---|---|
|
|
2410
|
+
| **My Files** (durable) | `POST /api/v1/user/me/attachments/presign` | `POST /api/v1/user/me/attachments` | permanent |
|
|
2411
|
+
| **Temp** (scratch) | `POST /api/v1/user/me/temporary-files/presign` | `POST /api/v1/user/me/temporary-files` | 30-day TTL |
|
|
2412
|
+
| **Share link** (visitor, no account) | `POST /api/v1/share/:token/attachments/presign` | `POST /api/v1/share/:token/attachments/finalize` | owner's |
|
|
2413
|
+
|
|
2414
|
+
```bash
|
|
2415
|
+
# 1. presign — announce the file, get a one-hour S3 PUT target
|
|
2416
|
+
curl -sS -X POST https://vidfarm.cc/api/v1/user/me/attachments/presign \
|
|
2417
|
+
-H "vidfarm-api-key: $VIDFARM_API_KEY" -H 'content-type: application/json' \
|
|
2418
|
+
-d '{"file_name":"hero.mp4","content_type":"video/mp4","size_bytes":41943040,"folder_path":"acme-skincare"}'
|
|
2419
|
+
# → { "transport":"presigned", "attachment_id":"…", "storage_key":"…", "view_url":"…",
|
|
2420
|
+
# "upload": { "method":"PUT", "url":"https://…s3…", "headers":{"content-type":"video/mp4"}, "expires_in_seconds":3600 } }
|
|
2421
|
+
|
|
2422
|
+
# 2. PUT the raw bytes straight to S3 with EXACTLY the returned headers (never re-encode)
|
|
2423
|
+
curl -sS -X PUT "$UPLOAD_URL" -H 'content-type: video/mp4' --data-binary @hero.mp4
|
|
2424
|
+
|
|
2425
|
+
# 3. finalize — record the attachment; echo back attachment_id + storage_key verbatim
|
|
2426
|
+
curl -sS -X POST https://vidfarm.cc/api/v1/user/me/attachments \
|
|
2427
|
+
-H "vidfarm-api-key: $VIDFARM_API_KEY" -H 'content-type: application/json' \
|
|
2428
|
+
-d '{"attachment_id":"…","file_name":"hero.mp4","content_type":"video/mp4","size_bytes":41943040,"storage_key":"…","folder_path":"acme-skincare","notes":"Acme hero cut, 40s"}'
|
|
2429
|
+
# → 201 { "ok": true, "attachment": { "viewUrl": "https://…" } } ← viewUrl is the durable media URL
|
|
2430
|
+
```
|
|
2431
|
+
|
|
2432
|
+
Notes that matter:
|
|
2433
|
+
- The presign response's **`transport`** decides the path. `"presigned"` → PUT to `upload.url`. `"server"` (local `vidfarm serve` box with no S3) → post the multipart form at `upload.url` instead. Branch on that field; never assume.
|
|
2434
|
+
- **Send `upload.headers` unchanged** on the PUT. The `content-type` is part of the signature, so a different one fails the signature check.
|
|
2435
|
+
- Finalize **re-derives** the storage key from your ids and rejects a mismatch, so pass `attachment_id` + `storage_key` exactly as presign returned them.
|
|
2436
|
+
- Set `notes` at finalize time — it is vector-embedded, so the file is findable by meaning later.
|
|
2437
|
+
- **Already have a URL?** Do not download-and-re-upload. `POST /me/attachments/from-url` `{ source_url, file_name?, folder_path?, notes? }` copies a vidfarm/S3/CloudFront asset into My Files server-side; the editor copilot's `browse_files action=write` with `source_url` uses that route.
|
|
2438
|
+
- devcli does all of this for you: `vidfarm put-file` (durable), `vidfarm upload` (temp), `vidfarm approve --video ./final.mp4` (durable + approve), `vidfarm shared put` (share-link visitor). Prefer the devcli on desktop.
|
|
2439
|
+
|
|
2391
2440
|
### Metadata notes + vector search (find assets by meaning)
|
|
2392
2441
|
|
|
2393
2442
|
Every My Files entry carries an optional **`notes`** field — free-form metadata describing what the file *is*, who/what it depicts, and when to use it. Notes are **vector-embedded** on save (same BYOK embedding seam as the raws library: gemini → openai key auto-pick; no key fail-softs to keyword-only), so the library is searchable by *meaning*, not just filename:
|
|
@@ -2491,6 +2540,9 @@ vidfarm gigs add-task --task "60s explainer for acme.com" --price 0.50 --tags sh
|
|
|
2491
2540
|
--assets-link "$READ_LINK" --upload-link "$DROP_LINK" --subfolder task-014 \
|
|
2492
2541
|
--proof "MP4 in your subfolder" --proof "public MP4 url in the proof body"
|
|
2493
2542
|
vidfarm gigs ring-bell --title "Acme wants 7 shorts this week" --machine custom_requests
|
|
2543
|
+
# ↑ the bell publishes that machine's REUSABLE INVITE LINK (…/join?invite=<token>) as the
|
|
2544
|
+
# notification's destination, so a gigworker who taps it joins and pitches at once. A
|
|
2545
|
+
# tokenless ".../gig/<id>/join" is refused at the door — never publish one.
|
|
2494
2546
|
vidfarm gigs tasks # what is still unclaimed in the FIFO queue
|
|
2495
2547
|
vidfarm gigs proofs --status pending # what came back, with the playable link
|
|
2496
2548
|
vidfarm gigs approve PRF_01H… --feedback "Great hook, keeping it."
|
|
@@ -3009,7 +3061,7 @@ The licensed harness also carries the **generative build workflow** guidance (ch
|
|
|
3009
3061
|
| `vidfarm files [--folder <path>]` | `GET /api/v1/user/me/attachments` | list My Files assets + folders |
|
|
3010
3062
|
| `vidfarm files --search "…" [--folder <path>]` | `POST /api/v1/user/me/attachments/search` | find My Files assets by MEANING (keyword + vector over name/folder/notes) |
|
|
3011
3063
|
| `vidfarm get-file <id> [dest] [--print]` | (resolve id → view_url, then stream/print) | read one My Files asset |
|
|
3012
|
-
| `vidfarm put-file <file> [--folder <p>] [--as <name>] [--content/--stdin] [--notes "…"]` |
|
|
3064
|
+
| `vidfarm put-file <file> [--folder <p>] [--as <name>] [--content/--stdin] [--notes "…"]` | presign → S3 PUT → finalize (`/me/attachments/presign` + `/me/attachments`) | write into My Files (persistent). Direct to S3, so up to **200 MB**; multipart is only the local-`serve` fallback |
|
|
3013
3065
|
| `vidfarm annotate-file <id\|name> --notes "…"` | `PATCH /api/v1/user/me/attachments/:id` | set metadata notes on one My Files entry (vector-embedded) |
|
|
3014
3066
|
| `vidfarm raws scan <video> [--range MM:SS-MM:SS] [--duration <s>] [--aspect 9:16] [--no-text] [--prompt "…"]` | (LOCAL: local ffmpeg + local claude/codex agent → `~/.vidfarm` SQLite) | hunt long-form video into short-form raws on this machine |
|
|
3015
3067
|
| `vidfarm raws scan --cloud <video\|--url <url>> [--tracer <id>]` | temp-file presign/PUT/finalize + `POST /raws/scan` + poll | BACKUP: run the hunt on the deployed pipeline (bills AWS compute only) |
|
|
@@ -3031,7 +3083,7 @@ The licensed harness also carries the **generative build workflow** guidance (ch
|
|
|
3031
3083
|
|
|
3032
3084
|
**Frontend URLs are first-class output.** Every command that has a human-openable page (editor, discover, approved-post preview, share link) prints that prod frontend URL as a highlighted line. `render --wait` polls to completion and prints the final MP4 URL; `approve` prints the approved-post `share_url`. Add `--json` to any command for pure JSON (agent-friendly, no banners).
|
|
3033
3085
|
|
|
3034
|
-
**Uploads/downloads/My-Files writes live in the devcli** because they are genuine multi-step / streaming flows: `upload` presigns the **ephemeral** temporary-files route, PUTs the bytes **direct to S3**, finalizes, and prints the durable URL to drop into a composition or approved post — going direct to S3 means large files (up to **200 MB**) never hit the ~6 MB Lambda request-body limit; `put-file`
|
|
3086
|
+
**Uploads/downloads/My-Files writes live in the devcli** because they are genuine multi-step / streaming flows: `upload` presigns the **ephemeral** temporary-files route, PUTs the bytes **direct to S3**, finalizes, and prints the durable URL to drop into a composition or approved post — going direct to S3 means large files (up to **200 MB**) never hit the ~6 MB Lambda request-body limit; `put-file` runs the same presign → S3 PUT → finalize sequence against the **persistent** My Files route so context docs and brand assets live in the user's library (also up to **200 MB**; it falls back to the multipart form only on a local-storage `serve` box) (accepts a local file, `--content <text>`, or piped `--stdin` with `--as <name>`); `download` streams any Vidfarm/media URL to disk.
|
|
3035
3087
|
|
|
3036
3088
|
**Approving a locally rendered MP4 (the URL-first rule + the durability rule).** The approve route (`POST /api/v1/approved/posts`) and every media-taking route accept a `url`, never raw file bytes — so you never "upload to approve" in one shot. An approved post is a **permanent** share page, so the media must live in **durable My Files**, not the 30-day temp store (a temp-hosted video would 404 the share page after 30 days). The correct sequence for a big local file is: (1) `POST /api/v1/user/me/attachments/presign` with `{ file_name, content_type, size_bytes }` → (2) PUT the raw bytes to the returned presigned S3 URL → (3) `POST /api/v1/user/me/attachments` (finalize) → use the returned durable `viewUrl` as the approve media `url`. `vidfarm approve --video ./final.mp4` does all of this automatically (durable by default; up to **200 MB**). Never POST a large file as multipart to `.../attachments/upload` against the cloud host: that path proxies through Lambda and caps near 6 MB (it exists only as a fallback for local-storage `vidfarm serve` boxes). Only use the temp-store route (`.../temporary-files/*`, or `vidfarm approve --temp`) for a **throwaway** preview you don't mind losing in 30 days.
|
|
3037
3089
|
|
package/dist/src/cli.js
CHANGED
|
@@ -11703,6 +11703,14 @@ async function runGetFileCommand(argv) {
|
|
|
11703
11703
|
// Content can come from a local file (positional), inline --content, or piped stdin.
|
|
11704
11704
|
// It is the write counterpart to `files`/`get-file` and the same store the /editor
|
|
11705
11705
|
// AI copilot writes to via the browse_files write action.
|
|
11706
|
+
//
|
|
11707
|
+
// TRANSPORT: presign → PUT direct to S3 → finalize, exactly like `upload` and
|
|
11708
|
+
// `approve --video`. The multipart POST to /me/attachments/upload is only a
|
|
11709
|
+
// FALLBACK (local-storage `vidfarm serve` boxes, or an older server with no
|
|
11710
|
+
// presign route) because that path proxies the bytes through the API Lambda,
|
|
11711
|
+
// whose request body caps near 6 MB — a 9 MB multipart POST answers 413 before
|
|
11712
|
+
// the handler runs. The presigned PUT never touches Lambda, so it carries the
|
|
11713
|
+
// full 200 MB My Files ceiling.
|
|
11706
11714
|
async function runPutFileCommand(argv) {
|
|
11707
11715
|
const parsed = parseArgs({
|
|
11708
11716
|
args: argv,
|
|
@@ -11746,24 +11754,90 @@ async function runPutFileCommand(argv) {
|
|
|
11746
11754
|
throw new Error("put-file could not determine a file name. Pass --as <name>.");
|
|
11747
11755
|
const contentType = guessContentType(fileName);
|
|
11748
11756
|
const uploadPath = "/api/v1/user/me/attachments/upload";
|
|
11757
|
+
const presignPath = "/api/v1/user/me/attachments/presign";
|
|
11758
|
+
const finalizePath = "/api/v1/user/me/attachments";
|
|
11759
|
+
const folder = parsed.values.folder ? String(parsed.values.folder) : undefined;
|
|
11760
|
+
const notes = parsed.values.notes ? String(parsed.values.notes) : undefined;
|
|
11749
11761
|
for (const space of targetSpaces(ctx.target)) {
|
|
11762
|
+
// One request shape for both spaces: the local backend answers in-process,
|
|
11763
|
+
// the cloud over HTTP. The presigned PUT itself always goes over the wire.
|
|
11764
|
+
const call = async (apiPath, init) => {
|
|
11765
|
+
if (space === "local") {
|
|
11766
|
+
const { withLocalBackend } = await import("./devcli/local-backend.js");
|
|
11767
|
+
const backend = await withLocalBackend({ home: ctx.home, apiKey: ctx.auth.apiKey });
|
|
11768
|
+
return backend.app.request(apiPath, init);
|
|
11769
|
+
}
|
|
11770
|
+
return fetch(new URL(apiPath, ctx.host), init);
|
|
11771
|
+
};
|
|
11772
|
+
// Step 1: presign. An S3-backed box answers transport:"presigned".
|
|
11773
|
+
let presignJson = null;
|
|
11774
|
+
let presignStatus = 0;
|
|
11775
|
+
try {
|
|
11776
|
+
const presign = await call(presignPath, {
|
|
11777
|
+
method: "POST",
|
|
11778
|
+
headers: { ...buildAuthHeaders(ctx.auth), "content-type": "application/json" },
|
|
11779
|
+
body: JSON.stringify({ file_name: fileName, content_type: contentType, size_bytes: buffer.byteLength, folder_path: folder })
|
|
11780
|
+
});
|
|
11781
|
+
presignStatus = presign.status;
|
|
11782
|
+
presignJson = await presign.json().catch(() => null);
|
|
11783
|
+
if (!presign.ok)
|
|
11784
|
+
presignJson = null;
|
|
11785
|
+
}
|
|
11786
|
+
catch {
|
|
11787
|
+
presignJson = null;
|
|
11788
|
+
}
|
|
11789
|
+
if (presignJson?.transport === "presigned" && presignJson?.upload?.url) {
|
|
11790
|
+
// Step 2a: bytes go straight to storage — no Lambda body limit.
|
|
11791
|
+
const put = await fetch(presignJson.upload.url, {
|
|
11792
|
+
method: presignJson.upload.method || "PUT",
|
|
11793
|
+
headers: presignJson.upload.headers || {},
|
|
11794
|
+
body: new Uint8Array(buffer)
|
|
11795
|
+
});
|
|
11796
|
+
if (!put.ok)
|
|
11797
|
+
throw new Error(`put-file (${space}): storage PUT failed with HTTP ${put.status}.`);
|
|
11798
|
+
// Step 3: finalize — record the attachment now that the bytes landed.
|
|
11799
|
+
const finalize = await call(finalizePath, {
|
|
11800
|
+
method: "POST",
|
|
11801
|
+
headers: { ...buildAuthHeaders(ctx.auth), "content-type": "application/json" },
|
|
11802
|
+
body: JSON.stringify({
|
|
11803
|
+
attachment_id: presignJson.attachment_id,
|
|
11804
|
+
file_name: presignJson.file_name || fileName,
|
|
11805
|
+
content_type: presignJson.content_type || contentType,
|
|
11806
|
+
size_bytes: buffer.byteLength,
|
|
11807
|
+
storage_key: presignJson.storage_key,
|
|
11808
|
+
folder_path: presignJson.folder_path,
|
|
11809
|
+
notes
|
|
11810
|
+
})
|
|
11811
|
+
});
|
|
11812
|
+
const finalizeText = await finalize.text();
|
|
11813
|
+
let finalizeJson = null;
|
|
11814
|
+
try {
|
|
11815
|
+
finalizeJson = finalizeText ? JSON.parse(finalizeText) : null;
|
|
11816
|
+
}
|
|
11817
|
+
catch {
|
|
11818
|
+
finalizeJson = null;
|
|
11819
|
+
}
|
|
11820
|
+
const finalizeResult = { status: finalize.status, ok: finalize.ok, json: finalizeJson, text: finalizeText };
|
|
11821
|
+
assertApiOk(finalizeResult, `put-file finalize (${space})`);
|
|
11822
|
+
emitResult(finalizeResult, ctx.json, [[`My Files [${space}]`, finalizeResult.json?.attachment?.viewUrl]]);
|
|
11823
|
+
continue;
|
|
11824
|
+
}
|
|
11825
|
+
// Step 2b (fallback): no presigned transport (local storage driver) or no
|
|
11826
|
+
// presign route at all. Multipart through the API — capped near 6 MB in the
|
|
11827
|
+
// cloud, so refuse a big file here instead of eating a confusing 413.
|
|
11828
|
+
if (space !== "local" && buffer.byteLength > 6 * 1024 * 1024) {
|
|
11829
|
+
throw new Error(`put-file (${space}): presign unavailable (HTTP ${presignStatus || 0}) and ${formatBytes(buffer.byteLength)} exceeds the ~6 MB multipart limit. ` +
|
|
11830
|
+
`Upgrade the server or retry when /me/attachments/presign is reachable.`);
|
|
11831
|
+
}
|
|
11750
11832
|
// FormData is single-use (its stream is consumed), so build a fresh one per
|
|
11751
11833
|
// space when writing to both.
|
|
11752
11834
|
const form = new FormData();
|
|
11753
11835
|
form.append("file", new Blob([new Uint8Array(buffer)], contentType ? { type: contentType } : undefined), fileName);
|
|
11754
|
-
if (
|
|
11755
|
-
form.append("folder_path",
|
|
11756
|
-
if (
|
|
11757
|
-
form.append("notes",
|
|
11758
|
-
|
|
11759
|
-
if (space === "local") {
|
|
11760
|
-
const { withLocalBackend } = await import("./devcli/local-backend.js");
|
|
11761
|
-
const backend = await withLocalBackend({ home: ctx.home, apiKey: ctx.auth.apiKey });
|
|
11762
|
-
res = await backend.app.request(uploadPath, { method: "POST", headers: buildAuthHeaders(ctx.auth), body: form });
|
|
11763
|
-
}
|
|
11764
|
-
else {
|
|
11765
|
-
res = await fetch(new URL(uploadPath, ctx.host), { method: "POST", headers: buildAuthHeaders(ctx.auth), body: form });
|
|
11766
|
-
}
|
|
11836
|
+
if (folder)
|
|
11837
|
+
form.append("folder_path", folder);
|
|
11838
|
+
if (notes)
|
|
11839
|
+
form.append("notes", notes);
|
|
11840
|
+
const res = await call(uploadPath, { method: "POST", headers: buildAuthHeaders(ctx.auth), body: form });
|
|
11767
11841
|
const text = await res.text();
|
|
11768
11842
|
let json = null;
|
|
11769
11843
|
try {
|
|
@@ -215,10 +215,50 @@ async function listMachines(auth) {
|
|
|
215
215
|
title: String(gig.title ?? gig.id ?? ""),
|
|
216
216
|
gigId: String(gig.id ?? ""),
|
|
217
217
|
distribution: gig.distribution ? String(gig.distribution) : undefined,
|
|
218
|
-
|
|
218
|
+
// NOT the gig record's `invite_url` — that one carries no `?invite=`
|
|
219
|
+
// token, and an invite-only gig refuses a tokenless join. See
|
|
220
|
+
// resolveInviteUrl(): the real link lives on the invite itself.
|
|
221
|
+
inviteUrl: null
|
|
219
222
|
};
|
|
220
223
|
}).filter((m) => m.gigId);
|
|
221
224
|
}
|
|
225
|
+
/** True only for a join URL that carries an `?invite=` token. */
|
|
226
|
+
function hasInviteToken(url) {
|
|
227
|
+
if (typeof url !== "string" || !url)
|
|
228
|
+
return false;
|
|
229
|
+
try {
|
|
230
|
+
return Boolean(new URL(url).searchParams.get("invite"));
|
|
231
|
+
}
|
|
232
|
+
catch {
|
|
233
|
+
return false;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* The machine's REUSABLE, tokened join link — the only link a gigworker can
|
|
238
|
+
* actually join through, and therefore the only valid `destination_url` for a
|
|
239
|
+
* bell. Reads the gig's invites (each gig ships an unlimited "default" one) and
|
|
240
|
+
* mints a fresh unlimited invite when none survives.
|
|
241
|
+
*/
|
|
242
|
+
async function resolveInviteUrl(auth, gigId) {
|
|
243
|
+
const pick = (invite) => {
|
|
244
|
+
if (hasInviteToken(invite?.invite_url))
|
|
245
|
+
return invite.invite_url;
|
|
246
|
+
if (invite?.token)
|
|
247
|
+
return `https://dollarplatoon.com/gig/${encodeURIComponent(gigId)}/join?invite=${encodeURIComponent(String(invite.token))}`;
|
|
248
|
+
return "";
|
|
249
|
+
};
|
|
250
|
+
const listed = await dp(auth, `/gigs/${gigId}/invites`).catch(() => ({}));
|
|
251
|
+
const invites = Array.isArray(listed.invites) ? listed.invites : [];
|
|
252
|
+
const reusable = invites.filter((i) => !i.revoked && !i.exhausted && !i.email && (i.max_uses === null || i.max_uses === undefined));
|
|
253
|
+
const chosen = reusable.find((i) => i.label === "default") ?? reusable[0];
|
|
254
|
+
if (chosen && pick(chosen))
|
|
255
|
+
return pick(chosen);
|
|
256
|
+
const minted = await dp(auth, `/gigs/${gigId}/invites`, {
|
|
257
|
+
method: "POST",
|
|
258
|
+
body: { max_uses: null, label: "vidfarm" }
|
|
259
|
+
}).catch(() => ({}));
|
|
260
|
+
return minted.invite ? pick(minted.invite) : "";
|
|
261
|
+
}
|
|
222
262
|
/** A gig id, a machine slug, or (default) the machine this command is about. */
|
|
223
263
|
async function resolveGigId(auth, raw, fallbackSlug) {
|
|
224
264
|
const value = (raw ?? "").trim();
|
|
@@ -258,6 +298,11 @@ function proofLink(proof) {
|
|
|
258
298
|
// ── client commands ──────────────────────────────────────────────────────────
|
|
259
299
|
async function cmdMachines(auth, values) {
|
|
260
300
|
const machines = await listMachines(auth);
|
|
301
|
+
// Each machine's shareable link is its reusable INVITE, not the tokenless
|
|
302
|
+
// join URL the gig record carries — hand this one to a gigworker.
|
|
303
|
+
await Promise.all(machines.map(async (machine) => {
|
|
304
|
+
machine.inviteUrl = (await resolveInviteUrl(auth, machine.gigId).catch(() => "")) || null;
|
|
305
|
+
}));
|
|
261
306
|
out(Boolean(values.json), { machines }, () => {
|
|
262
307
|
if (!machines.length) {
|
|
263
308
|
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}`);
|
|
@@ -403,14 +448,17 @@ async function cmdRingBell(auth, values) {
|
|
|
403
448
|
const machine = machines.find((m) => m.slug === slug);
|
|
404
449
|
if (!machine)
|
|
405
450
|
throw new Error(`No "${slug}" machine on this account. Run: vidfarm gigs machines`);
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
451
|
+
// The bell is only as good as its link: an agent that taps a tokenless
|
|
452
|
+
// ".../join" URL is refused at the door, so the destination is always the
|
|
453
|
+
// machine's reusable INVITE link.
|
|
454
|
+
let destination = await resolveInviteUrl(auth, machine.gigId);
|
|
455
|
+
if (!hasInviteToken(destination)) {
|
|
456
|
+
// Registering the gig in the feed returns its reusable join link too.
|
|
409
457
|
const registered = await dp(auth, `/feeds/${VIDFARM_FEED_ID}/registry`, {
|
|
410
458
|
method: "POST",
|
|
411
459
|
body: { gig_id: machine.gigId, tags: ["vidfarm", "video", slug] }
|
|
412
460
|
}).catch(() => ({}));
|
|
413
|
-
destination = registered.invite_url
|
|
461
|
+
destination = hasInviteToken(registered.invite_url) ? registered.invite_url : "";
|
|
414
462
|
}
|
|
415
463
|
if (!destination)
|
|
416
464
|
throw new Error("That machine has no invite link yet — open it once on vidfarm.cc/marketplace/buyer.");
|
|
@@ -17,10 +17,33 @@
|
|
|
17
17
|
// Windows-on-ARM note: neither ffprobe-installer nor ffmpeg-static publishes a
|
|
18
18
|
// win32-arm64 build, so steps 2/3 return null there and PATH is the only route.
|
|
19
19
|
// That is not a regression — ffmpeg-static already had the same gap.
|
|
20
|
-
import { existsSync } from "node:fs";
|
|
20
|
+
import { accessSync, chmodSync, constants as fsConstants, existsSync, statSync } from "node:fs";
|
|
21
21
|
import { createRequire } from "node:module";
|
|
22
22
|
const requireFrom = createRequire(import.meta.url);
|
|
23
23
|
let cached;
|
|
24
|
+
/**
|
|
25
|
+
* The @ffprobe-installer/<platform> tarballs ship `ffprobe` with mode 0644 (and
|
|
26
|
+
* no `bin` entry, so npm never adds the execute bit) — the old `ffprobe-static`
|
|
27
|
+
* chmod'd it in a postinstall script, this one does not. Without the bit every
|
|
28
|
+
* spawn fails with EACCES. Best-effort: a read-only filesystem (Lambda
|
|
29
|
+
* /var/task) throws, and there the image build must set the bit instead.
|
|
30
|
+
*/
|
|
31
|
+
function ensureExecutable(binPath) {
|
|
32
|
+
try {
|
|
33
|
+
accessSync(binPath, fsConstants.X_OK);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
// not executable — try to fix it below
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
chmodSync(binPath, statSync(binPath).mode | 0o111);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
// read-only install (container image, nix store, root-owned global) — the
|
|
44
|
+
// caller still gets the path and the spawn error stays explicit.
|
|
45
|
+
}
|
|
46
|
+
}
|
|
24
47
|
/** Pull `.path` off whichever module shape the installer package exports. */
|
|
25
48
|
function readPath(mod) {
|
|
26
49
|
if (typeof mod === "string")
|
|
@@ -48,8 +71,10 @@ export function resolveBundledFfprobe() {
|
|
|
48
71
|
for (const pkg of ["@ffprobe-installer/ffprobe", "ffprobe-static"]) {
|
|
49
72
|
try {
|
|
50
73
|
const resolved = readPath(requireFrom(pkg));
|
|
51
|
-
if (resolved && existsSync(resolved))
|
|
74
|
+
if (resolved && existsSync(resolved)) {
|
|
75
|
+
ensureExecutable(resolved);
|
|
52
76
|
return (cached = resolved);
|
|
77
|
+
}
|
|
53
78
|
}
|
|
54
79
|
catch {
|
|
55
80
|
// not installed for this platform — try the next candidate
|
package/marketplace.md
CHANGED
|
@@ -86,6 +86,16 @@ So a free account is not locked out of the marketplace, it is on the other side
|
|
|
86
86
|
|
|
87
87
|
Practical rule: **run both**. Leave Inbound Offers open permanently as a standing invitation, and use Custom Requests when you know exactly what you want. Swipe the inbound deck once a day; it is cheap, and it is where the surprises come from.
|
|
88
88
|
|
|
89
|
+
**Your pitch box starts with four $0 tutorial videos.** vidfarm seeds every Inbound Offers machine with four short explainers, submitted by the `onboarding@vidfarm.cc` gigworker as ordinary inbound proofs. They carry `task_identifier: vidfarm_onboarding_*` and `asking_price: 0`, so the swipe deck has real cards on your very first visit and you learn the product by swiping it. Watch them, then keep or pass exactly as you would a real pitch. They sit at the TOP of the deck in their authored order, so you meet the product before the pitches. Everything under them is **oldest first** — the swipe stack is a queue, and the pitch that has waited longest comes up next.
|
|
90
|
+
|
|
91
|
+
They are free **only if you approve at the ask**. dollarplatoon never lets a submission lock at `$0` by itself, so `locked_price` still shows the machine price of `$0.10`; the $0 is the worker's quote. The swipe page pre-selects it for you. An agent approving by API must name it:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
curl -s -X PATCH -H "x-api-key: $DOLLARPLATOON_API_KEY" -H "Content-Type: application/json" \
|
|
95
|
+
-d '{"action":"approve","amount":0,"feedback":"Watched, thanks."}' \
|
|
96
|
+
"https://dollarplatoon.com/api/gigs/$INBOUND_GIG/proofs/$PROOF_ID"
|
|
97
|
+
```
|
|
98
|
+
|
|
89
99
|
Both machines are ordinary dollarplatoon gigs underneath — same proofs, same approvals, same rollups, same USDC on Base. `/marketplace` is only a friendlier face on them. Anything below can also be done from `dollarplatoon.com` directly.
|
|
90
100
|
|
|
91
101
|
### Telling your AI agent to work the marketplace
|
|
@@ -198,6 +208,7 @@ vidfarm gigs approve PRF_01H… --feedback "Great hook, keeping it."
|
|
|
198
208
|
vidfarm gigs reject PRF_01H… --tag not_selected --feedback "Not a fit this round."
|
|
199
209
|
vidfarm gigs ring-bell --title "Acme wants 7 short-form ads this week" \
|
|
200
210
|
--subtext "$0.50 per kept video, 9:16, sticker style" --machine inbound_offers
|
|
211
|
+
# publishes that machine's reusable INVITE link (…/join?invite=<token>) as the destination
|
|
201
212
|
```
|
|
202
213
|
|
|
203
214
|
Add `--json` to any of them for a machine-readable answer. `--body <file.json>` sends a whole task payload you built elsewhere. `vidfarm gigs help` lists every flag.
|
|
@@ -278,9 +289,22 @@ curl -s -X POST https://vidfarm.cc/marketplace/buyer/ring-bell \
|
|
|
278
289
|
# → { "ok": true, "machine": "inbound_offers", "destinationUrl": "https://dollarplatoon.com/gig/GIG_.../join?invite=..." }
|
|
279
290
|
```
|
|
280
291
|
|
|
281
|
-
vidfarm resolves the destination for you: the notification links at that machine's own
|
|
292
|
+
vidfarm resolves the destination for you: the notification links at that machine's own **reusable invite** — the link that carries `?invite=<token>` — so a gigworker who taps the notification joins the machine and starts submitting immediately. You never have to paste an invite link.
|
|
293
|
+
|
|
294
|
+
**The invite token is the whole point.** A vending machine is invite-only, so the bare `https://dollarplatoon.com/gig/GIG_.../join` URL is a dead end: the worker lands on the join page and is refused. That bare URL is exactly what the gig record's own `invite_url` field gives you, so never publish it. The reusable, tokened link comes from the gig's invites:
|
|
295
|
+
|
|
296
|
+
```bash
|
|
297
|
+
GIG=GIG_01...
|
|
298
|
+
curl -s -H "x-api-key: $DOLLARPLATOON_API_KEY" "https://dollarplatoon.com/api/gigs/$GIG/invites" \
|
|
299
|
+
| jq -r '.invites[] | select(.revoked==false and .exhausted==false and .email==null and .max_uses==null) | .invite_url'
|
|
300
|
+
# nothing came back? mint a reusable one:
|
|
301
|
+
curl -s -X POST -H "x-api-key: $DOLLARPLATOON_API_KEY" -H "Content-Type: application/json" \
|
|
302
|
+
-d '{"max_uses":null,"label":"vidfarm"}' "https://dollarplatoon.com/api/gigs/$GIG/invites"
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
On `https://vidfarm.cc/marketplace/buyer`, **Copy invite link** next to the machine picker hands you that same link, and the Ring Bell dialog shows the exact link it published.
|
|
282
306
|
|
|
283
|
-
The same thing straight from dollarplatoon, if you would rather not go through vidfarm
|
|
307
|
+
The same thing straight from dollarplatoon, if you would rather not go through vidfarm — resolve the invite first and send it as `destination_url`:
|
|
284
308
|
|
|
285
309
|
```bash
|
|
286
310
|
curl -s -X POST "https://dollarplatoon.com/api/feeds/FEED_01M0BCTVTKSDFZQVEY1RXA4NJK/notifications" \
|
|
@@ -291,7 +315,7 @@ curl -s -X POST "https://dollarplatoon.com/api/feeds/FEED_01M0BCTVTKSDFZQVEY1RXA
|
|
|
291
315
|
"tags":["vidfarm","video","inbound_offers"]}'
|
|
292
316
|
```
|
|
293
317
|
|
|
294
|
-
`destination_url` must be `https://` — it renders as a link for every member of the feed.
|
|
318
|
+
`destination_url` must be `https://` — it renders as a link for every member of the feed. It must also carry `?invite=<token>`; a bell whose link cannot be joined wastes every agent who taps it.
|
|
295
319
|
|
|
296
320
|
**Ring the bell when something actually changed.** A new batch went up, a price went up, a format you now want, a deadline. Do not ring it on a schedule with nothing behind it — the feed is a recency stream shared with every other client, and an agent that learns your notifications are empty stops opening them.
|
|
297
321
|
|
|
@@ -408,7 +432,7 @@ curl -s -H "x-api-key: $DOLLARPLATOON_API_KEY" \
|
|
|
408
432
|
"https://dollarplatoon.com/api/feeds/$FEED/notifications?limit=1" | jq 'keys'
|
|
409
433
|
```
|
|
410
434
|
|
|
411
|
-
each item is `{ id, title, subtext, destination_url, tags, created_at }`, newest first. a client rings the bell when they want videos now, and `destination_url` is the
|
|
435
|
+
each item is `{ id, title, subtext, destination_url, tags, created_at }`, newest first. a client rings the bell when they want videos now, and `destination_url` is the **invite link** for the exact machine they are asking about — open it and you join. a link with no `?invite=` token is a client whose bell was rung by an old build: you cannot join it, so find that gig in the feed registry instead, where the `invite_url` is tokened. **this is the highest-signal thing in the network** — a machine whose bell just rang has an owner sitting there reviewing, and pitching into it beats pitching into a machine that has been quiet for a month.
|
|
412
436
|
|
|
413
437
|
record the newest notification `id` you have seen and stop paging when you reach it again. do not re-read the whole stream every loop.
|
|
414
438
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@officexapp/vidfarm-devcli",
|
|
3
|
-
"version": "0.21.
|
|
3
|
+
"version": "0.21.52",
|
|
4
4
|
"description": "Local bridge for the Vidfarm Trackpad Editor. `vidfarm serve <template_id>` boots the FULL editor on localhost (disk-backed records/storage, free in-process render); edit composition.html on disk (Claude Code, Codex, etc.) and the browser live-morphs it.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|