@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.
- package/dist/tendril-mcp.js +10 -6
- package/dist/tendril.js +29 -12
- package/package.json +1 -1
package/dist/tendril-mcp.js
CHANGED
|
@@ -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
|
|
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 (
|
|
80
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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"))
|
|
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
|
-
|
|
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"))
|
|
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"))
|
|
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"))
|
|
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"))
|
|
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.
|
|
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);
|