@tendrilapp/cli 0.1.46 → 0.1.48
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/tendril.js +68 -14
- package/package.json +1 -1
package/dist/tendril.js
CHANGED
|
@@ -842,9 +842,12 @@ import { existsSync } from "node:fs";
|
|
|
842
842
|
import path from "node:path";
|
|
843
843
|
import { z as z4 } from "zod";
|
|
844
844
|
function resolveRepEnvelopePath(setDir, slug, kind) {
|
|
845
|
-
|
|
845
|
+
return resolveRepEnvelopePathIn(path.join(setDir, slug), kind);
|
|
846
|
+
}
|
|
847
|
+
function resolveRepEnvelopePathIn(repDir, kind) {
|
|
848
|
+
const mcp = path.join(repDir, kind === "metadata" ? "get_metadata.json" : "get_screenshot.json");
|
|
846
849
|
if (existsSync(mcp)) return mcp;
|
|
847
|
-
const rest = path.join(
|
|
850
|
+
const rest = path.join(repDir, kind === "metadata" ? REST_METADATA_FILE : REST_SCREENSHOT_FILE);
|
|
848
851
|
return existsSync(rest) ? rest : mcp;
|
|
849
852
|
}
|
|
850
853
|
function repEnvelopeExists(setDir, slug, kind) {
|
|
@@ -1670,7 +1673,7 @@ function effectivelyInvisible(node) {
|
|
|
1670
1673
|
return false;
|
|
1671
1674
|
}
|
|
1672
1675
|
function repVisibility(setDir, slug) {
|
|
1673
|
-
const file = path3.join(setDir, slug, "
|
|
1676
|
+
const file = resolveRepEnvelopePathIn(path3.join(setDir, slug), "metadata");
|
|
1674
1677
|
if (!existsSync3(file)) return void 0;
|
|
1675
1678
|
let roots;
|
|
1676
1679
|
try {
|
|
@@ -1721,6 +1724,7 @@ var init_visibility = __esm({
|
|
|
1721
1724
|
"packages/figma/src/recording/visibility.ts"() {
|
|
1722
1725
|
"use strict";
|
|
1723
1726
|
init_normalize();
|
|
1727
|
+
init_rest_envelopes();
|
|
1724
1728
|
init_session();
|
|
1725
1729
|
normalizedLayerName = (name) => name.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
1726
1730
|
invisibleInEveryPose = (v) => v.presentIn.length > 0 && v.visibleIn.length === 0;
|
|
@@ -1778,7 +1782,7 @@ function buildComposeIndex(roots, depth = 3) {
|
|
|
1778
1782
|
for (const rep of manifest.reps) {
|
|
1779
1783
|
variantNodeIds.add(rep.nodeId);
|
|
1780
1784
|
repSlugByVariantNode.set(rep.nodeId, rep.slug);
|
|
1781
|
-
const metaFile = path4.join(dir, rep.slug, "
|
|
1785
|
+
const metaFile = resolveRepEnvelopePathIn(path4.join(dir, rep.slug), "metadata");
|
|
1782
1786
|
if (!existsSync4(metaFile)) continue;
|
|
1783
1787
|
try {
|
|
1784
1788
|
const text = envelopeTextContent(JSON.parse(readFileSync3(metaFile, "utf8")));
|
|
@@ -1838,7 +1842,7 @@ function emissionTails(setDir, repSlug) {
|
|
|
1838
1842
|
return byHead;
|
|
1839
1843
|
}
|
|
1840
1844
|
function hostInstances(setDir, repSlug) {
|
|
1841
|
-
const metaFile = path4.join(setDir, repSlug, "
|
|
1845
|
+
const metaFile = resolveRepEnvelopePathIn(path4.join(setDir, repSlug), "metadata");
|
|
1842
1846
|
if (!existsSync4(metaFile)) return [];
|
|
1843
1847
|
try {
|
|
1844
1848
|
const text = envelopeTextContent(JSON.parse(readFileSync3(metaFile, "utf8")));
|
|
@@ -2117,6 +2121,7 @@ var init_compose = __esm({
|
|
|
2117
2121
|
"packages/figma/src/recording/compose.ts"() {
|
|
2118
2122
|
"use strict";
|
|
2119
2123
|
init_session();
|
|
2124
|
+
init_rest_envelopes();
|
|
2120
2125
|
init_normalize();
|
|
2121
2126
|
init_visibility();
|
|
2122
2127
|
CompositionEntrySchema = z6.object({
|
|
@@ -9188,7 +9193,12 @@ function figmaRestCheck() {
|
|
|
9188
9193
|
return {
|
|
9189
9194
|
name: "figma-rest",
|
|
9190
9195
|
ok: true,
|
|
9191
|
-
|
|
9196
|
+
// ok stays true — recording works without it — but the field
|
|
9197
|
+
// taught that agents scan flags, not prose: the how-to rides the
|
|
9198
|
+
// remediation channel so a not-connected machine is never a
|
|
9199
|
+
// silently green row (first Windows channel run, 2026-08-26).
|
|
9200
|
+
detail: "Figma is not connected for REST recording on this machine \u2014 large component sets then pay the MCP daily call quota (informational; recording still works)",
|
|
9201
|
+
remediation: `Connect once and the quota problem disappears for pixels+geometry: ${tendrilCommand("figma-connect")}, one Allow click in the browser (agents: tendril_figma_connect, relay the link, then tendril_figma_connect_wait).`
|
|
9192
9202
|
};
|
|
9193
9203
|
}
|
|
9194
9204
|
const daysLeft = Math.floor((Date.parse(tokens.tokenExpiresAt) - Date.now()) / 864e5);
|
|
@@ -11626,7 +11636,8 @@ var init_brief = __esm({
|
|
|
11626
11636
|
import { existsSync as existsSync29, readFileSync as readFileSync26, readdirSync as readdirSync10 } from "node:fs";
|
|
11627
11637
|
import path36 from "node:path";
|
|
11628
11638
|
function repText(set, rep, tool) {
|
|
11629
|
-
const
|
|
11639
|
+
const file = tool === "get_metadata" ? resolveRepEnvelopePath(set, rep, "metadata") : tool === "get_screenshot" ? resolveRepEnvelopePath(set, rep, "screenshot") : path36.join(set, rep, `${tool}.json`);
|
|
11640
|
+
const env = JSON.parse(readFileSync26(file, "utf8"));
|
|
11630
11641
|
return tool === "get_design_context" ? envelopeFirstTextPart(env) : envelopeTextContent(env);
|
|
11631
11642
|
}
|
|
11632
11643
|
function refPngDims(set, rep) {
|
|
@@ -11786,7 +11797,6 @@ var init_segments = __esm({
|
|
|
11786
11797
|
"packages/generate/src/segments.ts"() {
|
|
11787
11798
|
"use strict";
|
|
11788
11799
|
init_src();
|
|
11789
|
-
init_src();
|
|
11790
11800
|
cssIdent = (name) => name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
11791
11801
|
}
|
|
11792
11802
|
});
|
|
@@ -14893,7 +14903,10 @@ async function runRecordPlan(opts) {
|
|
|
14893
14903
|
const restClause = channeled ? ` REST channel active: ~${restRequests} paced REST request(s) + ${restRepsRemaining} unmetered downloads carry the pixels+geometry \u2014 they spend NO MCP quota.` : "";
|
|
14894
14904
|
const callSentence = channeled ? `${posesRemaining} pose(s) still need their MCP emission call${setLevelClause} = ${callsNeeded} Figma MCP calls` : posesRemaining === poses ? `${poses} poses \xD7 ${FIGMA_CALLS_PER_REP} calls per pose${setLevelClause} = ${callsNeeded} Figma calls` : `${posesRemaining} poses still to record \xD7 ${FIGMA_CALLS_PER_REP} calls per pose${setLevelClause} = ${callsNeeded} Figma calls (${poses - posesRemaining} of ${poses} already on disk)`;
|
|
14895
14905
|
const planningSpent = variantScope === "selection" ? poses : 1;
|
|
14896
|
-
const
|
|
14906
|
+
const connectWouldHelp = channelNote?.reason === "no-credential";
|
|
14907
|
+
const callsIfConnected = posesRemaining + setLevelRemaining + 4;
|
|
14908
|
+
const connectDelta = connectWouldHelp ? ` CONNECT FIGMA AND THIS DROPS TO ~${callsIfConnected}: one Allow click (tendril_figma_connect) puts pixels+geometry on Figma REST \u2014 per-minute paced, no daily cap \u2014 then re-run this exact plan.` : "";
|
|
14909
|
+
const arithmetic = `${callSentence}${channeled ? "." : ` (up to ${posesRemaining * FIGMA_CALLS_PER_REP_WORST + setLevelRemaining} if screenshots or assets need a second fetch).`}${restClause}${connectDelta} ALREADY SPENT on planning, and counting against the same allowance: ~${planningSpent} get_metadata call(s)${variantScope === "selection" ? " \u2014 a selection-scoped plan pays one per node handed in, not one for the set" : ""}.`;
|
|
14897
14910
|
const multiDay = callsNeeded > MULTI_DAY_CALL_THRESHOLD;
|
|
14898
14911
|
const feasibilityCheck = {
|
|
14899
14912
|
arithmetic,
|
|
@@ -14906,6 +14919,20 @@ async function runRecordPlan(opts) {
|
|
|
14906
14919
|
surface: multiDay ? "This set exceeds every daily allowance we have heard of, so it CANNOT finish in one day. State the verdict to the user in plain numbers BEFORE recording anything and let them choose from `options` \u2014 do not start on the assumption it will fit." : "Say nothing about cost if the division fits inside one day's allowance \u2014 a set that comfortably fits is not worth the user's attention. Surface the verdict and `options` only when it does not fit.",
|
|
14907
14920
|
verdictTemplate: `${callSentence}. Your <seat> seat on <plan> allows <allowance>/day. This set needs about <days> day(s) at that rate.`,
|
|
14908
14921
|
options: [
|
|
14922
|
+
// CONNECT FIRST (measured on the first field run of the REST
|
|
14923
|
+
// channel, 2026-08-26): the plan's restChannel note said "not
|
|
14924
|
+
// connected" while these options offered only ways to pay the
|
|
14925
|
+
// full MCP bill — the agent dutifully rendered pace/sample/seat
|
|
14926
|
+
// and never mentioned the one click that removes the problem.
|
|
14927
|
+
// The remedy that dissolves the bill outranks the ones that
|
|
14928
|
+
// ration it, so it is option ONE, present whenever it applies.
|
|
14929
|
+
...!channeled && connectWouldHelp ? [
|
|
14930
|
+
{
|
|
14931
|
+
id: "connect-figma",
|
|
14932
|
+
decidedBy: "user clicks Allow once in the browser \u2014 and you START it: call tendril_figma_connect, relay the link, tendril_figma_connect_wait",
|
|
14933
|
+
text: `Connect Figma on this machine (read-only, revocable, one Allow click). Pixels+geometry then travel over Figma's REST API \u2014 per-minute paced, NO daily cap \u2014 and this set's MCP bill drops from ${callsNeeded} to ~${callsIfConnected} calls. After the connect completes, RE-RUN THIS EXACT PLAN: the channel is assignable until the first envelope is ingested.`
|
|
14934
|
+
}
|
|
14935
|
+
] : [],
|
|
14909
14936
|
{
|
|
14910
14937
|
id: "pace",
|
|
14911
14938
|
decidedBy: "user, and you can carry it out",
|
|
@@ -14986,11 +15013,15 @@ async function runRecordPlan(opts) {
|
|
|
14986
15013
|
`);
|
|
14987
15014
|
process.stdout.write(` ask Figma whoami (free \u2014 exempt from tool-call limits) for this seat's daily allowance, divide ${callsNeeded} by it, and say how many DAYS this needs before recording
|
|
14988
15015
|
`);
|
|
14989
|
-
|
|
15016
|
+
if (connectWouldHelp) {
|
|
15017
|
+
process.stdout.write(` (a) CONNECT FIGMA \u2014 one Allow click (\`${tendrilCommand("figma-connect")}\`) drops this set to ~${callsIfConnected} calls, then re-run this exact plan
|
|
15018
|
+
`);
|
|
15019
|
+
}
|
|
15020
|
+
process.stdout.write(` (${connectWouldHelp ? "b" : "a"}) pace it across days \u2014 the set persists after every pose, so a rate limit costs waiting, never recorded work
|
|
14990
15021
|
`);
|
|
14991
|
-
process.stdout.write(` (b) record fewer poses \u2014 YOUR call, not an agent's: delete recording-set.json and re-plan with --sample (buys calls with coverage)
|
|
15022
|
+
process.stdout.write(` (${connectWouldHelp ? "c" : "b"}) record fewer poses \u2014 YOUR call, not an agent's: delete recording-set.json and re-plan with --sample (buys calls with coverage)
|
|
14992
15023
|
`);
|
|
14993
|
-
process.stdout.write(` (c) a seat or plan with a larger daily allowance
|
|
15024
|
+
process.stdout.write(` (${connectWouldHelp ? "d" : "c"}) a seat or plan with a larger daily allowance
|
|
14994
15025
|
`);
|
|
14995
15026
|
} else {
|
|
14996
15027
|
process.stdout.write(`FEASIBILITY: check ${callsNeeded} against this seat's daily Figma allowance (ask whoami \u2014 it is free) before recording; it only needs raising if it does not fit
|
|
@@ -15053,11 +15084,22 @@ async function assignRestChannel(setDir, manifest, resumed) {
|
|
|
15053
15084
|
if (anything) return void 0;
|
|
15054
15085
|
}
|
|
15055
15086
|
if (current.figmaFile === void 0) {
|
|
15056
|
-
return {
|
|
15087
|
+
return {
|
|
15088
|
+
active: false,
|
|
15089
|
+
reason: "no-figma-file",
|
|
15090
|
+
note: "REST channel unavailable: the plan carries no --figma-file key \u2014 pass it (from the design URL) to record pixels+geometry over Figma REST and escape the MCP daily limit"
|
|
15091
|
+
};
|
|
15057
15092
|
}
|
|
15058
15093
|
const client = await figmaRestClient();
|
|
15059
15094
|
if (!client.ok) {
|
|
15060
|
-
return client.kind === "no-credential" ? {
|
|
15095
|
+
return client.kind === "no-credential" ? {
|
|
15096
|
+
active: false,
|
|
15097
|
+
reason: "no-credential",
|
|
15098
|
+
// The FIRST FIELD RUN taught this note to instruct, not
|
|
15099
|
+
// mention: the agent rendered the feasibility options and
|
|
15100
|
+
// never offered the connect, because nothing told it to.
|
|
15101
|
+
note: "Figma is not connected on THIS machine, so recording stays on the daily-limited MCP transport. AGENTS: offer the connect NOW, before any recording call \u2014 call tendril_figma_connect, relay the link (the user clicks Allow once), tendril_figma_connect_wait, then RE-RUN THIS EXACT PLAN: the channel stays assignable until the first envelope is ingested, and the bill drops to roughly one call per pose."
|
|
15102
|
+
} : { active: false, reason: client.kind, note: `REST channel unavailable (${client.kind}): ${client.refusal} \u2014 recording proceeds over MCP` };
|
|
15061
15103
|
}
|
|
15062
15104
|
const ids = current.reps.map((r) => r.nodeId);
|
|
15063
15105
|
const docs = {};
|
|
@@ -17209,6 +17251,17 @@ function resolveEngineTask(opts, callerCwd) {
|
|
|
17209
17251
|
const registry = TASKS[opts.taskOrSet];
|
|
17210
17252
|
if (registry !== void 0 && !isSet) return { task: registry, name: opts.taskOrSet, ref: opts.taskOrSet, disclosures: [], interactionEvidence: void 0 };
|
|
17211
17253
|
if (isSet) {
|
|
17254
|
+
const manifest = loadManifest(asPath);
|
|
17255
|
+
const missing = manifest.reps.filter(
|
|
17256
|
+
(r) => !repEnvelopeExists(asPath, r.slug, "metadata") || !existsSync40(path50.join(asPath, r.slug, "get_design_context.json"))
|
|
17257
|
+
);
|
|
17258
|
+
if (missing.length > 0) {
|
|
17259
|
+
fail(opts, ExitCode.RecordingIncomplete, {
|
|
17260
|
+
error: `recording set ${asPath} is missing envelopes for ${missing.length} rep(s): ${missing.slice(0, 3).map((r) => r.slug).join(", ")}${missing.length > 3 ? ", \u2026" : ""}`,
|
|
17261
|
+
code: "recording-incomplete",
|
|
17262
|
+
remediation: `Resume the recording (\`${tendrilCommand(`record next --set ${asPath}`)}\`) until \`${tendrilCommand(`record status --set ${asPath}`)}\` reports the set complete, then re-run this.`
|
|
17263
|
+
});
|
|
17264
|
+
}
|
|
17212
17265
|
try {
|
|
17213
17266
|
const authored = authorTaskFromSet(asPath);
|
|
17214
17267
|
for (const d of authored.disclosures) warn(opts, d);
|
|
@@ -17665,6 +17718,7 @@ var init_engine2 = __esm({
|
|
|
17665
17718
|
"packages/cli/src/commands/engine.ts"() {
|
|
17666
17719
|
"use strict";
|
|
17667
17720
|
init_src3();
|
|
17721
|
+
init_src();
|
|
17668
17722
|
init_src7();
|
|
17669
17723
|
init_src4();
|
|
17670
17724
|
init_environment();
|