@tendrilapp/cli 0.1.17 → 0.1.19
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 +30 -2
- package/dist/tendril-mcp.js +44 -18
- package/dist/tendril.js +1294 -296
- package/package.json +1 -1
package/dist/SKILL.md
CHANGED
|
@@ -20,11 +20,36 @@ Non-negotiables (the CLI enforces these; do not fight them):
|
|
|
20
20
|
- Sub-bar results are honest, not failures to hide: exit 5 ships the
|
|
21
21
|
bundle with real scores. Report them as they are.
|
|
22
22
|
|
|
23
|
+
First-run friction: if tendril/Figma tool calls are hitting permission
|
|
24
|
+
prompts, tell the user ONE approval can replace them all and offer
|
|
25
|
+
`tendril_permissions` with `write: true` — it merges the pipeline's
|
|
26
|
+
per-tool allowlist into the project's `.claude/settings.local.json`
|
|
27
|
+
(idempotent, touches nothing else; the user reloads the session to
|
|
28
|
+
apply). Never run it unoffered: it edits the user's settings.
|
|
29
|
+
|
|
30
|
+
Batch runs (several components in one session):
|
|
31
|
+
- Cap concurrent recorders at FOUR. All recorders share one Figma
|
|
32
|
+
desktop MCP server; a measured 8-wide batch hit its rate limit
|
|
33
|
+
(per-piece resume recovered, but the stall is avoidable).
|
|
34
|
+
- Batch the questions: plan ALL sets first, then put every
|
|
35
|
+
defaults-to-confirm and the one model question to the user together
|
|
36
|
+
— never one dialog per component.
|
|
37
|
+
- Create candidate directories with a bare `mkdir -p <dir>` — no
|
|
38
|
+
`&&`-compounds. Compound variants each need their own permission
|
|
39
|
+
approval; the bare form is one grant for the whole batch.
|
|
40
|
+
- Trust only tool output for completion state: check
|
|
41
|
+
`tendril_record_status` against disk, never a subagent's prose (a
|
|
42
|
+
measured batch had a recorder report success with zero reps
|
|
43
|
+
recorded).
|
|
44
|
+
|
|
23
45
|
## Recording a new component (needs Figma MCP)
|
|
24
46
|
|
|
25
47
|
1. Fetch the component set's frame metadata via Figma MCP
|
|
26
|
-
(`get_metadata` on the frame)
|
|
27
|
-
2. `tendril_record_plan` with
|
|
48
|
+
(`get_metadata` on the frame).
|
|
49
|
+
2. `tendril_record_plan` with the response text passed VERBATIM via
|
|
50
|
+
`metadataParts` (one array entry per response block, in order —
|
|
51
|
+
single-block responses may use `metadata` instead; nothing is saved
|
|
52
|
+
to a file, and never hand-join blocks) → the queue plan. The plan
|
|
28
53
|
records the FULL variant matrix — every pose the set defines
|
|
29
54
|
(sampling exists only as a human-run CLI flag; the MCP surface
|
|
30
55
|
cannot sample). CROSS-CHECK `variantsFound` in the plan output
|
|
@@ -51,6 +76,9 @@ Non-negotiables (the CLI enforces these; do not fight them):
|
|
|
51
76
|
every open-source face the recording declares, no questions needed.
|
|
52
77
|
Only faces that FAIL there are a licensing decision for the user:
|
|
53
78
|
offer `tendril fonts add` (Recommended) or a disclosed substitute.
|
|
79
|
+
A substitute never certifies: scores are capped at pass and a
|
|
80
|
+
cert-bar run exits fonts-unproven after its report — resolving the
|
|
81
|
+
real faces is the only path to certification.
|
|
54
82
|
3. Record each planned rep with FOUR tool calls: make the THREE
|
|
55
83
|
Figma MCP calls in protocol order — get_metadata, then
|
|
56
84
|
get_design_context (excludeScreenshot=true), then get_screenshot —
|
package/dist/tendril-mcp.js
CHANGED
|
@@ -25,26 +25,52 @@ var optStr = (d) => z.string().optional().describe(d);
|
|
|
25
25
|
var TOOLS = [
|
|
26
26
|
{
|
|
27
27
|
name: "tendril_record_plan",
|
|
28
|
-
description: "ENTRY POINT for implementing/building a React component from a Figma design or figma.com URL \u2014 start the Tendril pipeline here (after loading the tendril skill, if installed). Plans a recording session: computes the rep queue (anchor + one-factor + conflict crosses) from verbatim get_metadata
|
|
28
|
+
description: "ENTRY POINT for implementing/building a React component from a Figma design or figma.com URL \u2014 start the Tendril pipeline here (after loading the tendril skill, if installed). Plans a recording session: computes the rep queue (anchor + one-factor + conflict crosses) from the verbatim get_metadata response (pass its blocks via metadataParts \u2014 no file to write) and persists the set manifest. Resumes if the set already exists. The output may carry USER QUESTIONS \u2014 defaultsToConfirm (which pose is the component's default) or a multiple-component-sets error (which set to record): render them to a present user and apply the answers via `defaults` / `componentSet`; non-interactive runs follow each question's stated fallback. Recording cost is stated in figmaCallEstimate, never asked about \u2014 proceed with what the user provided.",
|
|
29
29
|
schema: z.object({
|
|
30
30
|
setDir: str("recording set directory to create/resume"),
|
|
31
31
|
component: str("component/system name"),
|
|
32
|
-
|
|
32
|
+
// Parts array FIRST, same reason as ingest_rep: real responses
|
|
33
|
+
// are usually multi-block, and the file param made plan the ONE
|
|
34
|
+
// remaining hand-built-envelope entry point (run 10: the agent
|
|
35
|
+
// wrote the file twice — once as text, once as JSON envelope).
|
|
36
|
+
metadataParts: z.array(z.string()).optional().describe("the frame-level get_metadata response blocks, every block in order, each verbatim \u2014 never hand-join or save them yourself; this is the NORMAL param"),
|
|
37
|
+
metadata: optStr("ONLY when get_metadata genuinely returned one single block: its text verbatim (otherwise use metadataParts)"),
|
|
38
|
+
metadataFiles: z.array(z.string()).optional().describe('saved verbatim get_metadata envelope file paths \u2014 JSON shape {"content":[{"type":"text","text":"<frame \u2026>"}]}; optionally <file>@<frameId>. Prefer metadataParts: no file to write'),
|
|
33
39
|
defaults: z.array(z.string()).optional().describe(`axis defaults as "Axis=Value" (from the user's defaultsToConfirm answers; may re-plan a set with nothing recorded yet)`),
|
|
34
40
|
componentSet: optStr("record only this component set (the user's pick from a multiple-component-sets error)")
|
|
35
41
|
}),
|
|
36
|
-
argv:
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
"--set",
|
|
40
|
-
i["
|
|
41
|
-
"
|
|
42
|
-
|
|
43
|
-
"
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
42
|
+
// Texts ride temp files, never argv: Windows caps a command line at
|
|
43
|
+
// ~32 KB and metadata envelopes can exceed it.
|
|
44
|
+
argv: (i) => {
|
|
45
|
+
const argvOut = ["record", "plan", "--set", i["setDir"], "--component", i["component"]];
|
|
46
|
+
if (i["metadata"] !== void 0 && i["metadataParts"] !== void 0) throw new Error("pass at most one of `metadata` and `metadataParts`");
|
|
47
|
+
const files = i["metadataFiles"];
|
|
48
|
+
if (files !== void 0 && files.length > 0) argvOut.push("--metadata", ...files);
|
|
49
|
+
const single = i["metadata"];
|
|
50
|
+
const parts = i["metadataParts"];
|
|
51
|
+
if (single !== void 0 || parts !== void 0) {
|
|
52
|
+
const tmp = path.join(mkdtempSync(path.join(os.tmpdir(), "tendril-envelope-")), "response.txt");
|
|
53
|
+
if (single !== void 0) {
|
|
54
|
+
writeFileSync(tmp, single);
|
|
55
|
+
argvOut.push("--metadata-raw-file", tmp);
|
|
56
|
+
} else {
|
|
57
|
+
if (parts.length === 0) throw new Error("`metadataParts` must be a non-empty array of block texts");
|
|
58
|
+
writeFileSync(tmp, JSON.stringify(parts));
|
|
59
|
+
argvOut.push("--metadata-raw-parts-file", tmp);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (argvOut.length === 6) throw new Error("nothing to plan from \u2014 pass metadataParts (normal), metadata (single-block), or metadataFiles");
|
|
63
|
+
argvOut.push(...i["defaults"] !== void 0 ? ["--default", ...i["defaults"]] : [], ...i["componentSet"] !== void 0 ? ["--component-set", i["componentSet"]] : []);
|
|
64
|
+
return argvOut;
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
name: "tendril_permissions",
|
|
69
|
+
description: "Install the Claude Code permission allowlist for the Tendril pipeline (write: true \u2014 merges per-tool entries into the project's .claude/settings.local.json, idempotent, never touches other settings), or list the entries without writing. USE THIS when tendril/Figma tool calls keep hitting permission prompts: offer it to the user once \u2014 ONE approval here replaces a prompt per pipeline call (run 8 measured 16+ prompts for a single component's recording). The user must reload the session for new settings to apply.",
|
|
70
|
+
schema: z.object({
|
|
71
|
+
write: z.boolean().optional().describe("true = install into .claude/settings.local.json (the point of this tool); false/absent = just return the entries")
|
|
72
|
+
}),
|
|
73
|
+
argv: (i) => ["permissions", "--claude", ...i["write"] === true ? ["--write"] : []]
|
|
48
74
|
},
|
|
49
75
|
{
|
|
50
76
|
name: "tendril_doctor",
|
|
@@ -81,7 +107,7 @@ var TOOLS = [
|
|
|
81
107
|
// multi-block (run 6: 49/49 reps — metadata 2 blocks, design
|
|
82
108
|
// context 5-6), so the arrays are the norm and the single-string
|
|
83
109
|
// params the rare case, not the reverse.
|
|
84
|
-
metadataParts: z.array(z.string()).optional().describe("get_metadata response blocks, every block in order, each verbatim \u2014 never hand-join blocks yourself.
|
|
110
|
+
metadataParts: z.array(z.string()).optional().describe("get_metadata response blocks, every block in order, each verbatim \u2014 never hand-join blocks yourself. Multi-block responses are common (run 6: 49/49 reps); a single block wrapped in a one-element array is equally fine."),
|
|
85
111
|
contextParts: z.array(z.string()).optional().describe("get_design_context response blocks, every block in order, each verbatim \u2014 the NORMAL param (real responses arrive as 5-6 blocks)"),
|
|
86
112
|
screenshotUrl: optStr("image_url from the get_screenshot response, verbatim \u2014 the CLI downloads it; the bytes never pass through your context"),
|
|
87
113
|
metadata: optStr("ONLY when get_metadata genuinely returned one single block: its text verbatim (otherwise use metadataParts)"),
|
|
@@ -241,7 +267,7 @@ var TOOLS = [
|
|
|
241
267
|
},
|
|
242
268
|
{
|
|
243
269
|
name: "tendril_verify",
|
|
244
|
-
description: "Verify/check that a component matches its Figma design \u2014 use when the user asks whether an implementation is faithful to the design, or to re-certify an existing Tendril bundle. Recomputes full verification (per-config status, behaviors, composition, evidence artifacts). Free, account-less, network-less \u2014 the trust anchor. Exit 5 means
|
|
270
|
+
description: "Verify/check that a component matches its Figma design \u2014 use when the user asks whether an implementation is faithful to the design, or to re-certify an existing Tendril bundle. Recomputes full verification (per-config status, behaviors, composition, evidence artifacts). Free, account-less, network-less \u2014 the trust anchor. Exit 5 means below the target bar with an honest report \u2014 sub-bar scores, or at bar cert a config demoted by absent-ink clusters.",
|
|
245
271
|
schema: z.object({
|
|
246
272
|
bundleDir: str("bundle directory to verify"),
|
|
247
273
|
bar: optStr("pass (default) or cert"),
|
|
@@ -343,7 +369,7 @@ function toolResult(result) {
|
|
|
343
369
|
const body = (result.stdout.trim() !== "" ? result.stdout : result.stderr) + stale;
|
|
344
370
|
if (result.exitCode === 5) {
|
|
345
371
|
return { content: [{ type: "text", text: `${body}
|
|
346
|
-
{"exitCode":5,"note":"
|
|
372
|
+
{"exitCode":5,"note":"HONEST below-target result \u2014 the report and bundle are valid; this is not a tool failure. Either sub-bar scores, or (at --bar cert) a config demoted by absent-ink clusters despite at-bar numbers \u2014 the report names which."}` }] };
|
|
347
373
|
}
|
|
348
374
|
const text = result.ok ? body : `${body}
|
|
349
375
|
{"exitCode":${result.exitCode},"note":"CLI exit-code contract: 3 input, 4 confirmation required, 6 fonts unproven, 7 recording incomplete"}`;
|
|
@@ -379,7 +405,7 @@ var IMPLEMENT_PROMPT = {
|
|
|
379
405
|
build: (figmaUrl) => [
|
|
380
406
|
`Implement the Figma component at ${figmaUrl} as a pixel-verified React component using the Tendril pipeline.`,
|
|
381
407
|
"- If the tendril skill (or plugin) is installed, load it and follow it.",
|
|
382
|
-
"- Otherwise: start at tendril_record_plan; while recording, make exactly the Figma call each tendril_record_next step names (get_metadata comes before any get_design_context) and save
|
|
408
|
+
"- Otherwise: start at tendril_record_plan; while recording, make exactly the Figma call each tendril_record_next step names (get_metadata comes before any get_design_context) and pass every response VERBATIM to the ingest tools (no files to save \u2014 the parts params take the response blocks directly); then tendril_engine_brief, implement the candidate, and tendril_engine_score until the bar passes; finish with tendril_verify.",
|
|
383
409
|
"- Verdicts come only from Tendril's scores \u2014 never claim or estimate fidelity numbers yourself."
|
|
384
410
|
].join("\n")
|
|
385
411
|
};
|