@tendrilapp/cli 0.1.5 → 0.1.6

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.
@@ -66,7 +66,7 @@ var TOOLS = [
66
66
  },
67
67
  {
68
68
  name: "tendril_record_ingest",
69
- description: "Ingest a VERBATIM Figma tool-response for a planned rep. PREFER `text`: paste the tool response text exactly as received \u2014 no file to write, no wrapper to build; the CLI constructs the envelope from the same bytes. The response includes `next` (the following instruction \u2014 no separate record_next call) and, for get_design_context, `assets`: the emission's SVG/PNG assets are AUTO-FETCHED server-side; only listed failures need manual record_fetch/record_asset handling.",
69
+ description: "Ingest a VERBATIM Figma tool-response for a planned rep. PREFER `text` (single block) or `texts` (response split into multiple output blocks \u2014 pass each block verbatim, in order; NEVER hand-join them): no file to write, no wrapper to build; the CLI constructs the envelope from the same bytes. The response includes `next` (the following instruction \u2014 no separate record_next call) and, for get_design_context, `assets`: the emission's SVG/PNG assets are AUTO-FETCHED server-side; only listed failures need manual record_fetch/record_asset handling.",
70
70
  schema: z.object({
71
71
  setDir: str("recording set directory"),
72
72
  rep: str("planned rep slug, or __set__ for the set-level get_variable_defs"),
@@ -76,22 +76,26 @@ var TOOLS = [
76
76
  // exit 0). The sink in session.ts now contains the path too — this
77
77
  // is the second layer, and it makes the tool self-documenting.
78
78
  tool: z.enum(["get_design_context", "get_metadata", "get_screenshot", "get_variable_defs", "get_metadata_interior"]),
79
- text: optStr("the tool response text VERBATIM \u2014 exactly as returned, unmodified (preferred; text tools only \u2014 screenshots go through record_fetch)"),
80
- file: optStr("path to a saved envelope JSON (alternative to text)")
79
+ text: optStr("the tool response text VERBATIM \u2014 exactly as returned, unmodified (single-block responses; text tools only \u2014 screenshots go through record_fetch)"),
80
+ texts: z.array(z.string()).optional().describe("when the response arrived as MULTIPLE output blocks: every block, in order, each verbatim \u2014 never hand-join blocks yourself"),
81
+ file: optStr("path to a saved envelope JSON (alternative to text/texts)")
81
82
  }),
82
83
  // The text rides a temp file, never argv: Windows caps a command
83
84
  // line at ~32 KB and design-context envelopes routinely exceed it.
84
85
  argv: (i) => {
85
86
  const base = ["record", "ingest", "--set", i["setDir"], "--rep", i["rep"], "--tool", i["tool"]];
86
87
  const text = i["text"];
88
+ const texts = i["texts"];
87
89
  const file = i["file"];
88
- if (text === void 0 === (file === void 0)) throw new Error("pass exactly one of `text` (the verbatim response text) or `file` (a saved envelope)");
90
+ if ([text, texts, file].filter((x) => x !== void 0).length !== 1) throw new Error("pass exactly one of `text` (single-block response), `texts` (multi-block response), or `file` (a saved envelope)");
91
+ if (file !== void 0) return [...base, "--file", file];
92
+ const tmp = path.join(mkdtempSync(path.join(os.tmpdir(), "tendril-envelope-")), "response.txt");
89
93
  if (text !== void 0) {
90
- const tmp = path.join(mkdtempSync(path.join(os.tmpdir(), "tendril-envelope-")), "response.txt");
91
94
  writeFileSync(tmp, text);
92
95
  return [...base, "--file", tmp, "--raw"];
93
96
  }
94
- return [...base, "--file", file];
97
+ writeFileSync(tmp, JSON.stringify(texts));
98
+ return [...base, "--file", tmp, "--raw-parts"];
95
99
  }
96
100
  },
97
101
  {
package/dist/tendril.js CHANGED
@@ -1008,6 +1008,14 @@ function ingestEnvelope(setDir, slug, tool, payload) {
1008
1008
  `);
1009
1009
  return { overwrote };
1010
1010
  }
1011
+ function envelopeTextContent(env) {
1012
+ const parts = env?.content ?? [];
1013
+ return parts.map((c) => c.text ?? "").filter((t) => t !== "").join("\n");
1014
+ }
1015
+ function envelopeFirstTextPart(env) {
1016
+ const parts = env?.content ?? [];
1017
+ return parts.find((c) => typeof c.text === "string" && c.text !== "")?.text ?? "";
1018
+ }
1011
1019
  function assetUrlsFromEnvelopeText(text) {
1012
1020
  const byUrl = /* @__PURE__ */ new Map();
1013
1021
  for (const m of text.matchAll(/const\s+\w+\s*=\s*"(https?:\/\/[^"]+\/assets?\/([a-z0-9-]+)\.(svg|png))"/gi)) {
@@ -4309,7 +4317,7 @@ var init_prelude = __esm({
4309
4317
  - Component root: -webkit-font-smoothing: antialiased and -moz-osx-font-smoothing: grayscale (recorded rasterization); font-synthesis: none (a missing font weight must fall back visibly, never fake-bold); color-scheme MATCHING YOUR RECORDING (light for a light capture, dark for a dark one) and direction: ltr \u2014 pin them, so the render cannot follow the viewer OS preference and drift from the capture it is graded against; isolation: isolate (own stacking context; overlay z-indexes never fight the host).
4310
4318
  - Interactive controls (buttons, options): touch-action: manipulation and user-select: none. Text inputs stay selectable (never user-select: none on them).
4311
4319
  - Scrollable popovers/menus: overscroll-behavior: contain.
4312
- - Focus rings on :focus-visible, never bare :focus (keyboard shows the ring; mouse click must not leave one).
4320
+ - Focus indicators bind to :focus-visible, never bare :focus. If the recording contains a focus pose, style the indicator from that recorded truth. If NO focus pose is recorded, do NOT invent ring colors/widths/offsets \u2014 an invented ring is unrecorded pixels; keep the browser's default focus indicator (leave outline in place on :focus-visible) and state the gap in your report. Text inputs match :focus-visible even on mouse click BY SPEC \u2014 that is platform-correct accessibility, never a bug to suppress with JS modality tracking.
4313
4321
  - Every animation wrapped in @media (prefers-reduced-motion: no-preference) or disabled under reduce.`;
4314
4322
  }
4315
4323
  });
@@ -6498,7 +6506,13 @@ async function autoFetchAssets(setDir, rep, envelopeText2) {
6498
6506
  async function runRecordIngest(opts) {
6499
6507
  let payload;
6500
6508
  try {
6501
- payload = opts.raw === true ? { content: [{ type: "text", text: readFileSync15(path24.resolve(opts.file), "utf8") }] } : JSON.parse(readFileSync15(path24.resolve(opts.file), "utf8"));
6509
+ if (opts.rawParts === true) {
6510
+ const parts = JSON.parse(readFileSync15(path24.resolve(opts.file), "utf8"));
6511
+ 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");
6512
+ payload = { content: parts.map((text) => ({ type: "text", text })) };
6513
+ } else {
6514
+ payload = opts.raw === true ? { content: [{ type: "text", text: readFileSync15(path24.resolve(opts.file), "utf8") }] } : JSON.parse(readFileSync15(path24.resolve(opts.file), "utf8"));
6515
+ }
6502
6516
  } catch (err) {
6503
6517
  fail(opts, ExitCode.InputValidation, {
6504
6518
  error: `cannot read envelope file: ${err instanceof Error ? err.message : String(err)}`,
@@ -6506,7 +6520,7 @@ async function runRecordIngest(opts) {
6506
6520
  remediation: "Pass the tool response saved verbatim: as JSON, or as raw text with --raw."
6507
6521
  });
6508
6522
  }
6509
- if (opts.raw === true && opts.tool === "get_screenshot") {
6523
+ if ((opts.raw === true || opts.rawParts === true) && opts.tool === "get_screenshot") {
6510
6524
  fail(opts, ExitCode.InputValidation, {
6511
6525
  error: "--raw is for text tool responses; screenshots are binary",
6512
6526
  code: "envelope-invalid",
@@ -7276,7 +7290,7 @@ function authorBehaviors(api) {
7276
7290
  return { behaviors, prelude: { controls: interactive ? ["> *"] : [], textInputs: [] }, disclosures };
7277
7291
  }
7278
7292
  function envelopeText(file) {
7279
- return JSON.parse(readFileSync17(file, "utf8")).content[0]?.text ?? "";
7293
+ return envelopeFirstTextPart(JSON.parse(readFileSync17(file, "utf8")));
7280
7294
  }
7281
7295
  function recordedFontNeeds(setDir) {
7282
7296
  const byFamily = /* @__PURE__ */ new Map();
@@ -7467,7 +7481,8 @@ var init_brief = __esm({
7467
7481
  import { existsSync as existsSync21, readFileSync as readFileSync18, readdirSync as readdirSync5 } from "node:fs";
7468
7482
  import path27 from "node:path";
7469
7483
  function repText(set, rep, tool) {
7470
- return JSON.parse(readFileSync18(path27.join(set, rep, `${tool}.json`), "utf8")).content[0]?.text ?? "";
7484
+ const env = JSON.parse(readFileSync18(path27.join(set, rep, `${tool}.json`), "utf8"));
7485
+ return tool === "get_design_context" ? envelopeFirstTextPart(env) : envelopeTextContent(env);
7471
7486
  }
7472
7487
  function stripFigmaInstructions(emission) {
7473
7488
  const STYLE_FACTS = "These styles are contained in the design:";
@@ -7528,7 +7543,7 @@ function buildSegments(task, mode = "fenced") {
7528
7543
  const SET = task.set;
7529
7544
  let rawDefs = {};
7530
7545
  if (existsSync21(path27.join(SET, "get_variable_defs.json"))) {
7531
- const text = JSON.parse(readFileSync18(path27.join(SET, "get_variable_defs.json"), "utf8")).content[0]?.text ?? "{}";
7546
+ const text = envelopeFirstTextPart(JSON.parse(readFileSync18(path27.join(SET, "get_variable_defs.json"), "utf8"))) || "{}";
7532
7547
  try {
7533
7548
  rawDefs = JSON.parse(text);
7534
7549
  } catch {
@@ -7537,7 +7552,7 @@ function buildSegments(task, mode = "fenced") {
7537
7552
  for (const cfg of task.configs) {
7538
7553
  const f = path27.join(SET, cfg.rep, "get_variable_defs.json");
7539
7554
  if (!existsSync21(f)) continue;
7540
- const text = JSON.parse(readFileSync18(f, "utf8")).content[0]?.text ?? "{}";
7555
+ const text = envelopeFirstTextPart(JSON.parse(readFileSync18(f, "utf8"))) || "{}";
7541
7556
  try {
7542
7557
  for (const [k, v] of Object.entries(JSON.parse(text))) rawDefs[k] ??= v;
7543
7558
  } catch {
@@ -7546,7 +7561,7 @@ function buildSegments(task, mode = "fenced") {
7546
7561
  }
7547
7562
  const emissionTexts = task.configs.map((cfg) => {
7548
7563
  const f = path27.join(SET, cfg.rep, "get_design_context.json");
7549
- return existsSync21(f) ? JSON.parse(readFileSync18(f, "utf8")).content[0]?.text ?? "" : "";
7564
+ return existsSync21(f) ? envelopeFirstTextPart(JSON.parse(readFileSync18(f, "utf8"))) : "";
7550
7565
  });
7551
7566
  const { map, note } = cleanTokenMap(rawDefs, emissionTexts);
7552
7567
  const defs = JSON.stringify(map, null, 1);
@@ -7596,6 +7611,7 @@ var cssIdent;
7596
7611
  var init_segments = __esm({
7597
7612
  "packages/generate/src/segments.ts"() {
7598
7613
  "use strict";
7614
+ init_src();
7599
7615
  cssIdent = (name) => name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
7600
7616
  }
7601
7617
  });
@@ -7720,7 +7736,7 @@ function countLatticeSymbols(setDir) {
7720
7736
  if (files.length === 0) return null;
7721
7737
  let count = 0;
7722
7738
  for (const f of files) {
7723
- const text = JSON.parse(readFileSync19(f, "utf8")).content[0]?.text ?? "";
7739
+ const text = envelopeTextContent(JSON.parse(readFileSync19(f, "utf8")));
7724
7740
  count += [...text.matchAll(/name="([^"]*)"/g)].filter((m) => m[1].includes("=")).length;
7725
7741
  }
7726
7742
  return count > 0 ? count : null;
@@ -7836,6 +7852,7 @@ var init_bundle_emit = __esm({
7836
7852
  "packages/generate/src/bundle-emit.ts"() {
7837
7853
  "use strict";
7838
7854
  init_src6();
7855
+ init_src();
7839
7856
  init_src4();
7840
7857
  BARS = {
7841
7858
  pass: { sim: 0.95, ink: 0.95 },
@@ -8449,7 +8466,7 @@ ${segments}`;
8449
8466
  ...notRecorded !== void 0 && notRecorded !== "" ? { notRecorded } : {},
8450
8467
  ...fontProvisioning !== void 0 ? { fontProvisioning } : {},
8451
8468
  modelSelection: {
8452
- instruction: "The model declaration is MECHANICAL: `engine score` refuses to run without --model. When a user is present, ask BEFORE reading the payload, using the template below verbatim where your host renders option dialogs. The audience is non-technical: plain cost/quality language, no model knowledge assumed. In a non-interactive session pick the balanced tier, state the reason in your report, and declare it \u2014 a silent default is not possible.",
8469
+ instruction: "The model declaration is MECHANICAL: `engine score` refuses to run without --model. ONLY ask when the answer can take effect \u2014 i.e. you can delegate implementation to an agent running the chosen model; if you cannot delegate in this session, skip the question, build as yourself, and declare your own model honestly (a question whose answer changes nothing wastes the user's trust \u2014 measured, second Windows run). When you do ask: BEFORE reading the payload, using the template below verbatim where your host renders option dialogs. The audience is non-technical: plain cost/quality language, no model knowledge assumed. In a non-interactive session pick the balanced tier, state the reason in your report, and declare it \u2014 a silent default is not possible.",
8453
8470
  questionTemplate: {
8454
8471
  prompt: "Which model should build this component? Every choice is scored by the same independent measurement \u2014 a cheaper model may need more attempts, but it can never ship a lower-quality certified result.",
8455
8472
  options: [
@@ -9859,11 +9876,11 @@ function buildProgram() {
9859
9876
  const { runRecordNext: runRecordNext2 } = await Promise.resolve().then(() => (init_record(), record_exports));
9860
9877
  runRecordNext2({ ...flags, setDir: cmd.opts()["set"] });
9861
9878
  });
9862
- record.command("ingest").requiredOption("--set <dir>", "recording set directory").requiredOption("--rep <slug>", "planned rep slug").requiredOption("--tool <tool>", "get_design_context | get_metadata | get_screenshot | get_variable_defs").requiredOption("--file <file>", "verbatim envelope JSON (or raw response text with --raw)").option("--raw", "the file holds the tool response TEXT verbatim; the CLI builds the envelope").action(async (_o, cmd) => {
9879
+ record.command("ingest").requiredOption("--set <dir>", "recording set directory").requiredOption("--rep <slug>", "planned rep slug").requiredOption("--tool <tool>", "get_design_context | get_metadata | get_screenshot | get_variable_defs").requiredOption("--file <file>", "verbatim envelope JSON (or raw response text with --raw)").option("--raw", "the file holds the tool response TEXT verbatim; the CLI builds the envelope").option("--raw-parts", "the file holds a JSON array of response block texts (multi-block transport); each becomes a content part verbatim").action(async (_o, cmd) => {
9863
9880
  const flags = globalFlags(cmd.parent.parent);
9864
9881
  const local = cmd.opts();
9865
9882
  const { runRecordIngest: runRecordIngest2 } = await Promise.resolve().then(() => (init_record(), record_exports));
9866
- await runRecordIngest2({ ...flags, setDir: local["set"], rep: local["rep"], tool: local["tool"], file: local["file"], raw: local["raw"] });
9883
+ await runRecordIngest2({ ...flags, setDir: local["set"], rep: local["rep"], tool: local["tool"], file: local["file"], raw: local["raw"], rawParts: local["rawParts"] });
9867
9884
  });
9868
9885
  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) => {
9869
9886
  const flags = globalFlags(cmd.parent.parent);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tendrilapp/cli",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "description": "Figma design systems → verified React components. CLI ruler + MCP server.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "type": "module",