@tendrilapp/cli 0.1.12 → 0.1.13
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/dist/SKILL.md +17 -9
- package/dist/tendril-mcp.js +39 -2
- package/dist/tendril.js +110 -35
- package/package.json +1 -1
package/dist/SKILL.md
CHANGED
|
@@ -51,15 +51,23 @@ Non-negotiables (the CLI enforces these; do not fight them):
|
|
|
51
51
|
every open-source face the recording declares, no questions needed.
|
|
52
52
|
Only faces that FAIL there are a licensing decision for the user:
|
|
53
53
|
offer `tendril fonts add` (Recommended) or a disclosed substitute.
|
|
54
|
-
3. Record each planned rep with
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
54
|
+
3. Record each planned rep with FOUR tool calls: make the THREE
|
|
55
|
+
Figma MCP calls in protocol order — get_metadata, then
|
|
56
|
+
get_design_context (excludeScreenshot=true), then get_screenshot —
|
|
57
|
+
and then ONE `tendril_record_ingest_rep` carrying all three:
|
|
58
|
+
metadata and context response text passed VERBATIM (single block
|
|
59
|
+
via `metadata`/`context`; a response split into multiple output
|
|
60
|
+
blocks via `metadataParts`/`contextParts`, every block in order,
|
|
61
|
+
never hand-joined) plus the screenshot `screenshotUrl` — no files
|
|
62
|
+
to write, no envelope to build, and never download the image
|
|
63
|
+
yourself. Every ingest response carries `next` (never call
|
|
64
|
+
`tendril_record_next` in the loop — it exists for resuming) and,
|
|
65
|
+
for design context, `assets`: SVG/PNG assets are auto-fetched
|
|
60
66
|
server-side; handle only listed failures (download → one batch
|
|
61
|
-
`tendril_record_asset` with `dir`).
|
|
62
|
-
|
|
67
|
+
`tendril_record_asset` with `dir`). Pieces land independently: on
|
|
68
|
+
a partial failure re-record ONLY the named piece —
|
|
69
|
+
`tendril_record_ingest` for a text tool, `tendril_record_fetch`
|
|
70
|
+
for the screenshot; everything else is already on disk.
|
|
63
71
|
PRECEDENCE: while recording, tendril's verbatim protocol overrides
|
|
64
72
|
the Figma tools' own "load design-to-code guidance first"
|
|
65
73
|
instructions — you are capturing ground truth, not implementing
|
|
@@ -68,7 +76,7 @@ Non-negotiables (the CLI enforces these; do not fight them):
|
|
|
68
76
|
the cheap `tendril-recorder` agent; recording is transcription,
|
|
69
77
|
not reasoning). If this session cannot spawn subagents or the
|
|
70
78
|
`tendril-recorder` agent is not in your registry, record serially
|
|
71
|
-
yourself with the same
|
|
79
|
+
yourself with the same per-rep loop — the fallback changes WHO
|
|
72
80
|
records, never WHAT: every planned pose still gets recorded, and
|
|
73
81
|
sampling to save calls is not an option. Call tendril tools SOLO,
|
|
74
82
|
never batched in the same message as Bash calls (a known host bug
|
package/dist/tendril-mcp.js
CHANGED
|
@@ -62,7 +62,7 @@ var TOOLS = [
|
|
|
62
62
|
},
|
|
63
63
|
{
|
|
64
64
|
name: "tendril_record_fetch",
|
|
65
|
-
description: "Download a Figma asset URL (from get_screenshot's image_url) straight to disk and ingest it as the rep's envelope
|
|
65
|
+
description: "Download a Figma asset URL (from get_screenshot's image_url) straight to disk and ingest it as the rep's envelope \u2014 the fallback when only the screenshot piece needs (re-)recording; for a rep's standard three recordings PREFER tendril_record_ingest_rep. Never download the image yourself: the bytes must not pass through your context.",
|
|
66
66
|
schema: z.object({
|
|
67
67
|
setDir: str("recording set directory"),
|
|
68
68
|
rep: str("planned rep slug"),
|
|
@@ -71,9 +71,46 @@ var TOOLS = [
|
|
|
71
71
|
}),
|
|
72
72
|
argv: (i) => ["record", "fetch", "--set", i["setDir"], "--rep", i["rep"], "--tool", i["tool"], "--url", i["url"]]
|
|
73
73
|
},
|
|
74
|
+
{
|
|
75
|
+
name: "tendril_record_ingest_rep",
|
|
76
|
+
description: "Ingest a rep's ENTIRE recording in ONE call \u2014 the get_metadata response, the get_design_context response, and the get_screenshot image_url together. Make the three Figma calls first, in protocol order (get_metadata, then get_design_context with excludeScreenshot=true, then get_screenshot), then pass all three here VERBATIM. PREFER THIS over three separate ingest/fetch calls: one approvable operation per rep instead of three. Pieces land independently: on a partial failure the error names exactly which piece(s) to re-record \u2014 the rest are already on disk. The response carries `next` and, for design context, `assets` (auto-fetched server-side; only listed failures need record_asset).",
|
|
77
|
+
schema: z.object({
|
|
78
|
+
setDir: str("recording set directory"),
|
|
79
|
+
rep: str("planned rep slug"),
|
|
80
|
+
metadata: optStr("get_metadata response text VERBATIM (single block)"),
|
|
81
|
+
metadataParts: z.array(z.string()).optional().describe("get_metadata response as MULTIPLE output blocks: every block, in order, each verbatim \u2014 never hand-join blocks yourself"),
|
|
82
|
+
context: optStr("get_design_context response text VERBATIM (single block)"),
|
|
83
|
+
contextParts: z.array(z.string()).optional().describe("get_design_context response as MULTIPLE output blocks, in order, each verbatim"),
|
|
84
|
+
screenshotUrl: optStr("image_url from the get_screenshot response, verbatim \u2014 the CLI downloads it; the bytes never pass through your context")
|
|
85
|
+
}),
|
|
86
|
+
// Texts ride temp files, never argv: Windows caps a command line at
|
|
87
|
+
// ~32 KB and design-context envelopes routinely exceed it.
|
|
88
|
+
argv: (i) => {
|
|
89
|
+
const argvOut = ["record", "ingest-rep", "--set", i["setDir"], "--rep", i["rep"]];
|
|
90
|
+
const bridge = (label, single, parts) => {
|
|
91
|
+
if (single !== void 0 && parts !== void 0) throw new Error(`pass at most one of \`${label}\` and \`${label}Parts\``);
|
|
92
|
+
if (single === void 0 && parts === void 0) return;
|
|
93
|
+
const tmp = path.join(mkdtempSync(path.join(os.tmpdir(), "tendril-envelope-")), "response.txt");
|
|
94
|
+
if (single !== void 0) {
|
|
95
|
+
writeFileSync(tmp, single);
|
|
96
|
+
argvOut.push(`--${label}-file`, tmp);
|
|
97
|
+
} else {
|
|
98
|
+
const blocks = parts;
|
|
99
|
+
if (blocks.length === 0) throw new Error(`\`${label}Parts\` must be a non-empty array of block texts`);
|
|
100
|
+
writeFileSync(tmp, JSON.stringify(blocks));
|
|
101
|
+
argvOut.push(`--${label}-parts-file`, tmp);
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
bridge("metadata", i["metadata"], i["metadataParts"]);
|
|
105
|
+
bridge("context", i["context"], i["contextParts"]);
|
|
106
|
+
if (i["screenshotUrl"] !== void 0) argvOut.push("--screenshot-url", i["screenshotUrl"]);
|
|
107
|
+
if (argvOut.length === 6) throw new Error("nothing to ingest \u2014 pass at least one of metadata/metadataParts, context/contextParts, screenshotUrl");
|
|
108
|
+
return argvOut;
|
|
109
|
+
}
|
|
110
|
+
},
|
|
74
111
|
{
|
|
75
112
|
name: "tendril_record_ingest",
|
|
76
|
-
description: "
|
|
113
|
+
description: "Single-piece ingest of a VERBATIM Figma tool-response \u2014 the fallback path (re-recording one failed piece, set-level get_variable_defs, get_metadata_interior for mains); for a rep's standard three recordings PREFER tendril_record_ingest_rep, which takes them all in one call. Pass `text` (single block) or `texts` (response split into multiple output blocks \u2014 each block verbatim, in order; NEVER hand-join them): the CLI constructs the envelope from the same bytes. The response includes `next` and, for get_design_context, `assets` (auto-fetched; only listed failures need manual handling).",
|
|
77
114
|
schema: z.object({
|
|
78
115
|
setDir: str("recording set directory"),
|
|
79
116
|
rep: str("planned rep slug, or __set__ for the set-level get_variable_defs"),
|
package/dist/tendril.js
CHANGED
|
@@ -6276,6 +6276,7 @@ __export(record_exports, {
|
|
|
6276
6276
|
runRecordFetch: () => runRecordFetch,
|
|
6277
6277
|
runRecordFinish: () => runRecordFinish,
|
|
6278
6278
|
runRecordIngest: () => runRecordIngest,
|
|
6279
|
+
runRecordIngestRep: () => runRecordIngestRep,
|
|
6279
6280
|
runRecordNext: () => runRecordNext,
|
|
6280
6281
|
runRecordPlan: () => runRecordPlan,
|
|
6281
6282
|
runRecordStatus: () => runRecordStatus
|
|
@@ -6521,46 +6522,37 @@ async function fetchAssetBytes(startUrl, allowed) {
|
|
|
6521
6522
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
6522
6523
|
return Buffer.from(await res.arrayBuffer());
|
|
6523
6524
|
}
|
|
6524
|
-
async function
|
|
6525
|
-
if (!isFigmaAssetUrl(
|
|
6526
|
-
|
|
6527
|
-
error: `refusing to fetch a non-Figma URL: ${opts.url}`,
|
|
6528
|
-
code: "asset-url-rejected",
|
|
6529
|
-
remediation: "Pass the image_url from the get_screenshot response verbatim."
|
|
6530
|
-
});
|
|
6525
|
+
async function ingestScreenshotFromUrl(setDir, rep, url) {
|
|
6526
|
+
if (!isFigmaAssetUrl(url)) {
|
|
6527
|
+
return { ok: false, code: "asset-url-rejected", error: `refusing to fetch a non-Figma URL: ${url}`, remediation: "Pass the image_url from the get_screenshot response verbatim." };
|
|
6531
6528
|
}
|
|
6532
6529
|
let bytes;
|
|
6533
6530
|
try {
|
|
6534
|
-
bytes = await fetchAssetBytes(
|
|
6531
|
+
bytes = await fetchAssetBytes(url, isFigmaAssetUrl);
|
|
6535
6532
|
} catch (err) {
|
|
6536
|
-
|
|
6537
|
-
error: `asset fetch failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
6538
|
-
code: "asset-fetch-failed",
|
|
6539
|
-
remediation: "Figma asset URLs expire after 7 days \u2014 re-run the Figma tool for a fresh URL."
|
|
6540
|
-
});
|
|
6533
|
+
return { ok: false, code: "asset-fetch-failed", error: `asset fetch failed: ${err instanceof Error ? err.message : String(err)}`, remediation: "Figma asset URLs expire after 7 days \u2014 re-run the Figma tool for a fresh URL." };
|
|
6541
6534
|
}
|
|
6542
6535
|
if (!(bytes.length > 8 && bytes[0] === 137 && bytes[1] === 80 && bytes[2] === 78 && bytes[3] === 71)) {
|
|
6543
|
-
|
|
6544
|
-
error: `fetched ${bytes.length} bytes that are not a PNG (expired URL usually returns HTML)`,
|
|
6545
|
-
code: "asset-not-png",
|
|
6546
|
-
remediation: "Re-run get_screenshot for a fresh URL and fetch again."
|
|
6547
|
-
});
|
|
6536
|
+
return { ok: false, code: "asset-not-png", error: `fetched ${bytes.length} bytes that are not a PNG (expired URL usually returns HTML)`, remediation: "Re-run get_screenshot for a fresh URL and fetch again." };
|
|
6548
6537
|
}
|
|
6549
6538
|
const payload = { content: [{ type: "image", data: bytes.toString("base64"), mimeType: "image/png" }] };
|
|
6550
6539
|
try {
|
|
6551
|
-
const { overwrote } = ingestEnvelope(
|
|
6552
|
-
|
|
6553
|
-
process.stdout.write(`fetched + ingested ${opts.rep}/${opts.tool} (${bytes.length} bytes, no model in the byte path)
|
|
6554
|
-
`);
|
|
6555
|
-
});
|
|
6540
|
+
const { overwrote } = ingestEnvelope(setDir, rep, "get_screenshot", payload);
|
|
6541
|
+
return { ok: true, bytes: bytes.length, overwrote };
|
|
6556
6542
|
} catch (err) {
|
|
6557
|
-
|
|
6558
|
-
error: `envelope rejected: ${err instanceof Error ? err.message : String(err)}`,
|
|
6559
|
-
code: "envelope-invalid",
|
|
6560
|
-
remediation: "Check the rep slug is planned in this set."
|
|
6561
|
-
});
|
|
6543
|
+
return { ok: false, code: "envelope-invalid", error: `envelope rejected: ${err instanceof Error ? err.message : String(err)}`, remediation: "Check the rep slug is planned in this set." };
|
|
6562
6544
|
}
|
|
6563
6545
|
}
|
|
6546
|
+
async function runRecordFetch(opts) {
|
|
6547
|
+
const result = await ingestScreenshotFromUrl(opts.setDir, opts.rep, opts.url);
|
|
6548
|
+
if (!result.ok) {
|
|
6549
|
+
fail(opts, ExitCode.InputValidation, { error: result.error, code: result.code, remediation: result.remediation });
|
|
6550
|
+
}
|
|
6551
|
+
emitData(opts, { rep: opts.rep, tool: opts.tool, bytes: result.bytes, overwrote: result.overwrote, source: "cli-fetch", next: nextPayload(opts.setDir) }, () => {
|
|
6552
|
+
process.stdout.write(`fetched + ingested ${opts.rep}/${opts.tool} (${result.bytes} bytes, no model in the byte path)
|
|
6553
|
+
`);
|
|
6554
|
+
});
|
|
6555
|
+
}
|
|
6564
6556
|
async function autoFetchAssets(setDir, rep, envelopeText2) {
|
|
6565
6557
|
const fetched = [];
|
|
6566
6558
|
const skipped = [];
|
|
@@ -6586,16 +6578,18 @@ async function autoFetchAssets(setDir, rep, envelopeText2) {
|
|
|
6586
6578
|
}
|
|
6587
6579
|
return { fetched, skipped, failed };
|
|
6588
6580
|
}
|
|
6581
|
+
function rawEnvelopeFromFile(file, parts) {
|
|
6582
|
+
if (parts) {
|
|
6583
|
+
const blocks = JSON.parse(readFileSync15(path24.resolve(file), "utf8"));
|
|
6584
|
+
if (!Array.isArray(blocks) || blocks.length === 0 || blocks.some((p) => typeof p !== "string")) throw new Error("parts file must be a non-empty JSON array of strings");
|
|
6585
|
+
return { content: blocks.map((text) => ({ type: "text", text })) };
|
|
6586
|
+
}
|
|
6587
|
+
return { content: [{ type: "text", text: readFileSync15(path24.resolve(file), "utf8") }] };
|
|
6588
|
+
}
|
|
6589
6589
|
async function runRecordIngest(opts) {
|
|
6590
6590
|
let payload;
|
|
6591
6591
|
try {
|
|
6592
|
-
|
|
6593
|
-
const parts = JSON.parse(readFileSync15(path24.resolve(opts.file), "utf8"));
|
|
6594
|
-
if (!Array.isArray(parts) || parts.length === 0 || parts.some((p) => typeof p !== "string")) throw new Error("--raw-parts file must be a non-empty JSON array of strings");
|
|
6595
|
-
payload = { content: parts.map((text) => ({ type: "text", text })) };
|
|
6596
|
-
} else {
|
|
6597
|
-
payload = opts.raw === true ? { content: [{ type: "text", text: readFileSync15(path24.resolve(opts.file), "utf8") }] } : JSON.parse(readFileSync15(path24.resolve(opts.file), "utf8"));
|
|
6598
|
-
}
|
|
6592
|
+
payload = opts.rawParts === true || opts.raw === true ? rawEnvelopeFromFile(opts.file, opts.rawParts === true) : JSON.parse(readFileSync15(path24.resolve(opts.file), "utf8"));
|
|
6599
6593
|
} catch (err) {
|
|
6600
6594
|
fail(opts, ExitCode.InputValidation, {
|
|
6601
6595
|
error: `cannot read envelope file: ${err instanceof Error ? err.message : String(err)}`,
|
|
@@ -6647,6 +6641,72 @@ async function runRecordIngest(opts) {
|
|
|
6647
6641
|
});
|
|
6648
6642
|
}
|
|
6649
6643
|
}
|
|
6644
|
+
async function runRecordIngestRep(opts) {
|
|
6645
|
+
const validate = (label, single, parts) => {
|
|
6646
|
+
if (single !== void 0 && parts !== void 0) {
|
|
6647
|
+
fail(opts, ExitCode.InputValidation, {
|
|
6648
|
+
error: `pass at most one of --${label}-file and --${label}-parts-file`,
|
|
6649
|
+
code: "ingest-rep-args",
|
|
6650
|
+
remediation: `Single-block responses go in --${label}-file; multi-block responses as a JSON string array in --${label}-parts-file.`
|
|
6651
|
+
});
|
|
6652
|
+
}
|
|
6653
|
+
};
|
|
6654
|
+
validate("metadata", opts.metadataFile, opts.metadataPartsFile);
|
|
6655
|
+
validate("context", opts.contextFile, opts.contextPartsFile);
|
|
6656
|
+
const metaFile = opts.metadataPartsFile ?? opts.metadataFile;
|
|
6657
|
+
const ctxFile = opts.contextPartsFile ?? opts.contextFile;
|
|
6658
|
+
if (metaFile === void 0 && ctxFile === void 0 && opts.screenshotUrl === void 0) {
|
|
6659
|
+
fail(opts, ExitCode.InputValidation, {
|
|
6660
|
+
error: "nothing to ingest \u2014 pass at least one piece",
|
|
6661
|
+
code: "ingest-rep-args",
|
|
6662
|
+
remediation: "Provide --metadata-file/--metadata-parts-file, --context-file/--context-parts-file, and/or --screenshot-url."
|
|
6663
|
+
});
|
|
6664
|
+
}
|
|
6665
|
+
const pieces = {};
|
|
6666
|
+
const failed = [];
|
|
6667
|
+
if (metaFile !== void 0) {
|
|
6668
|
+
try {
|
|
6669
|
+
const { overwrote } = ingestEnvelope(opts.setDir, opts.rep, "get_metadata", rawEnvelopeFromFile(metaFile, opts.metadataPartsFile !== void 0));
|
|
6670
|
+
pieces["metadata"] = { overwrote };
|
|
6671
|
+
} catch (err) {
|
|
6672
|
+
failed.push({ piece: "get_metadata", error: err instanceof Error ? err.message : String(err), remediation: "Re-record the get_metadata response verbatim and re-ingest just this piece (tendril_record_ingest)." });
|
|
6673
|
+
}
|
|
6674
|
+
}
|
|
6675
|
+
if (ctxFile !== void 0) {
|
|
6676
|
+
try {
|
|
6677
|
+
const envelope = rawEnvelopeFromFile(ctxFile, opts.contextPartsFile !== void 0);
|
|
6678
|
+
const { overwrote } = ingestEnvelope(opts.setDir, opts.rep, "get_design_context", envelope);
|
|
6679
|
+
const assets = await autoFetchAssets(opts.setDir, opts.rep, envelope.content.map((c) => c.text ?? "").join("\n"));
|
|
6680
|
+
pieces["context"] = { overwrote, assets };
|
|
6681
|
+
} catch (err) {
|
|
6682
|
+
failed.push({ piece: "get_design_context", error: err instanceof Error ? err.message : String(err), remediation: "Re-record the get_design_context response verbatim and re-ingest just this piece (tendril_record_ingest)." });
|
|
6683
|
+
}
|
|
6684
|
+
}
|
|
6685
|
+
if (opts.screenshotUrl !== void 0) {
|
|
6686
|
+
const shot = await ingestScreenshotFromUrl(opts.setDir, opts.rep, opts.screenshotUrl);
|
|
6687
|
+
if (shot.ok) pieces["screenshot"] = { bytes: shot.bytes, overwrote: shot.overwrote };
|
|
6688
|
+
else failed.push({ piece: "get_screenshot", error: shot.error, remediation: `${shot.remediation} Then re-ingest just this piece with tendril_record_fetch (\`record fetch\`).` });
|
|
6689
|
+
}
|
|
6690
|
+
if (failed.length > 0) {
|
|
6691
|
+
const landed = Object.keys(pieces);
|
|
6692
|
+
fail(opts, ExitCode.InputValidation, {
|
|
6693
|
+
error: `ingest-rep ${opts.rep}: ${failed.map((f) => `${f.piece} FAILED (${f.error})`).join("; ")}${landed.length > 0 ? ` \u2014 recorded ok: ${landed.join(", ")}` : ""}`,
|
|
6694
|
+
code: "ingest-rep-partial",
|
|
6695
|
+
remediation: `Only the failed piece(s) need re-recording. ${failed.map((f) => `${f.piece}: ${f.remediation}`).join(" ")}`
|
|
6696
|
+
});
|
|
6697
|
+
}
|
|
6698
|
+
emitData(opts, { rep: opts.rep, ...pieces, next: nextPayload(opts.setDir) }, () => {
|
|
6699
|
+
process.stdout.write(`ingested ${opts.rep}: ${Object.keys(pieces).join(" + ")}
|
|
6700
|
+
`);
|
|
6701
|
+
const assets = pieces["context"]?.assets;
|
|
6702
|
+
if (assets !== void 0) {
|
|
6703
|
+
for (const a of assets.fetched) process.stdout.write(` asset fetched ${a}
|
|
6704
|
+
`);
|
|
6705
|
+
for (const f of assets.failed) process.stdout.write(` ASSET FAILED ${f.name}: ${f.reason} \u2014 download it and run tendril record asset
|
|
6706
|
+
`);
|
|
6707
|
+
}
|
|
6708
|
+
});
|
|
6709
|
+
}
|
|
6650
6710
|
function runRecordAsset(opts) {
|
|
6651
6711
|
if (opts.dir !== void 0) {
|
|
6652
6712
|
const dir = path24.resolve(opts.dir);
|
|
@@ -10402,6 +10462,21 @@ function buildProgram() {
|
|
|
10402
10462
|
const { runRecordIngest: runRecordIngest2 } = await Promise.resolve().then(() => (init_record(), record_exports));
|
|
10403
10463
|
await runRecordIngest2({ ...flags, setDir: local["set"], rep: local["rep"], tool: local["tool"], file: local["file"], raw: local["raw"], rawParts: local["rawParts"] });
|
|
10404
10464
|
});
|
|
10465
|
+
record.command("ingest-rep").description("Batched per-rep ingest: metadata + design context + screenshot URL in one call; pieces land independently.").requiredOption("--set <dir>", "recording set directory").requiredOption("--rep <slug>", "planned rep slug").option("--metadata-file <file>", "get_metadata response TEXT verbatim").option("--metadata-parts-file <file>", "get_metadata response as a JSON array of block texts (multi-block transport)").option("--context-file <file>", "get_design_context response TEXT verbatim").option("--context-parts-file <file>", "get_design_context response as a JSON array of block texts").option("--screenshot-url <url>", "image_url from the get_screenshot response, verbatim").action(async (_o, cmd) => {
|
|
10466
|
+
const flags = globalFlags(cmd.parent.parent);
|
|
10467
|
+
const local = cmd.opts();
|
|
10468
|
+
const { runRecordIngestRep: runRecordIngestRep2 } = await Promise.resolve().then(() => (init_record(), record_exports));
|
|
10469
|
+
await runRecordIngestRep2({
|
|
10470
|
+
...flags,
|
|
10471
|
+
setDir: local["set"],
|
|
10472
|
+
rep: local["rep"],
|
|
10473
|
+
metadataFile: local["metadataFile"],
|
|
10474
|
+
metadataPartsFile: local["metadataPartsFile"],
|
|
10475
|
+
contextFile: local["contextFile"],
|
|
10476
|
+
contextPartsFile: local["contextPartsFile"],
|
|
10477
|
+
screenshotUrl: local["screenshotUrl"]
|
|
10478
|
+
});
|
|
10479
|
+
});
|
|
10405
10480
|
record.command("fetch").description("Download a Figma asset URL straight to disk and ingest it \u2014 no shell, no model in the byte path.").requiredOption("--set <dir>", "recording set directory").requiredOption("--rep <slug>", "planned rep slug").requiredOption("--tool <tool>", "get_screenshot").requiredOption("--url <url>", "image_url from the Figma tool response, verbatim").action(async (_o, cmd) => {
|
|
10406
10481
|
const flags = globalFlags(cmd.parent.parent);
|
|
10407
10482
|
const local = cmd.opts();
|