@officexapp/vidfarm-devcli 0.21.60 → 0.21.62

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.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: vidfarm
3
- skill_version: 0.21.60
3
+ skill_version: 0.21.62
4
4
  description: Use Vidfarm as a director. Run a strategy **consultation** (the `brainstorm/*` chain — cold-start interview, awareness stages, persuasive angles, hooks, product placement). Answer "give me content ideas" / "what should I post" / "I need 30 videos this month" from the bundled idea banks (50 content frames x 5 awareness stages x 44 problem angles). Browse/add inspiration videos, browse the free public raws catalog BY CATEGORY (curated shelves like scroll-stoppers/greenscreen/reaction — the cheapest way to source footage for one video, and a ready-made clip pool for bulk scripting N variants), fork a template into a composition, edit it in the Trackpad Editor (timeline-based like Premiere/DaVinci), auto-decompose source video into scenes, render to MP4, approve into a shareable post, and schedule it. Includes login, provider keys, discovery, versioning, uploads/downloads, and billing. Every step is available as raw REST; `vidfarm-devcli` wraps those routes and composes the file-backed scripting flows.
5
5
  ---
6
6
 
package/SKILL.director.md CHANGED
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: vidfarm
3
- skill_version: 0.21.60
3
+ skill_version: 0.21.62
4
4
  description: Use Vidfarm as a director. Run a strategy **consultation** (the `brainstorm/*` chain — cold-start interview, awareness stages, persuasive angles, hooks, product placement). Answer "give me content ideas" / "what should I post" / "I need 30 videos this month" from the bundled idea banks (50 content frames x 5 awareness stages x 44 problem angles). Browse/add inspiration videos, browse the free public raws catalog BY CATEGORY (curated shelves like scroll-stoppers/greenscreen/reaction — the cheapest way to source footage for one video, and a ready-made clip pool for bulk scripting N variants), fork a template into a composition, edit it in the Trackpad Editor (timeline-based like Premiere/DaVinci), auto-decompose source video into scenes, render to MP4, approve into a shareable post, and schedule it. Includes login, provider keys, discovery, versioning, uploads/downloads, and billing. Every step is available as raw REST; `vidfarm-devcli` wraps those routes and composes the file-backed scripting flows.
5
5
  ---
6
6
 
package/SKILL.md CHANGED
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: vidfarm
3
- skill_version: 0.21.60
3
+ skill_version: 0.21.62
4
4
  description: Entry point for making, editing, and rendering short-form video with Vidfarm. Use when the task is to create a video, ad, explainer, or social clip; to edit or re-theme an existing composition; to source footage or audio; or to run a batch of video variants. Routes to the full director skill and the `vidfarm` CLI.
5
5
  ---
6
6
 
@@ -44,6 +44,18 @@ CLIENT (paid vidfarm plan — the key is read from your vidfarm account)
44
44
  gigs machines Your two standing machines + gig ids + invite links
45
45
  gigs tasks [machine] What is waiting in the Custom Requests queue
46
46
  gigs add-task --task "<brief>" Post a task into Custom Requests
47
+
48
+ BID AUCTION — let clippers name the price instead of taking yours
49
+ gigs request-bids --task "<brief>" Post a task NOBODY can claim; they bid in the
50
+ comments. --sealed hides bids from each other,
51
+ --bell broadcasts it, --opening "$0.10".
52
+ Prints a share link that carries a gig invite,
53
+ so a clipper who never joined can join AND bid
54
+ on that one page. Price stays TBD.
55
+ gigs bids <task-id> The bids on it, newest thread first
56
+ gigs award <task-id> --bid <id> Reserve that SAME task for the winner and tell
57
+ them privately. Losing bidders see nothing.
58
+ Then: gigs approve <proof-id> --amount <usd>
47
59
  --price <usd> Price per delivered video (default 0.25; "tbd" allowed)
48
60
  --tags a,b --priority <n> Routing hints (lower priority polls sooner)
49
61
  --angle/--hook/--url/--format The usual brief fields
@@ -54,7 +66,10 @@ CLIENT (paid vidfarm plan — the key is read from your vidfarm account)
54
66
  --body <file.json> Send this JSON as the whole task instead
55
67
  gigs proofs [machine] Submitted videos, newest first
56
68
  --status <pending|approved|rejected> --limit <n>
57
- gigs approve <proof-id> [machine] [--feedback "<text>"]
69
+ gigs approve <proof-id> [machine] [--amount <usd>] [--feedback "<text>"]
70
+ --amount names the price for a TBD task (what
71
+ you agreed in the bid thread). Omit it on a
72
+ TBD proof and it pays the machine's base rate.
58
73
  gigs reject <proof-id> [machine] [--tag not_selected] [--feedback "<text>"]
59
74
  not_selected = a pass; it costs the worker NO reputation
60
75
  gigs ring-bell --title "<t>" [--subtext "<s>"] [--machine inbound_offers]
@@ -584,6 +599,178 @@ async function cmdAddTask(auth, values) {
584
599
  console.log(` ${DIM}Nobody knows yet — broadcast it: vidfarm gigs ring-bell --title "…" --machine custom_requests${RESET}`);
585
600
  });
586
601
  }
602
+ // ── The bid auction ─────────────────────────────────────────────────────────
603
+ // A normal Custom Requests task is claimed by whoever polls first, at the
604
+ // machine's price. A BID task inverts that: it is published `view_only` so
605
+ // nobody can claim it, clippers post their price in the comment thread, and the
606
+ // buyer awards one by flipping that SAME task to `reserved`. One record carries
607
+ // the whole lifecycle, so the winner reads the entire negotiation on the task
608
+ // they are about to do.
609
+ //
610
+ // The price stays `tbd` throughout. The deal is plain text in the thread, and
611
+ // the real number is named once, at approval (`gigs approve --amount`), because
612
+ // arrangements ("$5 now $5 on completion", "$12 for three") do not fit a float.
613
+ /**
614
+ * The share link for a bid task. `/task/:gig/:id` is MEMBERS ONLY — Dollar
615
+ * Platoon shows a stranger the gig's join link instead of the task, and it
616
+ * ignores ?invite=. Only `/claim/:gig/:id?invite=<token>` admits a non-member:
617
+ * the "you have not joined" panel becomes "you are invited" and joining returns
618
+ * them to this task. A view-only task simply renders without the Accept button
619
+ * — the brief plus the comment thread — which is exactly the bidding page.
620
+ */
621
+ function bidTaskUrl(gigId, taskId, inviteUrl) {
622
+ let token = "";
623
+ try {
624
+ token = new URL(inviteUrl).searchParams.get("invite") ?? "";
625
+ }
626
+ catch {
627
+ token = "";
628
+ }
629
+ const base = `https://dollarplatoon.com/${token ? "claim" : "task"}`
630
+ + `/${encodeURIComponent(gigId)}/${encodeURIComponent(taskId)}`;
631
+ return token ? `${base}?invite=${encodeURIComponent(token)}` : base;
632
+ }
633
+ async function cmdRequestBids(auth, values) {
634
+ const task = values.task?.trim();
635
+ if (!task)
636
+ throw new Error('gigs request-bids needs --task "<what you want made>".');
637
+ const gigId = await resolveGigId(auth, values.machine, "custom_requests");
638
+ const gig = await dp(auth, `/gigs/${encodeURIComponent(gigId)}`);
639
+ const webhook = gig.gig?.webhook ?? gig.webhook ?? "";
640
+ if (!webhook)
641
+ throw new Error("That machine has no publisher webhook — open it on dollarplatoon.com.");
642
+ // --sealed => each clipper reads only their own thread (no undercutting).
643
+ // Default is open: bidders see each other, which is fine because discovery
644
+ // runs UPWARD from the opening bid, unlike a procurement reverse auction.
645
+ const policy = values.sealed ? "private" : "public";
646
+ const url = new URL(webhook);
647
+ url.searchParams.set("availability", "view_only");
648
+ url.searchParams.set("price", "tbd");
649
+ url.searchParams.set("tags", String(values.tags ?? "vidfarm,bid"));
650
+ const published = await fetch(url, {
651
+ method: "POST",
652
+ headers: { "content-type": "application/json" },
653
+ body: JSON.stringify({
654
+ task,
655
+ ...(values.url ? { url: String(values.url) } : {}),
656
+ ...(values.reference ? { reference_title: String(values.reference) } : {}),
657
+ starting_bid: String(values.opening ?? "$0.10"),
658
+ bidding: policy === "public" ? "open — reply with your price" : "sealed — your reply is private"
659
+ })
660
+ });
661
+ const publishText = await published.text();
662
+ let publishBody = {};
663
+ try {
664
+ publishBody = publishText ? JSON.parse(publishText) : {};
665
+ }
666
+ catch {
667
+ publishBody = {};
668
+ }
669
+ if (!published.ok)
670
+ throw new Error(`request-bids failed: ${publishBody?.error ?? `HTTP ${published.status}`}`);
671
+ // ONE task answers `message_id` (a string); only a batch answers `message_ids`.
672
+ const taskId = publishBody.message_id ?? publishBody.message_ids?.[0] ?? "";
673
+ if (!taskId)
674
+ throw new Error("Dollar Platoon accepted the task but returned no task id.");
675
+ await dp(auth, `/gigs/${encodeURIComponent(gigId)}/tasks/${encodeURIComponent(taskId)}/comments-policy`, {
676
+ method: "PATCH",
677
+ body: { policy }
678
+ });
679
+ const shareUrl = bidTaskUrl(gigId, taskId, await resolveInviteUrl(auth, gigId).catch(() => ""));
680
+ if (values.bell) {
681
+ await dp(auth, `/feeds/${VIDFARM_FEED_ID}/notifications`, {
682
+ method: "POST",
683
+ body: {
684
+ title: `Video wanted: ${task.slice(0, 120)}`,
685
+ subtext: `Bidding is ${policy === "public" ? "open" : "sealed"}. Opening bid ${values.opening ?? "$0.10"}.`,
686
+ destination_url: shareUrl,
687
+ tags: ["vidfarm", "video", "bid"]
688
+ }
689
+ }).catch((error) => {
690
+ console.log(`${YELLOW}!${RESET} Task posted, but the bell did not ring: ${error instanceof Error ? error.message : error}`);
691
+ });
692
+ }
693
+ out(Boolean(values.json), { gig_id: gigId, task_id: taskId, share_url: shareUrl, comments_policy: policy }, () => {
694
+ console.log(`${GREEN}✓${RESET} Bid request posted — ${BOLD}nobody can claim it${RESET}, they bid in the comments.`);
695
+ console.log(` ${BOLD}${taskId}${RESET} ${DIM}${policy === "public" ? "open bidding" : "sealed bids"} · price TBD${RESET}`);
696
+ console.log(` ${BOLD}Share this:${RESET} ${shareUrl}`);
697
+ console.log(` ${DIM}That link carries a gig invite — a clipper who never joined can join AND bid on that one page.${RESET}`);
698
+ if (!values.bell)
699
+ console.log(` ${DIM}Nobody was notified. Add --bell to broadcast it to the vidfarm feed.${RESET}`);
700
+ console.log(` ${DIM}Next: vidfarm gigs bids ${taskId}${RESET}`);
701
+ });
702
+ }
703
+ async function cmdBids(auth, values, positionals) {
704
+ const taskId = positionals[0];
705
+ if (!taskId)
706
+ throw new Error("gigs bids needs a task id: vidfarm gigs bids TASK_01H…");
707
+ const gigId = await resolveGigId(auth, values.machine, "custom_requests");
708
+ const body = await dp(auth, `/gigs/${encodeURIComponent(gigId)}/tasks/${encodeURIComponent(taskId)}/comments`);
709
+ const comments = body.comments ?? body.items ?? [];
710
+ // Root comments are the bids; replies underneath are the negotiation.
711
+ const bids = comments.filter((c) => !c.parent_id);
712
+ out(Boolean(values.json), { gig_id: gigId, task_id: taskId, bids }, () => {
713
+ if (!bids.length) {
714
+ console.log(`${DIM}No bids yet on ${taskId}.${RESET}`);
715
+ console.log(` ${DIM}Nobody may know it exists — vidfarm gigs request-bids … --bell, or send them the share link.${RESET}`);
716
+ return;
717
+ }
718
+ for (const bid of bids) {
719
+ // author_mailbox_id comes back ONLY to the gig owner, and only on a
720
+ // WORKER's comment — it is what `gigs award` reserves the task for.
721
+ const who = bid.author_name ?? "gigworker";
722
+ console.log(` ${BOLD}${bid.id}${RESET} ${DIM}${who}${bid.author_mailbox_id ? "" : " (no mailbox — cannot be awarded)"}${RESET}`);
723
+ console.log(` ${short(String(bid.body ?? ""), 110)}`);
724
+ }
725
+ console.log("");
726
+ console.log(`${DIM}Award one: vidfarm gigs award ${taskId} --bid <comment-id>${RESET}`);
727
+ });
728
+ }
729
+ async function cmdAward(auth, values, positionals) {
730
+ const taskId = positionals[0];
731
+ const bidId = values.bid?.trim();
732
+ if (!taskId)
733
+ throw new Error("gigs award needs a task id: vidfarm gigs award TASK_01H… --bid TCOMMENT_01H…");
734
+ if (!bidId)
735
+ throw new Error('gigs award needs --bid <comment-id>. See them with: vidfarm gigs bids <task-id>');
736
+ const gigId = await resolveGigId(auth, values.machine, "custom_requests");
737
+ const body = await dp(auth, `/gigs/${encodeURIComponent(gigId)}/tasks/${encodeURIComponent(taskId)}/comments`);
738
+ const comments = body.comments ?? body.items ?? [];
739
+ const winner = comments.find((c) => c.id === bidId);
740
+ if (!winner)
741
+ throw new Error(`No bid ${bidId} on ${taskId}. List them: vidfarm gigs bids ${taskId}`);
742
+ const mailbox = winner.author_mailbox_id ?? "";
743
+ if (!mailbox) {
744
+ throw new Error("Dollar Platoon did not identify that bidder. Only the gig OWNER sees a commenter's mailbox — check you are using the buyer's key, not a gigworker key.");
745
+ }
746
+ // The SAME task flips from view_only to reserved. No new task, so the winner
747
+ // reads the whole negotiation on the record they are about to work.
748
+ await dp(auth, `/gigs/${encodeURIComponent(gigId)}/tasks/${encodeURIComponent(taskId)}/availability`, {
749
+ method: "PATCH",
750
+ body: { availability: "reserved", reserved_for: mailbox }
751
+ });
752
+ const shareUrl = bidTaskUrl(gigId, taskId, await resolveInviteUrl(auth, gigId).catch(() => ""));
753
+ // A private reply has exactly two readers — its author and the person it
754
+ // answers — so the losing bidders never see the agreed terms.
755
+ await dp(auth, `/gigs/${encodeURIComponent(gigId)}/tasks/${encodeURIComponent(taskId)}/comments`, {
756
+ method: "POST",
757
+ body: {
758
+ body: values.message?.trim()
759
+ || `You won this one. It is reserved for you — accept it here: ${shareUrl}`,
760
+ parent_id: bidId,
761
+ private: true
762
+ }
763
+ });
764
+ out(Boolean(values.json), { gig_id: gigId, task_id: taskId, reserved_for: mailbox, share_url: shareUrl }, () => {
765
+ console.log(`${GREEN}✓${RESET} ${BOLD}${taskId}${RESET} reserved for ${BOLD}${winner.author_name ?? mailbox}${RESET}.`);
766
+ console.log(` ${DIM}They were told privately. The other bidders saw nothing.${RESET}`);
767
+ console.log(` ${DIM}${shareUrl}${RESET}`);
768
+ console.log("");
769
+ console.log(`${YELLOW}The task is still priced TBD${RESET}${DIM} — name the agreed amount when you approve their proof:${RESET}`);
770
+ console.log(` ${DIM}vidfarm gigs approve <proof-id> --amount 8.00${RESET}`);
771
+ console.log(` ${DIM}Approve without --amount and it silently pays the machine's base rate, not what you agreed.${RESET}`);
772
+ });
773
+ }
587
774
  async function cmdProofs(auth, values, positional) {
588
775
  const gigId = await resolveGigId(auth, positional, "inbound_offers");
589
776
  const body = await dp(auth, `/gigs/${encodeURIComponent(gigId)}/proofs`, {
@@ -617,10 +804,34 @@ async function cmdReview(auth, values, positionals, action) {
617
804
  body.feedback = String(values.feedback);
618
805
  if (action === "reject")
619
806
  body.rejection_tag = values.tag ? String(values.tag) : "not_selected";
807
+ // --amount names the price at APPROVAL. This is the only moment a TBD task
808
+ // gets its real number, and a bid task is TBD by design: the deal was agreed
809
+ // in plain text in the comment thread, not on the record. Dollar Platoon
810
+ // accepts an amount only while `locked_price` is null (a TBD proof, or one
811
+ // carrying the worker's own asking_price) — anything else answers 409, since
812
+ // a price a worker already agreed to can never be lowered at review.
813
+ //
814
+ // "TBD is not $0": approving a TBD proof with NO amount silently falls back
815
+ // to the gig's base rate, so an $8 bid pays $0.25 and the price locks on the
816
+ // spot with no way back. Warn loudly rather than let that pass unnoticed.
817
+ let amount = null;
818
+ if (action === "approve" && values.amount !== undefined && String(values.amount).trim() !== "") {
819
+ const parsed = Number(String(values.amount).trim().replace(/^\$/, ""));
820
+ if (!Number.isFinite(parsed) || parsed < 0) {
821
+ throw new Error(`--amount must be a positive number of dollars, got "${values.amount}".`);
822
+ }
823
+ amount = Math.round(parsed * 100) / 100;
824
+ body.amount = amount;
825
+ }
620
826
  const result = await dp(auth, `/gigs/${encodeURIComponent(gigId)}/proofs/${encodeURIComponent(proofId)}`, { method: "PATCH", body });
621
827
  out(Boolean(values.json), result, () => {
622
828
  if (action === "approve") {
623
- console.log(`${GREEN}✓${RESET} Approved ${BOLD}${proofId}${RESET} it pays out on the next rollup.`);
829
+ const locked = typeof result?.locked_price === "number" ? result.locked_price : null;
830
+ console.log(`${GREEN}✓${RESET} Approved ${BOLD}${proofId}${RESET}${locked != null ? ` at ${BOLD}$${locked.toFixed(2)}${RESET}` : ""} — it pays out on the next rollup.`);
831
+ if (amount == null && locked != null) {
832
+ console.log(` ${DIM}No --amount given, so it locked at the machine's own price.${RESET}`);
833
+ console.log(` ${YELLOW}If this came from a bid, that is NOT what you agreed${RESET}${DIM} — bid tasks are priced TBD and the number lives in the comment thread. Use --amount <usd>.${RESET}`);
834
+ }
624
835
  }
625
836
  else {
626
837
  console.log(`${GREEN}✓${RESET} Rejected ${BOLD}${proofId}${RESET} as ${BOLD}${body.rejection_tag}${RESET}.`);
@@ -1430,6 +1641,14 @@ function options() {
1430
1641
  tag: { type: "string" },
1431
1642
  title: { type: "string" },
1432
1643
  subtext: { type: "string" },
1644
+ // Bid auction (request-bids / bids / award).
1645
+ sealed: { type: "boolean" },
1646
+ bell: { type: "boolean" },
1647
+ opening: { type: "string" },
1648
+ reference: { type: "string" },
1649
+ bid: { type: "string" },
1650
+ message: { type: "string" },
1651
+ amount: { type: "string" },
1433
1652
  registry: { type: "boolean" },
1434
1653
  funded: { type: "boolean" },
1435
1654
  name: { type: "string" },
@@ -1471,6 +1690,11 @@ export async function runGigsCommand(argv) {
1471
1690
  case "queue": return cmdTasks(auth, values, rest[0]);
1472
1691
  case "add-task":
1473
1692
  case "task": return cmdAddTask(auth, values);
1693
+ case "request-bids":
1694
+ case "request":
1695
+ case "invite-bids": return cmdRequestBids(auth, values);
1696
+ case "bids": return cmdBids(auth, values, rest);
1697
+ case "award": return cmdAward(auth, values, rest);
1474
1698
  case "proofs":
1475
1699
  case "submissions": return cmdProofs(auth, values, rest[0]);
1476
1700
  case "approve":
package/marketplace.md CHANGED
@@ -374,6 +374,52 @@ vidfarm gigs ring-bell --title "Acme wants 7 short-form ads this week" \
374
374
 
375
375
  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.
376
376
 
377
+ ### Let clippers name the price — the bid auction
378
+
379
+ `add-task` sets your price and the first free gigworker takes it. Sometimes you would rather ask the
380
+ market what a job is worth — especially for a one-off that does not fit the machine's flat rate.
381
+
382
+ A **bid request** is one task published `view_only`, so nobody can claim it. Clippers post their
383
+ price in its comment thread, you award one, and that **same task** flips to `reserved` for them. One
384
+ record carries the whole lifecycle, so the winner reads the entire negotiation on the job they are
385
+ about to do.
386
+
387
+ ```bash
388
+ vidfarm gigs request-bids --task "30s UGC recaption for a plumbing business" \
389
+ --opening '$0.10' --reference "template_019fd6b0…" --bell
390
+ # → posts a view_only task at price TBD and prints a share link:
391
+ # https://dollarplatoon.com/claim/GIG_01N…/TASK_01M…?invite=<token>
392
+
393
+ vidfarm gigs bids TASK_01M… # who bid, and what they asked
394
+ vidfarm gigs award TASK_01M… --bid TCOMMENT_01M… # reserve it for them, privately
395
+ vidfarm gigs approve PRF_01H… --amount 8.00 # pay what you actually agreed
396
+ ```
397
+
398
+ | Flag | |
399
+ |---|---|
400
+ | `--sealed` | Each clipper reads only their own thread. Default is open, so bidders see each other. |
401
+ | `--bell` | Broadcast it to the vidfarm feed. **Off by default** — the task waits quietly in the queue. |
402
+ | `--opening` | Where the auction starts. Default `$0.10`. |
403
+ | `--reference` | A template id the bidders should match. |
404
+
405
+ **The share link carries a gig invite, and that is the point.** DollarPlatoon's plain
406
+ `/task/:gig/:id` page is members-only — a clipper who has never joined sees the gig's join link
407
+ *instead of* your task, with no idea what they were invited to. The `/claim/…?invite=<token>` link
408
+ this command prints turns "you have not joined" into "you are invited", and joining returns them to
409
+ your task. A view-only task simply renders without the Accept button, so the page is the brief plus
410
+ the comment thread: **they join and bid on one page.** Once you award it, that same URL grows the
411
+ Accept button for the winner.
412
+
413
+ ⚠️ **A bid task stays priced `TBD` on purpose.** The deal is plain text in the thread, because real
414
+ arrangements ("$5 now $5 on completion", "$12 for three") do not fit a single number. You name the
415
+ agreed amount once, at approval, with `--amount`. **TBD is not $0** — approve a TBD proof without
416
+ `--amount` and it silently pays the machine's base rate, so an $8 bid pays $0.25 and the price locks
417
+ with no way back.
418
+
419
+ The same flow exists over REST for a logged-in browser session:
420
+ `POST /marketplace/buyer/request-template` (from a template in `/discover`),
421
+ `GET /marketplace/buyer/bids/:gigId/:taskId`, and `POST /marketplace/buyer/award-bid`.
422
+
377
423
  **`add-task` has no flag for `format_harness` or `reference_template_id` — use `--body`.** Those two
378
424
  fields are the ones that decide what a weak-model clipper hands back, so do not drop them just
379
425
  because there is no flag:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@officexapp/vidfarm-devcli",
3
- "version": "0.21.60",
3
+ "version": "0.21.62",
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": {
@@ -107,6 +107,7 @@
107
107
  "operator:template-artifact": "node scripts/platform-operator-template-artifact.mjs",
108
108
  "bootstrap:staging-templates": "tsx scripts/bootstrap-staging-template-releases.ts",
109
109
  "migrate:sqlite-to-dynamodb": "tsx scripts/migrate-sqlite-to-dynamodb.ts",
110
+ "onboard": "node scripts/onboard-customer.mjs",
110
111
  "deploy:prod": "bash scripts/deploy-prod.sh",
111
112
  "deploy:staging": "bash scripts/deploy-staging.sh",
112
113
  "build": "npm run build:frontend && npm run build:hyperframes-editor && npm run build:serve-shells && node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\" && tsc -p tsconfig.json",
@@ -144,6 +145,8 @@
144
145
  "pack:devcli": "npm run build && node scripts/check-devcli-pack-safe.mjs && node scripts/build-devcli-package.mjs",
145
146
  "pack:devcli:fast": "node scripts/check-devcli-pack-safe.mjs && node scripts/build-devcli-package.mjs",
146
147
  "test:render-routing": "node --import tsx --test test/render-routing.test.ts",
148
+ "test:onboarding": "node --import tsx --test test/onboarding.test.ts",
149
+ "test:dp-invites": "node --import tsx --test test/dollarplatoon-invites.test.ts",
147
150
  "test:shared-folder": "node --import tsx --test test/shared-folder.test.ts"
148
151
  },
149
152
  "dependencies": {
@@ -174,6 +177,7 @@
174
177
  "ai": "^6.0.191",
175
178
  "dotenv": "^16.5.0",
176
179
  "esbuild": "^0.28.1",
180
+ "ethers": "^6.17.0",
177
181
  "ffmpeg-static": "^5.3.0",
178
182
  "fontkit": "^2.0.4",
179
183
  "hono": "^4.8.3",