@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.
Files changed (2) hide show
  1. package/dist/tendril.js +68 -14
  2. 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
- const mcp = path.join(setDir, slug, kind === "metadata" ? "get_metadata.json" : "get_screenshot.json");
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(setDir, slug, kind === "metadata" ? REST_METADATA_FILE : REST_SCREENSHOT_FILE);
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, "get_metadata.json");
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, "get_metadata.json");
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, "get_metadata.json");
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
- detail: `Figma is not connected for REST recording \u2014 large component sets then pay the MCP daily call quota. Run ${tendrilCommand("figma-connect")} and click Allow once (agents: the tendril_figma_connect tool).`
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 env = JSON.parse(readFileSync26(path36.join(set, rep, `${tool}.json`), "utf8"));
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 arithmetic = `${callSentence}${channeled ? "." : ` (up to ${posesRemaining * FIGMA_CALLS_PER_REP_WORST + setLevelRemaining} if screenshots or assets need a second fetch).`}${restClause} 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" : ""}.`;
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
- process.stdout.write(` (a) pace it across days \u2014 the set persists after every pose, so a rate limit costs waiting, never recorded work
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 { active: false, 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" };
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" ? { active: false, note: client.refusal } : { active: false, note: `REST channel unavailable (${client.kind}): ${client.refusal} \u2014 recording proceeds over MCP` };
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();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tendrilapp/cli",
3
- "version": "0.1.46",
3
+ "version": "0.1.48",
4
4
  "description": "Figma design systems → verified React components. CLI ruler + MCP server.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "type": "module",