@officexapp/vidfarm-devcli 0.21.59 → 0.21.61
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 +2 -2
- package/.agents/skills/vidfarm/references/content-ideas.md +97 -14
- package/SKILL.director.md +99 -16
- package/SKILL.md +3 -1
- package/clipper.md +7 -3
- package/dist/src/cli.js +194 -15
- package/dist/src/devcli/clipper-panel.js +1828 -0
- package/dist/src/devcli/clipper-run.js +635 -126
- package/dist/src/devcli/marketplace-gigs.js +226 -2
- package/dist/src/devcli/skill-docs.js +14 -0
- package/experimental/meme-recaption.md +355 -54
- package/marketplace.md +67 -1
- package/package.json +3 -1
- package/public/assets/homepage-client-app.js +14 -14
|
@@ -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
|
-
|
|
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":
|
|
@@ -117,6 +117,8 @@ export const PACK_TOPICS = [
|
|
|
117
117
|
blurb: "The 5-stage ladder — what the viewer knows, what the video must do, and what it may ask for" },
|
|
118
118
|
{ topic: "problem-angles", aliases: ["angle", "lenses", "problem-angle"], doc: "references/content-ideas.md", heading: "The problem angles",
|
|
119
119
|
blurb: "44 angles on the problem — hold the frame, change the angle when a topic is \"already covered\"" },
|
|
120
|
+
{ topic: "ad-formats", aliases: ["ad-format", "formats", "format", "executions"], doc: "references/content-ideas.md", heading: "The ad formats",
|
|
121
|
+
blurb: "40 ad formats — what the finished video IS (iphone notes, tier list, trustpilot reviews, warning), and the honesty rule for the ones that imitate a record" },
|
|
120
122
|
{ topic: "meme-recaption", // `meme_recaption` needs no alias: resolvePackTopic folds `_` to `-` first.
|
|
121
123
|
aliases: ["meme", "recaption", "meme-caption"], doc: "references/editor-workflows.md", heading: "Writing a meme recaption",
|
|
122
124
|
blurb: "Recaption a meme at a pain or a win the niche knows — the cold-viewer test. Building one from scratch? the full format is vidfarm.cc/experimental/meme-recaption.md" },
|
|
@@ -248,6 +250,18 @@ export function loadAngleBank(name = DEFAULT_PACK) {
|
|
|
248
250
|
const { frames, families } = parseBulletBank(contents, "The problem angles", true);
|
|
249
251
|
return { angles: frames, families };
|
|
250
252
|
}
|
|
253
|
+
/**
|
|
254
|
+
* The ad-format bank — the FOURTH axis. A frame says what the video is the
|
|
255
|
+
* story of and an angle says which side of the problem it comes from; a format
|
|
256
|
+
* says what the finished thing IS (an iPhone note, a tier list, a warning
|
|
257
|
+
* label). Left unnamed, a director shoots the same talking head all month, so
|
|
258
|
+
* it belongs next to the other banks rather than in the prose nobody reads.
|
|
259
|
+
*/
|
|
260
|
+
export function loadAdFormatBank(name = DEFAULT_PACK) {
|
|
261
|
+
const { contents } = readPackDoc("references/content-ideas.md", name);
|
|
262
|
+
const { frames, families } = parseBulletBank(contents, "The ad formats", true);
|
|
263
|
+
return { formats: frames, families };
|
|
264
|
+
}
|
|
251
265
|
export function loadAwarenessLadder(name = DEFAULT_PACK) {
|
|
252
266
|
const { contents } = readPackDoc("references/content-ideas.md", name);
|
|
253
267
|
const section = extractSection(contents, "The awareness ladder");
|