@tendrilapp/cli 0.1.48 → 0.1.50
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 +121 -13
- package/dist/tendril-mcp.js +44 -6
- package/dist/tendril.js +2368 -1231
- package/package.json +1 -1
package/dist/SKILL.md
CHANGED
|
@@ -254,15 +254,52 @@ Batch runs (several components in one session):
|
|
|
254
254
|
lists unconfirmed partner pairs, this recording references OTHER
|
|
255
255
|
recorded components (a segmented control built from a recorded
|
|
256
256
|
Control, a dialog holding a recorded Button). Order matters:
|
|
257
|
-
generate the PARTNER bundle first, then
|
|
258
|
-
|
|
257
|
+
generate the PARTNER bundle first, then put the pairing decision in
|
|
258
|
+
front of the human — it is human-only on either channel, and you
|
|
259
|
+
can never make it. CLICK PATH (lead with this): `tendril_compose`
|
|
260
|
+
puts the pairing in their browser as an Approve card (sign in first
|
|
261
|
+
via `tendril_login` if needed — one browser Approve each); relay
|
|
262
|
+
the approve link VERBATIM, name the account the result says to
|
|
263
|
+
approve as, never urge, then loop `tendril_compose_wait` (same
|
|
264
|
+
heartbeat contract as publish waits: pending is a heartbeat, one
|
|
265
|
+
short liveness line, never "stuck"). The card's Deny is NOT NOW —
|
|
266
|
+
this request only, never a permanent decline. TERMINAL PATH: the
|
|
267
|
+
human runs
|
|
259
268
|
`npx -y -p @tendrilapp/cli@latest tendril compose --set <host-set-dir>`
|
|
260
|
-
|
|
261
|
-
|
|
269
|
+
themselves (confirm or `--decline <pair-key>`; decline there IS
|
|
270
|
+
permanent — asked once). Never confirm on either channel yourself.
|
|
262
271
|
Generating the host with pairs undecided is valid — the brief will
|
|
263
272
|
say so — but the host then re-implements the partner's pixels
|
|
264
273
|
locally instead of composing the verified partner bundle, so
|
|
265
274
|
surface the choice before you spend the rounds.
|
|
275
|
+
Discovery only sees recording sets in the SCANNED roots: the set's
|
|
276
|
+
parent directory by default (bounded), or `--library <dir>`. Sets
|
|
277
|
+
in another project root are invisible to pairing until co-located
|
|
278
|
+
or pointed at — the tools name their scanned roots so "not
|
|
279
|
+
scanned" never reads as "no relationship".
|
|
280
|
+
CONNECTING COMPONENTS THAT ALREADY EXIST (both recorded, both
|
|
281
|
+
generated): step ONE is putting both recording sets in one scanned
|
|
282
|
+
root (co-locate, or --library). If the pairing then shows as
|
|
283
|
+
NAME-ONLY (the host was recorded over MCP, so Figma's instance→
|
|
284
|
+
component bindings were never captured), run
|
|
285
|
+
`tendril_record_bindings` on the HOST set — one or two batched
|
|
286
|
+
REST calls, congruence-verified, no re-recording; needs the Figma
|
|
287
|
+
connect — if absent, that is ONE more browser Allow via
|
|
288
|
+
`tendril_figma_connect`, name it up front so it never reads as a
|
|
289
|
+
detour — and the pairing becomes id-backed and confirmable. Then
|
|
290
|
+
the click path above (tendril_compose → Approve →
|
|
291
|
+
tendril_compose_wait). After the approve: the HOST bundle is
|
|
292
|
+
REGENERATED with the same --library roots (a verified bundle is
|
|
293
|
+
frozen — composition never retrofits one; the partner bundle is
|
|
294
|
+
reused verbatim). That regeneration is a normal generation run:
|
|
295
|
+
ask the model question there as always, and set the expectation in
|
|
296
|
+
one line that it takes a few scored attempts. If the regenerated
|
|
297
|
+
host was ALREADY published, the republish completes in one
|
|
298
|
+
announced call; if it was NEVER published, a SECOND, different
|
|
299
|
+
browser card appears — the publish approval — say so before it
|
|
300
|
+
does. On session resume, a pending connect decision may be waiting:
|
|
301
|
+
if `pending-compose.json` exists under the user config dir, run
|
|
302
|
+
`tendril_compose_wait` first.
|
|
266
303
|
|
|
267
304
|
## Generating (the agent-harness engine — you are the proposer)
|
|
268
305
|
|
|
@@ -388,15 +425,25 @@ on disk — or `null` where a scoring path produced a score with no
|
|
|
388
425
|
pixels, which is a real outcome and not a bug.
|
|
389
426
|
|
|
390
427
|
The verdict, the pixels and the diff-colour legend join into one offline
|
|
391
|
-
sheet at `verify-evidence/inspect.html`.
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
428
|
+
sheet at `verify-evidence/inspect.html`. A GREEN verify writes that
|
|
429
|
+
sheet itself (the report's `eyeCheck.sheetBuilt` says so) — report its
|
|
430
|
+
path, never ask whether to produce it. After a FAILED verify, run
|
|
431
|
+
`npx @tendrilapp/cli inspect <bundle-dir>` yourself before diagnosing:
|
|
432
|
+
the sheet is free, offline and account-less, so no cost or risk exists
|
|
433
|
+
that would justify a prompt in front of it. Note the ORDER — `inspect`
|
|
434
|
+
reads the evidence a `verify` run wrote, so it runs after `verify`,
|
|
435
|
+
never before.
|
|
395
436
|
|
|
396
437
|
Point the user at that sheet, not at terminal scrollback: a verdict
|
|
397
438
|
nobody can re-read beside the evidence it describes cannot be handed to
|
|
398
|
-
anyone.
|
|
399
|
-
|
|
439
|
+
anyone.
|
|
440
|
+
|
|
441
|
+
Verdict caveats and coverage lines (`INCOMPLETE`, `PIXEL-ONLY`,
|
|
442
|
+
`coverage-denominator-unknown`, `composition-not-checked`) are
|
|
443
|
+
DISCLOSURES, not questions: nothing is blocked and no answer is
|
|
444
|
+
required. State them as plain findings in your final report — never
|
|
445
|
+
convert one into an options dialog. A verified run's happy path has
|
|
446
|
+
ZERO questions.
|
|
400
447
|
|
|
401
448
|
NEVER re-score a bundle against a DIFFERENT recording set than the one
|
|
402
449
|
it is bound to — scoring rewrites the bundle's verification identity
|
|
@@ -408,13 +455,74 @@ denominator — if it prints INCOMPLETE with unrecorded poses, the
|
|
|
408
455
|
component is not done: record the missing poses (re-running plan tops
|
|
409
456
|
up sets recorded under the old sampled default).
|
|
410
457
|
|
|
458
|
+
## Publishing — the terminal step of a green run
|
|
459
|
+
|
|
460
|
+
A verified component's value lands when it is LIVE. This is an
|
|
461
|
+
invariant, not a preference: every run that reaches a green verify ends
|
|
462
|
+
with the user holding the live portal URL — or, if the first-publish
|
|
463
|
+
approval is still pending, unmistakably looking at the approve action.
|
|
464
|
+
No success path terminates with the component reachable only on local
|
|
465
|
+
disk. Do not ask permission to enter this sequence — the human gate is
|
|
466
|
+
the browser approval itself, which only the user can give and which no
|
|
467
|
+
machine token can fake.
|
|
468
|
+
|
|
469
|
+
The terminal sequence on green: `verify` → the inspect sheet (written
|
|
470
|
+
automatically) → `tendril_publish` → surface what it returns:
|
|
471
|
+
|
|
472
|
+
- `status: "approval-pending"` (a component's FIRST publish): relay the
|
|
473
|
+
`approveUrl` VERBATIM and name the `approveAsAccount` it carries —
|
|
474
|
+
state the account explicitly even when it looks obvious, because the
|
|
475
|
+
approving browser can be signed in to a different account than this
|
|
476
|
+
session runs under, and a mismatch strands the user on an empty
|
|
477
|
+
approve page. Do not urge them to approve: approving includes
|
|
478
|
+
accepting the design system's publishing terms, and that is their
|
|
479
|
+
decision to read and make.
|
|
480
|
+
- Then `tendril_publish_wait`, in a loop: each call waits about a
|
|
481
|
+
minute and returns either the outcome or `approval-pending` again.
|
|
482
|
+
A pending return is a HEARTBEAT, not a failure — keep calling while
|
|
483
|
+
`remainingSeconds` says the request is live, relaying a brief
|
|
484
|
+
liveness line roughly every few minutes (each return carries
|
|
485
|
+
`waitedTotalSeconds`/`remainingSeconds`; you need not narrate every
|
|
486
|
+
heartbeat). Never describe an in-flight or pending wait as stuck —
|
|
487
|
+
a decided approval continues straight into upload and commit inside
|
|
488
|
+
the same call, which can take minutes with no visible progress —
|
|
489
|
+
and never abandon the loop while the request is live unless the
|
|
490
|
+
user redirects you. A denial, a lapse and success each arrive as
|
|
491
|
+
their own sentence — report the one you got.
|
|
492
|
+
- A re-publish of an already-published component completes in one call,
|
|
493
|
+
no approval round trip — so it has NO browser gate. Announce in one
|
|
494
|
+
line what is about to go live BEFORE the call ("publishing the
|
|
495
|
+
updated Checkbox to your portal"); that line is the human-visible
|
|
496
|
+
moment for a step no click will confirm.
|
|
497
|
+
- Not signed in? Surface `tendril_login` first — the sequence pauses at
|
|
498
|
+
sign-in; it never silently skips publishing.
|
|
499
|
+
|
|
500
|
+
Sub-bar runs do NOT publish — the CLI refuses a declined run, and the
|
|
501
|
+
honest failure report is that run's correct terminal state. The
|
|
502
|
+
invariant is scoped to success.
|
|
503
|
+
|
|
504
|
+
Make the finish legible: the FIRST line of your final report is "done —
|
|
505
|
+
live at <URL>" (or "done — waiting for your Approve at <link>, as
|
|
506
|
+
<account>"), before scores, caveats and file lists. The user should
|
|
507
|
+
never have to ask where their component is.
|
|
508
|
+
|
|
509
|
+
A green verify may also print COMPOSE lines: other recorded components
|
|
510
|
+
that embed the one just verified. Relay them after the done-line as
|
|
511
|
+
DISCLOSURES (zero questions — the confirm decision is human-only, in
|
|
512
|
+
the user's terminal, with the exact commands the lines already carry),
|
|
513
|
+
and never confirm or decline a pairing yourself.
|
|
514
|
+
|
|
411
515
|
## Code Connect (extra value, after verify passes)
|
|
412
516
|
|
|
413
517
|
`tendril_codeconnect` emits a Figma Code Connect template (.figma.ts)
|
|
414
518
|
for a certified bundle: every Figma variant value mapped to its
|
|
415
|
-
verified prop fragment, stamped with the trust statement.
|
|
416
|
-
|
|
417
|
-
|
|
519
|
+
verified prop fragment, stamped with the trust statement. This is
|
|
520
|
+
NEVER a dialog on the happy path (the zero-questions rule): mention it
|
|
521
|
+
in ONE line of the final report, AFTER the done-and-live line ("Code
|
|
522
|
+
Connect template available — say the word"), and only act on it when
|
|
523
|
+
the user takes it up. It applies when the user's team is on a Figma
|
|
524
|
+
Organization/Enterprise plan (Code Connect is unavailable below
|
|
525
|
+
those). You need the component set's
|
|
418
526
|
figma.com URL (node-id included). Publishing is the USER'S action with
|
|
419
527
|
their token — `npx @figma/code-connect connect publish` — or, if this
|
|
420
528
|
session has the Figma MCP's code-connect write tools, offer to publish
|
package/dist/tendril-mcp.js
CHANGED
|
@@ -131,12 +131,12 @@ var TOOLS = [
|
|
|
131
131
|
},
|
|
132
132
|
{
|
|
133
133
|
name: "tendril_publish_wait",
|
|
134
|
-
description:
|
|
134
|
+
description: 'Phase two of the browser-approved publish. Waits in a BOUNDED window (~1 minute per call) for the user\'s Approve click on the page tendril_publish returned, then uploads the bundle, commits it, and returns the live publication URL. Call it right after relaying the approve link. While the human has not decided, each call returns `status: "approval-pending"` \u2014 that is a heartbeat, not a failure: tell the user in one short line that you are still waiting (restate the approve link and the account to approve as, ONLY if they seem lost; never urge the decision), then call this again to keep waiting. The request stays live for ~30 minutes. A denial, a lapse, and success each come back as their own sentence \u2014 report the outcome, and on success lead with the live URL. A decided approval continues straight into upload and commit INSIDE the same call \u2014 that phase can take minutes and may render no progress in some hosts; that is normal, not stuck. Some hosts render no progress at all during a call; the bounded window IS the liveness, so never describe an in-flight call as stuck \u2014 and a brief liveness line roughly every few minutes is enough, you need not narrate every heartbeat (each pending return carries waitedTotalSeconds/remainingSeconds to say where the wait stands).',
|
|
135
135
|
schema: z.object({
|
|
136
136
|
bundleDir: str("the same bundle directory tendril_publish was called with"),
|
|
137
137
|
portal: optStr("portal origin override (must match tendril_publish's)")
|
|
138
138
|
}),
|
|
139
|
-
argv: (i) => ["publish", i["bundleDir"], "--approve-wait", ...typeof i["portal"] === "string" ? ["--to", i["portal"]] : []]
|
|
139
|
+
argv: (i) => ["publish", i["bundleDir"], "--approve-wait", "--wait-window", "55", ...typeof i["portal"] === "string" ? ["--to", i["portal"]] : []]
|
|
140
140
|
},
|
|
141
141
|
{
|
|
142
142
|
name: "tendril_record_next",
|
|
@@ -151,6 +151,40 @@ var TOOLS = [
|
|
|
151
151
|
schema: z.object({ setDir: str("recording set directory") }),
|
|
152
152
|
argv: (i) => ["record", "rest-fetch", "--set", i["setDir"]]
|
|
153
153
|
},
|
|
154
|
+
{
|
|
155
|
+
name: "tendril_record_bindings",
|
|
156
|
+
description: "Fetch Figma's instance\u2192component bindings for an EXISTING MCP-recorded set \u2014 one or two batched REST calls, no re-recording, pixels/geometry/identity untouched. This is how components that are ALREADY recorded and generated become connectable: the bindings make cross-component pairing id-backed, so `tendril compose` can propose it for the human's confirm. Every binding is congruence-verified against the recorded metadata (a design that changed since recording refuses per pose, named). The CLI makes the requests itself with the user's connected Figma credential (tendril_figma_connect); you make NO Figma call. The result names the next step \u2014 including when no partner set is visible in the scanned root (co-locate the sets or pass compose --library). For sets recorded before file identity was captured, pass the design's file key as `file`.",
|
|
157
|
+
schema: z.object({ setDir: str("recording set directory"), file: optStr("the design's file key (figma.com/design/<KEY>/\u2026) \u2014 only for sets whose manifest lacks figmaFile; an operator assertion, congruence still gates every binding") }),
|
|
158
|
+
argv: (i) => ["record", "bindings", "--set", i["setDir"], ...typeof i["file"] === "string" ? ["--file", i["file"]] : []]
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
name: "tendril_compose",
|
|
162
|
+
description: "Put ONE composition pairing in front of the user's BROWSER \u2014 phase one of the browser-approved connect (invariant-5 migration; the terminal `--confirm-compositions` path remains for humans at a TTY). Requires a portal session (tendril_login \u2014 one browser Approve). Returns the approve-page link: RELAY IT verbatim, name the account the result says to approve as, and never urge the decision \u2014 the card carries the engine's honest disclosures and the human reads them. Deny on the card is NOT NOW (this request only, never a permanent decline). You cannot decide this yourself on any channel: the portal accepts the decision only from the user's signed-in browser, and the terminal flag only from an interactive TTY. A host with several open pairings needs `pair` (one card = one decision). Then finish with tendril_compose_wait. Both recording sets must be visible in one scanned workspace \u2014 co-locate them or pass `library`.",
|
|
163
|
+
schema: z.object({
|
|
164
|
+
setDir: str("the HOST recording set directory (the component that embeds the partner)"),
|
|
165
|
+
pair: optStr("the pair-key to request when the host has several open pairings"),
|
|
166
|
+
library: z.array(z.string()).optional().describe("workspace root(s) holding the partner recording set(s) \u2014 required when they live in another project root"),
|
|
167
|
+
portal: optStr("portal origin override (defaults to the stored session's portal)")
|
|
168
|
+
}),
|
|
169
|
+
argv: (i) => [
|
|
170
|
+
"compose",
|
|
171
|
+
"--set",
|
|
172
|
+
i["setDir"],
|
|
173
|
+
"--approve-start",
|
|
174
|
+
...typeof i["pair"] === "string" ? ["--pair", i["pair"]] : [],
|
|
175
|
+
...Array.isArray(i["library"]) ? i["library"].flatMap((d) => ["--library", d]) : [],
|
|
176
|
+
...typeof i["portal"] === "string" ? ["--to", i["portal"]] : []
|
|
177
|
+
]
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
name: "tendril_compose_wait",
|
|
181
|
+
description: 'Phase two of the browser-approved connect. Waits in a BOUNDED window (~1 minute per call) for the user\'s decision on the card tendril_compose returned; on Approve it records the SAME manifest entry the terminal confirm writes \u2014 after re-deriving the pairing from the CURRENT recordings and refusing if anything changed underneath the click (the click is then not wrong; the project moved \u2014 run the connect again). While undecided, each call returns `status: "approval-pending"` \u2014 a heartbeat, not a failure: one short liveness line to the user, then call again; the request stays live ~30 minutes, and a decided card can take moments to land. NOT NOW, a lapse, and success each arrive as their own sentence \u2014 report the one you got. After success: regenerate the HOST bundle with the same library roots (brief \u2192 generate \u2192 score \u2192 verify), then republish \u2014 announce the republish in one line first. Compose waits follow the same heartbeat contract as publish waits: never urge, never call an in-flight wait stuck.',
|
|
182
|
+
schema: z.object({
|
|
183
|
+
setDir: str("the same HOST set directory tendril_compose was called with"),
|
|
184
|
+
portal: optStr("portal origin override (must match tendril_compose's)")
|
|
185
|
+
}),
|
|
186
|
+
argv: (i) => ["compose", "--set", i["setDir"], "--approve-wait", "--wait-window", "55", ...typeof i["portal"] === "string" ? ["--to", i["portal"]] : []]
|
|
187
|
+
},
|
|
154
188
|
{
|
|
155
189
|
name: "tendril_record_fetch",
|
|
156
190
|
description: "Download a Figma asset URL (from get_screenshot's image_url) straight to disk and ingest it as the rep's envelope \u2014 the fallback when only the screenshot piece needs (re-)recording; for a rep's standard three recordings PREFER tendril_record_ingest_rep. Never download the image yourself: the bytes must not pass through your context.",
|
|
@@ -298,7 +332,7 @@ var TOOLS = [
|
|
|
298
332
|
},
|
|
299
333
|
{
|
|
300
334
|
name: "tendril_engine_score",
|
|
301
|
-
description: "AGENT-HARNESS engine, step 2 (the oracle): scores a candidate bundle directory against recorded truth \u2014 per-config pixels, behaviors, state parity (recording-selected \u2014 a bundle cannot unschedule it) \u2014 and returns feedback plus evidence artifacts. Iterate until allPass or two non-improving rounds. Only THIS tool's output counts as a score; never claim numbers yourself.",
|
|
335
|
+
description: "AGENT-HARNESS engine, step 2 (the oracle): scores a candidate bundle directory against recorded truth \u2014 per-config pixels, behaviors, state parity (recording-selected \u2014 a bundle cannot unschedule it) \u2014 and returns feedback plus evidence artifacts. Iterate until allPass or two non-improving rounds. Only THIS tool's output counts as a score; never claim numbers yourself. allPass ends the LOOP, not the run: finish with tendril_verify, and a green verify's terminal step is publishing \u2014 the component is done when it is LIVE, not when it scores.",
|
|
302
336
|
schema: z.object({
|
|
303
337
|
taskOrSet: str("reference task name or recording-set directory"),
|
|
304
338
|
candidateDir: str("directory containing the proposed bundle files"),
|
|
@@ -341,7 +375,7 @@ var TOOLS = [
|
|
|
341
375
|
},
|
|
342
376
|
{
|
|
343
377
|
name: "tendril_verify",
|
|
344
|
-
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.",
|
|
378
|
+
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. A GREEN run writes the inspect sheet itself and returns `next`: its terminal step is publishing (tendril_publish \u2192 tendril_publish_wait) \u2014 a verified component reachable only on local disk is an unfinished run; a sub-bar run never publishes.",
|
|
345
379
|
schema: z.object({
|
|
346
380
|
bundleDir: str("bundle directory to verify"),
|
|
347
381
|
bar: optStr("pass (default) or cert"),
|
|
@@ -494,7 +528,7 @@ var IMPLEMENT_PROMPT = {
|
|
|
494
528
|
build: (figmaUrl) => [
|
|
495
529
|
`Implement the Figma component at ${figmaUrl} as a pixel-verified React component using the Tendril pipeline.`,
|
|
496
530
|
"- If the tendril skill (or plugin) is installed, load it and follow it.",
|
|
497
|
-
"- 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.",
|
|
531
|
+
"- 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 \u2014 and on green, publish (tendril_publish, then tendril_publish_wait until decided): the run ends at the live URL or the pending approve action, never at local disk.",
|
|
498
532
|
"- Verdicts come only from Tendril's scores \u2014 never claim or estimate fidelity numbers yourself."
|
|
499
533
|
].join("\n")
|
|
500
534
|
};
|
|
@@ -521,7 +555,11 @@ for (const tool of TOOLS) {
|
|
|
521
555
|
const onProgress = token === void 0 || extra?.sendNotification === void 0 ? void 0 : (p) => {
|
|
522
556
|
void extra.sendNotification({
|
|
523
557
|
method: "notifications/progress",
|
|
524
|
-
|
|
558
|
+
// The label is the CLI's own sentence for THIS phase —
|
|
559
|
+
// scoring, uploading, waiting for an approval. A
|
|
560
|
+
// hardcoded "scored N/M configs" here made every
|
|
561
|
+
// non-scoring tool narrate someone else's work.
|
|
562
|
+
params: { progressToken: token, progress: p.done, total: p.total, message: `${p.label} (${p.done}/${p.total})` }
|
|
525
563
|
});
|
|
526
564
|
};
|
|
527
565
|
return toolResult(await runCli(tool.argv(input), 9e5, onProgress));
|