@tendrilapp/cli 0.1.28 → 0.1.30
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 -14
- package/dist/tendril-mcp.js +12 -4
- package/dist/tendril.js +172 -43
- package/package.json +1 -1
package/dist/SKILL.md
CHANGED
|
@@ -29,7 +29,13 @@ settings file the host merges — the project's
|
|
|
29
29
|
user's `~/.claude/settings.json` — contains tendril MCP entries
|
|
30
30
|
(plugin installs use `mcp__plugin_tendril_tendril__*`; direct
|
|
31
31
|
`claude mcp add` installs use `mcp__<server-name>__*`) AND shell-
|
|
32
|
-
surface entries (`Bash(ls:*)`-style prefixes plus `Write`/`Edit`)
|
|
32
|
+
surface entries (`Bash(ls:*)`-style prefixes plus `Write`/`Edit`)
|
|
33
|
+
AND Figma entries under the prefix THIS session's Figma tools
|
|
34
|
+
actually carry — take a Figma tool you can see (e.g. get_metadata)
|
|
35
|
+
and check its full `mcp__…` name against the allowlist; entries
|
|
36
|
+
under a different server's prefix are dead and DO NOT count (a
|
|
37
|
+
measured session held six such entries while every Figma call
|
|
38
|
+
prompted).
|
|
33
39
|
THE MCP LIST ALONE IS NOT "PERMISSIONS ARE FINE": a run is mostly
|
|
34
40
|
shell — mkdir, evidence reads, log writes, the printed npx commands —
|
|
35
41
|
and a measured session whose every MCP call was covered still put
|
|
@@ -40,17 +46,22 @@ and offer `tendril_permissions` with `write: true` — it merges the
|
|
|
40
46
|
per-tool allowlist PLUS the run's shell surface and project-scoped
|
|
41
47
|
Write/Edit (with deny guards so that grant can never touch
|
|
42
48
|
`./.claude/**` or `./.git/**`) into the project's
|
|
43
|
-
settings.local.json (idempotent, touches nothing else
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
49
|
+
settings.local.json (idempotent, touches nothing else). PREFIXES:
|
|
50
|
+
pass `figmaPrefix` (and `tendrilPrefix` for non-plugin installs)
|
|
51
|
+
spelled exactly as THIS session names those tools — the part before
|
|
52
|
+
`__get_metadata` — whenever they differ from the plugin defaults; a
|
|
53
|
+
measured figma-remote session got six entries that could never
|
|
54
|
+
match because the write assumed the plugin shape. These are
|
|
55
|
+
CONVENIENCE grants, not a security boundary — the command's output
|
|
56
|
+
names what they trade (read verbs read any file without prompting;
|
|
57
|
+
host Bash patterns are best-effort prefix matches; the CLI
|
|
58
|
+
subcommand grants carry the CLI's own write surface). RELAY that
|
|
59
|
+
note to the user with the offer, never soften it, and say BOTH that
|
|
60
|
+
a reload is required AND that the current session keeps prompting
|
|
61
|
+
until then — the savings land next session; that is expected, not a
|
|
62
|
+
failure. Offer ONCE per session; never run it unoffered (it edits
|
|
63
|
+
the user's settings), and if declined, proceed without mentioning
|
|
64
|
+
it again.
|
|
54
65
|
|
|
55
66
|
Long generator/score waits: silence is not a health signal, and POLL
|
|
56
67
|
cadence is not RELAY cadence (run 13: rounds landed 6–11 minutes apart,
|
|
@@ -243,8 +254,13 @@ Batch runs (several components in one session):
|
|
|
243
254
|
CSS. Import every React API you use.
|
|
244
255
|
3. `tendril_engine_score` → read the per-config results and feedback.
|
|
245
256
|
Fix FAIL configs without regressing PASS configs; re-score.
|
|
246
|
-
4. Stop when `allPass` is true
|
|
247
|
-
|
|
257
|
+
4. Stop when `allPass` is true. Two consecutive rounds with no
|
|
258
|
+
improvement is a TRIGGER TO INSPECT, not to stop: open the
|
|
259
|
+
verify-evidence diff images for the worst configs and look before
|
|
260
|
+
deciding (a measured run scored byte-identical twice, inspected
|
|
261
|
+
the diffs, and round 3 went 27/27 certified — a stop rule would
|
|
262
|
+
have shipped 5/27). Stop only after an inspection round produced
|
|
263
|
+
no fix hypothesis — then report the honest final state.
|
|
248
264
|
5. MODEL SELECTION — mechanical AND asked. `engine brief` and
|
|
249
265
|
`engine score` refuse to run without a declared model, so the
|
|
250
266
|
choice must be settled first. When a user is present, ask before
|
package/dist/tendril-mcp.js
CHANGED
|
@@ -66,11 +66,19 @@ var TOOLS = [
|
|
|
66
66
|
},
|
|
67
67
|
{
|
|
68
68
|
name: "tendril_permissions",
|
|
69
|
-
description: "Install the Claude Code permission allowlist for the Tendril pipeline (write: true \u2014 merges the MCP per-tool entries, the run's shell surface, and project-scoped Write/Edit with deny guards for ./.claude and ./.git into the project's .claude/settings.local.json, idempotent, never touches other keys), or list the entries without writing. The shell/Write grants are CONVENIENCE, not a security boundary \u2014 the output's note names exactly what they trade; RELAY it with the offer. OFFER THIS AT PIPELINE START whenever NO merged Claude settings file (project .claude/settings.local.json or .claude/settings.json, or user ~/.claude/settings.json) contains tendril MCP entries \u2014 plugin installs use mcp__plugin_tendril_tendril__*, direct claude-mcp-add installs use mcp__<server>__*
|
|
69
|
+
description: "Install the Claude Code permission allowlist for the Tendril pipeline (write: true \u2014 merges the MCP per-tool entries, the run's shell surface, and project-scoped Write/Edit with deny guards for ./.claude and ./.git into the project's .claude/settings.local.json, idempotent, never touches other keys), or list the entries without writing. The shell/Write grants are CONVENIENCE, not a security boundary \u2014 the output's note names exactly what they trade; RELAY it with the offer. OFFER THIS AT PIPELINE START whenever NO merged Claude settings file (project .claude/settings.local.json or .claude/settings.json, or user ~/.claude/settings.json) contains tendril MCP entries \u2014 plugin installs use mcp__plugin_tendril_tendril__*, direct claude-mcp-add installs use mcp__<server>__* \u2014 when this session's tool names differ from the plugin defaults, pass figmaPrefix/tendrilPrefix with the prefixes you actually see, or the written entries never match. A FILE check, never prompt-watching \u2014 agents cannot observe permission prompts. ONE approval here replaces a prompt per pipeline call. Never run it unoffered; the user must reload the session for new settings to apply \u2014 say so.",
|
|
70
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")
|
|
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
|
+
figmaPrefix: z.string().optional().describe("The Figma server's entry prefix AS THIS SESSION NAMES ITS TOOLS \u2014 the part before __get_metadata (e.g. mcp__figma-remote, mcp__plugin_figma_figma). Pass it whenever the session's Figma tools are not mcp__plugin_figma_figma__* \u2014 entries written under the wrong prefix never match anything (measured: six dead entries in a figma-remote session)."),
|
|
73
|
+
tendrilPrefix: z.string().optional().describe("Same for the tendril server when this session's tendril tools are not mcp__plugin_tendril_tendril__* (e.g. mcp__tendril for claude mcp add installs).")
|
|
72
74
|
}),
|
|
73
|
-
argv: (i) => [
|
|
75
|
+
argv: (i) => [
|
|
76
|
+
"permissions",
|
|
77
|
+
"--claude",
|
|
78
|
+
...i["write"] === true ? ["--write"] : [],
|
|
79
|
+
...typeof i["figmaPrefix"] === "string" ? ["--figma-prefix", i["figmaPrefix"]] : [],
|
|
80
|
+
...typeof i["tendrilPrefix"] === "string" ? ["--tendril-prefix", i["tendrilPrefix"]] : []
|
|
81
|
+
]
|
|
74
82
|
},
|
|
75
83
|
{
|
|
76
84
|
name: "tendril_doctor",
|
|
@@ -195,7 +203,7 @@ var TOOLS = [
|
|
|
195
203
|
{
|
|
196
204
|
name: "tendril_record_status",
|
|
197
205
|
annotations: { readOnlyHint: true },
|
|
198
|
-
description: "Recording-set completeness: per-rep recorded/missing tools.",
|
|
206
|
+
description: "Recording-set completeness: per-rep recorded/missing tools, files that exist but are unusable (invalid \u2014 re-record those), and whether the set-level token map is recorded. `complete` means USABLE by the next pipeline step, including the set-level get_variable_defs.",
|
|
199
207
|
schema: z.object({ setDir: str("recording set directory") }),
|
|
200
208
|
argv: (i) => ["record", "status", "--set", i["setDir"]]
|
|
201
209
|
},
|
package/dist/tendril.js
CHANGED
|
@@ -1083,14 +1083,54 @@ function planSet(setDir, component, symbols, opts = {}) {
|
|
|
1083
1083
|
writeManifest(setDir, manifest);
|
|
1084
1084
|
return { manifest, plan, resumed: false };
|
|
1085
1085
|
}
|
|
1086
|
+
function usableEnvelope(file, tool) {
|
|
1087
|
+
let payload;
|
|
1088
|
+
try {
|
|
1089
|
+
payload = JSON.parse(readFileSync(file, "utf8"));
|
|
1090
|
+
} catch {
|
|
1091
|
+
return { ok: false, reason: "the recorded file is not JSON" };
|
|
1092
|
+
}
|
|
1093
|
+
const schema = tool === "get_screenshot" ? ImageEnvelopeSchema : TextEnvelopeSchema;
|
|
1094
|
+
if (!schema.safeParse(payload).success) {
|
|
1095
|
+
return { ok: false, reason: "not a verbatim tool envelope (structure the ingest boundary would refuse)" };
|
|
1096
|
+
}
|
|
1097
|
+
const content = checkEnvelopeContent(tool, payload);
|
|
1098
|
+
if (!content.ok) return { ok: false, reason: content.reason };
|
|
1099
|
+
if (tool === "get_metadata" && !/name="/.test(envelopeTextContent(payload))) {
|
|
1100
|
+
return { ok: false, reason: "node markup carries no name attribute \u2014 task authoring cannot derive the pose" };
|
|
1101
|
+
}
|
|
1102
|
+
return { ok: true };
|
|
1103
|
+
}
|
|
1086
1104
|
function sessionStatus(setDir) {
|
|
1087
1105
|
const manifest = loadManifest(setDir);
|
|
1088
1106
|
const reps = manifest.reps.map((rep) => {
|
|
1089
|
-
const
|
|
1090
|
-
const
|
|
1091
|
-
|
|
1107
|
+
const required = requiredToolsFor(manifest, rep.slug);
|
|
1108
|
+
const invalid = [];
|
|
1109
|
+
const recorded = RECORD_TOOLS.filter((t) => {
|
|
1110
|
+
const file = path.join(setDir, rep.slug, `${t}.json`);
|
|
1111
|
+
if (!existsSync(file)) return false;
|
|
1112
|
+
const usable = usableEnvelope(file, t);
|
|
1113
|
+
if (!usable.ok) {
|
|
1114
|
+
invalid.push({
|
|
1115
|
+
tool: t,
|
|
1116
|
+
reason: required.includes(t) ? usable.reason : `${usable.reason} (optional for this rep \u2014 not counted against completeness; re-record only if you need this evidence)`
|
|
1117
|
+
});
|
|
1118
|
+
return false;
|
|
1119
|
+
}
|
|
1120
|
+
return true;
|
|
1121
|
+
});
|
|
1122
|
+
const missing = byProtocolOrder(required.filter((t) => !recorded.includes(t)));
|
|
1123
|
+
return { slug: rep.slug, nodeId: rep.nodeId, recorded, missing, ...invalid.length > 0 ? { invalid } : {} };
|
|
1092
1124
|
});
|
|
1093
|
-
|
|
1125
|
+
const setDefsFile = path.join(setDir, "get_variable_defs.json");
|
|
1126
|
+
const setDefs = existsSync(setDefsFile) ? usableEnvelope(setDefsFile, "get_variable_defs") : { ok: false, reason: "never recorded" };
|
|
1127
|
+
const setTokenMapRecorded = setDefs.ok;
|
|
1128
|
+
return {
|
|
1129
|
+
reps,
|
|
1130
|
+
setTokenMapRecorded,
|
|
1131
|
+
...!setDefs.ok && setDefs.reason !== "never recorded" ? { setTokenMapInvalid: setDefs.reason } : {},
|
|
1132
|
+
complete: reps.every((r) => r.missing.length === 0) && setTokenMapRecorded
|
|
1133
|
+
};
|
|
1094
1134
|
}
|
|
1095
1135
|
function nextInstruction(setDir) {
|
|
1096
1136
|
const { reps } = sessionStatus(setDir);
|
|
@@ -2192,6 +2232,8 @@ async function runTokenLint(css, fileLabel = "generated.css", definedVars2, reco
|
|
|
2192
2232
|
property: "custom-property-ident",
|
|
2193
2233
|
message: `var(${name}) is not a legal custom property reference \u2014 the browser drops the whole declaration`
|
|
2194
2234
|
});
|
|
2235
|
+
} else if (name.startsWith("--tendril-")) {
|
|
2236
|
+
continue;
|
|
2195
2237
|
} else if (definedVars2 !== void 0 && !definedVars2.has(name) && !ownDefs.has(name)) {
|
|
2196
2238
|
violations.push({
|
|
2197
2239
|
file: fileLabel,
|
|
@@ -3615,9 +3657,12 @@ function weightFromSubfamily(subfamily) {
|
|
|
3615
3657
|
return void 0;
|
|
3616
3658
|
}
|
|
3617
3659
|
function systemFaceRefusal(family) {
|
|
3618
|
-
if (/^\.?SF([ -]|$)/i.test(family) || /^San Francisco/i.test(family) || /^\.?Apple/i.test(family) || /^\.?New York
|
|
3660
|
+
if (/^\.?SF(NS|[ -]|$)/i.test(family) || /^San Francisco/i.test(family) || /^\.?Apple/i.test(family) || /^\.?New York\b/i.test(family) || /^\.?System Font\b/i.test(family)) {
|
|
3619
3661
|
return "Apple's bundled system faces (San Francisco, New York, the Apple-named families) are licensed only for mock-ups of interfaces running on Apple operating systems, by registered Apple developers \u2014 a grant Tendril cannot verify, so these faces are never cached (ROADMAP 0i).";
|
|
3620
3662
|
}
|
|
3663
|
+
if (/^\./.test(family)) {
|
|
3664
|
+
return "dot-prefixed families are the operating system's own hidden UI faces (macOS marks internal faces with a leading dot) \u2014 not user-licensable, so they are never cached (ROADMAP 0i).";
|
|
3665
|
+
}
|
|
3621
3666
|
if (/^Segoe UI Variable/i.test(family)) {
|
|
3622
3667
|
return "Segoe UI Variable cannot be licensed for use outside Microsoft products at all \u2014 an 'I have the rights' assertion has nothing it could attach to, so it is never cached (ROADMAP 0i).";
|
|
3623
3668
|
}
|
|
@@ -3943,7 +3988,7 @@ function verifiedFontFamilies(cacheDir = DEFAULT_FONT_CACHE) {
|
|
|
3943
3988
|
return [...byFamily.entries()].map(([family, w]) => ({ family, weights: [...w].sort((a, b) => a - b) }));
|
|
3944
3989
|
}
|
|
3945
3990
|
function addSystemFamily(family, opts = {}) {
|
|
3946
|
-
const refusal = systemFaceRefusal(family);
|
|
3991
|
+
const refusal = systemFaceRefusal(family.trim());
|
|
3947
3992
|
if (refusal !== void 0) return { added: [], skipped: [], overwrote: [], refusal };
|
|
3948
3993
|
const plainStyle = (sub) => weightFromSubfamily(sub) !== void 0 || /^(regular|plain|)$/i.test(sub.trim());
|
|
3949
3994
|
const faces = [...systemFacesForFamily(family, opts.dirs)].sort((a, b) => {
|
|
@@ -3961,6 +4006,11 @@ function addSystemFamily(family, opts = {}) {
|
|
|
3961
4006
|
const taken = /* @__PURE__ */ new Set();
|
|
3962
4007
|
for (const face of faces) {
|
|
3963
4008
|
const skip = (reason) => skipped.push({ subfamily: face.subfamily, weight: face.weight, reason });
|
|
4009
|
+
const faceRefusal = systemFaceRefusal(face.family.trim());
|
|
4010
|
+
if (faceRefusal !== void 0) {
|
|
4011
|
+
skip(`refusal-class family \u2014 ${faceRefusal}`);
|
|
4012
|
+
continue;
|
|
4013
|
+
}
|
|
3964
4014
|
if (opts.weights !== void 0 && !opts.weights.includes(face.weight)) continue;
|
|
3965
4015
|
if (face.italic) {
|
|
3966
4016
|
skip("italic \u2014 the cache cannot represent italic faces end to end yet; the upright face serves the family");
|
|
@@ -8129,10 +8179,11 @@ function nextPayload(setDir) {
|
|
|
8129
8179
|
const instruction = nextInstruction(setDir);
|
|
8130
8180
|
const status = sessionStatus(setDir);
|
|
8131
8181
|
const progress = { recordedReps: status.reps.filter((x) => x.missing.length === 0).length, totalReps: status.reps.length };
|
|
8132
|
-
if (instruction === null && !
|
|
8182
|
+
if (instruction === null && !status.setTokenMapRecorded) {
|
|
8133
8183
|
const manifest = loadManifest(setDir);
|
|
8134
8184
|
const frameNode = Object.keys(manifest.sourceFrames ?? {})[0] ?? manifest.reps[0]?.nodeId ?? "";
|
|
8135
|
-
|
|
8185
|
+
const invalidNote = status.setTokenMapInvalid !== void 0 ? ` (the existing set-level file is UNUSABLE \u2014 ${status.setTokenMapInvalid} \u2014 re-record it)` : "";
|
|
8186
|
+
return { slug: "__set__", nodeId: frameNode, tool: "get_variable_defs", note: `SET-LEVEL: call get_variable_defs on the component frame and ingest with --rep __set__.${invalidNote} ${ENVELOPE_HELP}`, progress };
|
|
8136
8187
|
}
|
|
8137
8188
|
return instruction === null ? { complete: true, progress } : {
|
|
8138
8189
|
...instruction,
|
|
@@ -8152,7 +8203,7 @@ function runRecordNext(opts) {
|
|
|
8152
8203
|
const payload = nextPayload(opts.setDir);
|
|
8153
8204
|
emitData(opts, payload, () => {
|
|
8154
8205
|
if (payload["complete"] === true) {
|
|
8155
|
-
process.stdout.write("set complete \u2014 every planned rep has its required envelopes\n");
|
|
8206
|
+
process.stdout.write("set complete \u2014 every planned rep has its required envelopes, all usable, and the set-level token map is recorded\n");
|
|
8156
8207
|
return;
|
|
8157
8208
|
}
|
|
8158
8209
|
const progress = payload["progress"];
|
|
@@ -8446,10 +8497,21 @@ function runRecordStatus(opts) {
|
|
|
8446
8497
|
for (const r of status.reps) {
|
|
8447
8498
|
process.stdout.write(`${r.missing.length === 0 ? "DONE " : "TODO "} ${r.slug.padEnd(24)} recorded=[${r.recorded.join(",")}]${r.missing.length > 0 ? ` missing=[${r.missing.join(",")}]` : ""}
|
|
8448
8499
|
`);
|
|
8449
|
-
|
|
8450
|
-
|
|
8451
|
-
${status.reps.filter((r) => r.missing.length > 0).length} rep(s) pending
|
|
8500
|
+
for (const inv of r.invalid ?? []) {
|
|
8501
|
+
process.stdout.write(` ${r.slug}/${inv.tool}: recorded file is UNUSABLE \u2014 ${inv.reason} (re-record this call)
|
|
8452
8502
|
`);
|
|
8503
|
+
}
|
|
8504
|
+
}
|
|
8505
|
+
const pending = status.reps.filter((r) => r.missing.length > 0).length;
|
|
8506
|
+
process.stdout.write(
|
|
8507
|
+
status.complete ? "\nset complete\n" : pending > 0 ? `
|
|
8508
|
+
${pending} rep(s) pending
|
|
8509
|
+
` : status.setTokenMapInvalid !== void 0 ? `
|
|
8510
|
+
all reps recorded, but the SET-LEVEL token map file is UNUSABLE (${status.setTokenMapInvalid}) \u2014 re-record it via \`record next\`
|
|
8511
|
+
` : `
|
|
8512
|
+
all reps recorded, but the SET-LEVEL token map is missing \u2014 run \`record next\` for the get_variable_defs step (without it briefs ship an empty token table)
|
|
8513
|
+
`
|
|
8514
|
+
);
|
|
8453
8515
|
});
|
|
8454
8516
|
}
|
|
8455
8517
|
function narrowedRoles(derived, override) {
|
|
@@ -9260,6 +9322,9 @@ function authorBehaviors(api, extras = {}) {
|
|
|
9260
9322
|
function envelopeText(file) {
|
|
9261
9323
|
return envelopeFirstTextPart(JSON.parse(readFileSync19(file, "utf8")));
|
|
9262
9324
|
}
|
|
9325
|
+
function metadataText(file) {
|
|
9326
|
+
return envelopeTextContent(JSON.parse(readFileSync19(file, "utf8")));
|
|
9327
|
+
}
|
|
9263
9328
|
function dismissEvidence(setDir, repSlugs) {
|
|
9264
9329
|
for (const slug of repSlugs) {
|
|
9265
9330
|
const f = path29.join(setDir, slug, "get_design_context.json");
|
|
@@ -9381,7 +9446,7 @@ function recordedTextSlots(setDir, repSlugs) {
|
|
|
9381
9446
|
for (const slug of repSlugs) {
|
|
9382
9447
|
const metaFile = path29.join(setDir, slug, "get_metadata.json");
|
|
9383
9448
|
if (!existsSync23(metaFile)) continue;
|
|
9384
|
-
const name = symbolName(
|
|
9449
|
+
const name = symbolName(metadataText(metaFile));
|
|
9385
9450
|
if (name === void 0) continue;
|
|
9386
9451
|
const values = /* @__PURE__ */ new Set();
|
|
9387
9452
|
for (const part of name.split(",")) {
|
|
@@ -9484,7 +9549,7 @@ function authorTaskFromSet(setDir, opts = {}) {
|
|
|
9484
9549
|
missing.push(rep.slug);
|
|
9485
9550
|
continue;
|
|
9486
9551
|
}
|
|
9487
|
-
const name = symbolName(
|
|
9552
|
+
const name = symbolName(metadataText(metaFile));
|
|
9488
9553
|
if (name === void 0) {
|
|
9489
9554
|
missing.push(rep.slug);
|
|
9490
9555
|
continue;
|
|
@@ -9495,7 +9560,7 @@ function authorTaskFromSet(setDir, opts = {}) {
|
|
|
9495
9560
|
throw new Error(`recording set ${setDir} is missing metadata for ${missing.length} rep(s): ${missing.slice(0, 5).join(", ")}${missing.length > 5 ? ", \u2026" : ""}`);
|
|
9496
9561
|
}
|
|
9497
9562
|
const setMeta = path29.join(setDir, "get_metadata.json");
|
|
9498
|
-
const latticeNames = manifest.latticeNames ?? (existsSync23(setMeta) ? [...
|
|
9563
|
+
const latticeNames = manifest.latticeNames ?? (existsSync23(setMeta) ? [...metadataText(setMeta).matchAll(/name="([^"]*)"/g)].map((m) => decodeXmlEntities(m[1])) : void 0);
|
|
9499
9564
|
const defaults = { ...manifest.defaults ?? {}, ...opts.defaults ?? {} };
|
|
9500
9565
|
const recordedFonts = recordedFontFamilies(setDir);
|
|
9501
9566
|
const textSlots = recordedTextSlots(setDir, manifest.reps.map((r) => r.slug));
|
|
@@ -9951,7 +10016,7 @@ function fontsCssFor(faces, bundleDir, cacheDir = DEFAULT_FONT_CACHE, substitute
|
|
|
9951
10016
|
const family = face.family.replace(/['\\]/g, "").replace(/\*\//g, "");
|
|
9952
10017
|
const decl = `@font-face { font-family: '${family}'; font-weight: ${face.weight}; font-style: normal; src: url(${target}) format('${format}'); }`;
|
|
9953
10018
|
const license = normalizeFontLicense(face.license);
|
|
9954
|
-
const terms = face.source.startsWith("system:") ? void 0 : REDISTRIBUTABLE.get(license);
|
|
10019
|
+
const terms = face.source.startsWith("system:") || face.source.startsWith("local:") ? void 0 : REDISTRIBUTABLE.get(license);
|
|
9955
10020
|
if (terms === void 0) {
|
|
9956
10021
|
if (face.source.startsWith("system:")) {
|
|
9957
10022
|
lines.push(
|
|
@@ -10677,11 +10742,17 @@ import { existsSync as existsSync26, readFileSync as readFileSync22 } from "node
|
|
|
10677
10742
|
import path33 from "node:path";
|
|
10678
10743
|
async function runFontsResolve(opts) {
|
|
10679
10744
|
const result = await resolveFonts(opts.family, opts.weights, opts.cacheDir);
|
|
10680
|
-
|
|
10745
|
+
const queryRefusal = systemFaceRefusal(opts.family.trim());
|
|
10746
|
+
emitData(opts, { ...result, ...queryRefusal !== void 0 ? { queryRefusal } : {} }, () => {
|
|
10681
10747
|
for (const f of result.resolved) process.stdout.write(`resolved ${f.family} ${f.weight} (${f.sha256.slice(0, 12)}\u2026)
|
|
10682
10748
|
`);
|
|
10683
10749
|
for (const f of result.failures) process.stdout.write(`FAILED ${f.family} ${f.weight}: ${f.reason}
|
|
10684
10750
|
`);
|
|
10751
|
+
if (queryRefusal !== void 0 && result.failures.length > 0) {
|
|
10752
|
+
process.stdout.write(` "${opts.family}" is a refusal-class family: ${queryRefusal}
|
|
10753
|
+
Options: proceed under a disclosed substitute (rules out certification), or register a file you license yourself: ${tendrilCommand(`fonts add ${quoteArg(opts.family)} <weight> <file>`)} \u2014 bundles declare the face and never carry its bytes.
|
|
10754
|
+
`);
|
|
10755
|
+
}
|
|
10685
10756
|
});
|
|
10686
10757
|
if (result.failures.length > 0) {
|
|
10687
10758
|
warn(opts, `${result.failures.length} face(s) unresolved \u2014 verification will refuse to score under substitution`);
|
|
@@ -10739,7 +10810,8 @@ async function runFontsResolveSet(opts) {
|
|
|
10739
10810
|
unlicensed.push(...result.failures);
|
|
10740
10811
|
}
|
|
10741
10812
|
}
|
|
10742
|
-
|
|
10813
|
+
const refusals = [...new Set(failures.map((f) => f.family))].map((family) => ({ family, reason: systemFaceRefusal(family.trim()) })).filter((r) => r.reason !== void 0);
|
|
10814
|
+
emitData(opts, { set: setDir, needs, cached: cached2, resolved, relicensed, unlicensed, failures, ...refusals.length > 0 ? { refusals } : {} }, () => {
|
|
10743
10815
|
process.stdout.write(`set declares: ${needs.map((n) => `${n.family} (${n.weights.join(", ")})`).join(" \xB7 ")}
|
|
10744
10816
|
`);
|
|
10745
10817
|
for (const f of cached2) process.stdout.write(`cached ${f.family} ${f.weight}
|
|
@@ -10753,8 +10825,11 @@ async function runFontsResolveSet(opts) {
|
|
|
10753
10825
|
for (const f of failures) process.stdout.write(`FAILED ${f.family} ${f.weight}: ${f.reason}
|
|
10754
10826
|
`);
|
|
10755
10827
|
for (const fam of [...new Set(failures.map((f) => f.family))]) {
|
|
10828
|
+
const refusal = systemFaceRefusal(fam);
|
|
10756
10829
|
process.stdout.write(
|
|
10757
|
-
|
|
10830
|
+
refusal !== void 0 ? ` "${fam}" is a refusal-class family: ${refusal}
|
|
10831
|
+
Options: proceed and let the mount substitute a provided face (disclosed; rules out certification), or register a file you license yourself: ${tendrilCommand(`fonts add ${quoteArg(fam)} <weight> <file>`)} \u2014 licensing is yours to judge; bundles declare the face and never carry its bytes.
|
|
10832
|
+
` : ` if "${fam}" is installed on this machine: ${tendrilCommand(`fonts discover ${quoteArg(fam)}`)} then ${tendrilCommand(`fonts add-system ${quoteArg(fam)} --set ${quoteArg(setDir)}`)} (licence stays unknown; bundles declare, never copy)
|
|
10758
10833
|
`
|
|
10759
10834
|
);
|
|
10760
10835
|
}
|
|
@@ -10855,12 +10930,20 @@ function runFontsAdd(opts) {
|
|
|
10855
10930
|
});
|
|
10856
10931
|
}
|
|
10857
10932
|
function runFontsDiscover(opts) {
|
|
10858
|
-
const faces = opts.family !== void 0 ? systemFacesForFamily(opts.family) : discoverSystemFaces();
|
|
10933
|
+
const faces = opts.family !== void 0 ? systemFacesForFamily(opts.family, opts.dirs) : discoverSystemFaces(opts.dirs);
|
|
10859
10934
|
const rows = faces.map((f) => ({ ...f, ...systemFaceRefusal(f.family) !== void 0 ? { refused: systemFaceRefusal(f.family) } : {} }));
|
|
10860
|
-
|
|
10935
|
+
const queryRefusal = opts.family !== void 0 ? systemFaceRefusal(opts.family) : void 0;
|
|
10936
|
+
emitData(opts, { faces: rows, dirs: opts.dirs ?? systemFontDirs(), ...queryRefusal !== void 0 ? { queryRefusal } : {} }, () => {
|
|
10861
10937
|
if (rows.length === 0) {
|
|
10862
|
-
process.stdout.write(`no matching system faces found (scanned: ${systemFontDirs().join(", ")})
|
|
10938
|
+
process.stdout.write(`no matching system faces found (scanned: ${(opts.dirs ?? systemFontDirs()).join(", ")})
|
|
10863
10939
|
`);
|
|
10940
|
+
if (queryRefusal !== void 0) {
|
|
10941
|
+
process.stdout.write(
|
|
10942
|
+
`note: "${opts.family}" is a refusal-class family regardless \u2014 ${queryRefusal}
|
|
10943
|
+
A file you license yourself can still be registered for local scoring: ${tendrilCommand(`fonts add ${quoteArg(opts.family ?? "")} <weight> <file>`)} \u2014 licensing is yours to judge, and bundles declare the face but never carry its bytes.
|
|
10944
|
+
`
|
|
10945
|
+
);
|
|
10946
|
+
}
|
|
10864
10947
|
return;
|
|
10865
10948
|
}
|
|
10866
10949
|
for (const f of rows) {
|
|
@@ -11065,14 +11148,15 @@ function latticeCoverage(setManifest, scoredConfigs) {
|
|
|
11065
11148
|
const lattice = setManifest.latticeNames?.length;
|
|
11066
11149
|
const established = setManifest.variantScope === "component-set";
|
|
11067
11150
|
const notRecorded = setManifest.notRecorded !== void 0 && setManifest.notRecorded !== "" ? { notRecorded: setManifest.notRecorded } : {};
|
|
11068
|
-
|
|
11151
|
+
const latticeUnestablished = setManifest.variantScope === "selection" ? "the recording was planned from a node SELECTION, not from a component set \u2014 the plan counted the nodes it was handed and could not see whether the set holds more variants, so the number of unrecorded poses is UNKNOWN, not zero" : setManifest.variantScope === "component-set" ? "the planner saw the component set and it declares no variant axes \u2014 the planned pose(s) are the whole set as Figma showed it, but there is no variant lattice to count" : "this set was planned before variant scope was recorded, so whether its lattice came from a component set or from a handed-in selection was never established \u2014 the number of unrecorded poses is unknown, not zero";
|
|
11152
|
+
if (lattice === void 0) return { latticeConfigs: null, latticeUnestablished, ...notRecorded };
|
|
11069
11153
|
if (established) return { latticeConfigs: lattice, unrecordedConfigs: Math.max(0, lattice - scoredConfigs), ...notRecorded };
|
|
11070
11154
|
return {
|
|
11071
11155
|
latticeConfigs: null,
|
|
11072
11156
|
// The count is still worth reporting — it is just not a
|
|
11073
11157
|
// denominator, and the name has to stop implying that it is.
|
|
11074
11158
|
variantsPlanned: lattice,
|
|
11075
|
-
latticeUnestablished
|
|
11159
|
+
latticeUnestablished,
|
|
11076
11160
|
...notRecorded
|
|
11077
11161
|
};
|
|
11078
11162
|
}
|
|
@@ -11472,9 +11556,14 @@ ${certified}/${statuses.length} certified \xB7 ${report.coverage.pass}/${statuse
|
|
|
11472
11556
|
`INCOMPLETE ${cov.unrecordedConfigs} of ${cov.latticeConfigs} lattice poses were NEVER RECORDED \u2014 nothing verifies them; any implementation of those poses is inference, not verified truth. Re-plan the set (full matrix is the default) and record the missing poses.
|
|
11473
11557
|
`
|
|
11474
11558
|
);
|
|
11475
|
-
} else if (cov.latticeConfigs ===
|
|
11476
|
-
process.stdout.write(`
|
|
11559
|
+
} else if (typeof cov.latticeConfigs === "number") {
|
|
11560
|
+
process.stdout.write(`LATTICE all ${cov.latticeConfigs} poses of the component set's own lattice are recorded and scored \u2014 the denominator is the set's, not the recorded subset's
|
|
11477
11561
|
`);
|
|
11562
|
+
} else if (cov.latticeConfigs === null) {
|
|
11563
|
+
process.stdout.write(
|
|
11564
|
+
`COVERAGE denominator unknown \u2014 ${cov.latticeUnestablished ?? "set predates lattice tracking"} \u2014 scored configs are verified; completeness is not established
|
|
11565
|
+
`
|
|
11566
|
+
);
|
|
11478
11567
|
}
|
|
11479
11568
|
}
|
|
11480
11569
|
process.stdout.write(`environment: chrome=${report.environment.chromeVersion ?? report.environment.chrome} fonts=${report.environment.fontsManifestSha256 ?? "UNRESOLVED"}
|
|
@@ -11683,7 +11772,7 @@ ${segments}`;
|
|
|
11683
11772
|
`Run \`${tendrilCommand(
|
|
11684
11773
|
`engine score ${quoteArg(ref)} ${quoteArg(candidateDirSuggestion)} --bar ${opts.bar} --model ${opts.model ?? "<declared-model>"} --json`
|
|
11685
11774
|
)}\` \u2014 the CLI is the sole judge, and it refuses to score an undeclared model. If you wrote the bundle elsewhere, pass that directory instead.`,
|
|
11686
|
-
"Apply the returned feedback and re-score. Stop when all checks pass
|
|
11775
|
+
"Apply the returned feedback and re-score. Stop when all checks pass. Two consecutive non-improving scores mean INSPECT the verify-evidence diffs before deciding \u2014 stop only when inspection yields no fix hypothesis (a measured run was byte-identical twice, then went 27/27 after reading the diffs).",
|
|
11687
11776
|
"Never edit recording sets; never claim scores yourself \u2014 only the score command's output counts."
|
|
11688
11777
|
]
|
|
11689
11778
|
},
|
|
@@ -11696,7 +11785,10 @@ ${segments}`;
|
|
|
11696
11785
|
);
|
|
11697
11786
|
}
|
|
11698
11787
|
function appendScoreHistory(candidateDir, entry) {
|
|
11699
|
-
|
|
11788
|
+
const file = path35.join(candidateDir, "score-history.jsonl");
|
|
11789
|
+
const starts = existsSync28(file) ? readFileSync24(file, "utf8").split("\n").filter((l) => l.includes('"event":"round-start"')).length : 0;
|
|
11790
|
+
const round = entry.event === "round-start" ? starts + 1 : Math.max(1, starts);
|
|
11791
|
+
appendFileSync(file, `${JSON.stringify({ at: (/* @__PURE__ */ new Date()).toISOString(), round, ...entry })}
|
|
11700
11792
|
`);
|
|
11701
11793
|
}
|
|
11702
11794
|
async function runEngineScore(opts) {
|
|
@@ -11784,7 +11876,10 @@ ${absentFindings.join("\n")}` : "";
|
|
|
11784
11876
|
|
|
11785
11877
|
CERTIFICATION: ${certifiedReps.length}/${scores.length} configs at the certification bar (sim \u2265${certBar.sim} AND ink \u2265${certBar.ink}, exact values, after parity demotion \u2014 composition checks at verify can demote further).${certifiedReps.length < scores.length ? ` Below cert: ${scores.filter((sc) => !certifiedSet.has(sc.rep)).map((sc) => sc.rep).join(", ")}.` : ""}
|
|
11786
11878
|
METRIC DEADBAND (read before iterating on near-misses): the scored similarity/ink deliberately tolerate \xB11px edge shift and antialiased-edge differences \u2014 cross-rasterizer noise absorption. A change entirely inside that band moves these numbers by EXACTLY ZERO (working as designed, not a stuck scorer). The per-config \`exact\` fields in the JSON are tolerance-free and move first: compare exact across rounds to confirm a small fix landed, and stop iterating when only exact moves \u2014 the bar reads the tolerant numbers.`;
|
|
11787
|
-
const
|
|
11879
|
+
const stampNotice = `
|
|
11880
|
+
|
|
11881
|
+
PROVENANCE STAMP: this scoring call itself (the Tendril CLI) just wrote/refreshed a comment on line 1 of styles.css carrying these scores, marked non-authoritative. If your editor or host reports styles.css was modified externally, that modification is this scorer \u2014 expected, not tampering. Keep the comment; it self-invalidates on any edit and \`tendril verify\` recomputes it.`;
|
|
11882
|
+
const feedback = buildFeedback(scores, behaviors, bar, "files") + certificationFeedback + stampNotice + qualityFeedback;
|
|
11788
11883
|
const emitted = emitBundleV1({
|
|
11789
11884
|
bundleDir: candidateDir,
|
|
11790
11885
|
task,
|
|
@@ -11849,7 +11944,7 @@ METRIC DEADBAND (read before iterating on near-misses): the scored similarity/in
|
|
|
11849
11944
|
parityCoverage,
|
|
11850
11945
|
evidenceDir,
|
|
11851
11946
|
bundleManifest: emitted.written[0],
|
|
11852
|
-
note: "
|
|
11947
|
+
note: "the Tendril CLI (this scoring call) wrote the bundle provenance comment onto styles.css line 1 \u2014 an EXPECTED external modification, not tampering; preserve it in any post-score edit (it self-invalidates on edit and verify recomputes it)",
|
|
11853
11948
|
allPass,
|
|
11854
11949
|
environment: { ...environmentStamp(taskFontFamilies(task.set)), ruler: cliVersion() },
|
|
11855
11950
|
...lcdTextEnabled() ? { environmentOverrides: ["enable-lcd-text"] } : {},
|
|
@@ -11977,7 +12072,7 @@ function runCodeConnect(opts) {
|
|
|
11977
12072
|
const meta = path36.join(setDir, r.slug, "get_metadata.json");
|
|
11978
12073
|
if (!existsSync29(meta)) return void 0;
|
|
11979
12074
|
try {
|
|
11980
|
-
return /name="([^"]*)"/.exec(
|
|
12075
|
+
return /name="([^"]*)"/.exec(envelopeTextContent(JSON.parse(readFileSync25(meta, "utf8"))))?.[1];
|
|
11981
12076
|
} catch {
|
|
11982
12077
|
return void 0;
|
|
11983
12078
|
}
|
|
@@ -12164,11 +12259,19 @@ var init_server = __esm({
|
|
|
12164
12259
|
},
|
|
12165
12260
|
{
|
|
12166
12261
|
name: "tendril_permissions",
|
|
12167
|
-
description: "Install the Claude Code permission allowlist for the Tendril pipeline (write: true \u2014 merges the MCP per-tool entries, the run's shell surface, and project-scoped Write/Edit with deny guards for ./.claude and ./.git into the project's .claude/settings.local.json, idempotent, never touches other keys), or list the entries without writing. The shell/Write grants are CONVENIENCE, not a security boundary \u2014 the output's note names exactly what they trade; RELAY it with the offer. OFFER THIS AT PIPELINE START whenever NO merged Claude settings file (project .claude/settings.local.json or .claude/settings.json, or user ~/.claude/settings.json) contains tendril MCP entries \u2014 plugin installs use mcp__plugin_tendril_tendril__*, direct claude-mcp-add installs use mcp__<server>__*
|
|
12262
|
+
description: "Install the Claude Code permission allowlist for the Tendril pipeline (write: true \u2014 merges the MCP per-tool entries, the run's shell surface, and project-scoped Write/Edit with deny guards for ./.claude and ./.git into the project's .claude/settings.local.json, idempotent, never touches other keys), or list the entries without writing. The shell/Write grants are CONVENIENCE, not a security boundary \u2014 the output's note names exactly what they trade; RELAY it with the offer. OFFER THIS AT PIPELINE START whenever NO merged Claude settings file (project .claude/settings.local.json or .claude/settings.json, or user ~/.claude/settings.json) contains tendril MCP entries \u2014 plugin installs use mcp__plugin_tendril_tendril__*, direct claude-mcp-add installs use mcp__<server>__* \u2014 when this session's tool names differ from the plugin defaults, pass figmaPrefix/tendrilPrefix with the prefixes you actually see, or the written entries never match. A FILE check, never prompt-watching \u2014 agents cannot observe permission prompts. ONE approval here replaces a prompt per pipeline call. Never run it unoffered; the user must reload the session for new settings to apply \u2014 say so.",
|
|
12168
12263
|
schema: z12.object({
|
|
12169
|
-
write: z12.boolean().optional().describe("true = install into .claude/settings.local.json (the point of this tool); false/absent = just return the entries")
|
|
12264
|
+
write: z12.boolean().optional().describe("true = install into .claude/settings.local.json (the point of this tool); false/absent = just return the entries"),
|
|
12265
|
+
figmaPrefix: z12.string().optional().describe("The Figma server's entry prefix AS THIS SESSION NAMES ITS TOOLS \u2014 the part before __get_metadata (e.g. mcp__figma-remote, mcp__plugin_figma_figma). Pass it whenever the session's Figma tools are not mcp__plugin_figma_figma__* \u2014 entries written under the wrong prefix never match anything (measured: six dead entries in a figma-remote session)."),
|
|
12266
|
+
tendrilPrefix: z12.string().optional().describe("Same for the tendril server when this session's tendril tools are not mcp__plugin_tendril_tendril__* (e.g. mcp__tendril for claude mcp add installs).")
|
|
12170
12267
|
}),
|
|
12171
|
-
argv: (i) => [
|
|
12268
|
+
argv: (i) => [
|
|
12269
|
+
"permissions",
|
|
12270
|
+
"--claude",
|
|
12271
|
+
...i["write"] === true ? ["--write"] : [],
|
|
12272
|
+
...typeof i["figmaPrefix"] === "string" ? ["--figma-prefix", i["figmaPrefix"]] : [],
|
|
12273
|
+
...typeof i["tendrilPrefix"] === "string" ? ["--tendril-prefix", i["tendrilPrefix"]] : []
|
|
12274
|
+
]
|
|
12172
12275
|
},
|
|
12173
12276
|
{
|
|
12174
12277
|
name: "tendril_doctor",
|
|
@@ -12293,7 +12396,7 @@ var init_server = __esm({
|
|
|
12293
12396
|
{
|
|
12294
12397
|
name: "tendril_record_status",
|
|
12295
12398
|
annotations: { readOnlyHint: true },
|
|
12296
|
-
description: "Recording-set completeness: per-rep recorded/missing tools.",
|
|
12399
|
+
description: "Recording-set completeness: per-rep recorded/missing tools, files that exist but are unusable (invalid \u2014 re-record those), and whether the set-level token map is recorded. `complete` means USABLE by the next pipeline step, including the set-level get_variable_defs.",
|
|
12297
12400
|
schema: z12.object({ setDir: str("recording set directory") }),
|
|
12298
12401
|
argv: (i) => ["record", "status", "--set", i["setDir"]]
|
|
12299
12402
|
},
|
|
@@ -12451,6 +12554,13 @@ function mergeAllowlist(file, entries, denyEntries = []) {
|
|
|
12451
12554
|
}
|
|
12452
12555
|
async function buildPermissions(options) {
|
|
12453
12556
|
const LEGAL_TOOL_NAME = /^[A-Za-z0-9_-]+$/;
|
|
12557
|
+
for (const [flag, value] of [["--figma-prefix", options.figmaPrefix], ["--tendril-prefix", options.tendrilPrefix]]) {
|
|
12558
|
+
if (value !== void 0 && !LEGAL_MCP_PREFIX.test(value)) {
|
|
12559
|
+
throw new Error(`${flag} must look like mcp__<server-name> (letters, digits, _ and - only) \u2014 got ${JSON.stringify(value)}`);
|
|
12560
|
+
}
|
|
12561
|
+
}
|
|
12562
|
+
const figmaPrefix = options.figmaPrefix ?? FIGMA_PLUGIN_PREFIX;
|
|
12563
|
+
const tendrilPrefix = options.tendrilPrefix ?? TENDRIL_PLUGIN_PREFIX;
|
|
12454
12564
|
const pipeline = new Set(FIGMA_TOOL_FALLBACK);
|
|
12455
12565
|
let figmaTools = FIGMA_TOOL_FALLBACK;
|
|
12456
12566
|
try {
|
|
@@ -12462,10 +12572,10 @@ async function buildPermissions(options) {
|
|
|
12462
12572
|
}
|
|
12463
12573
|
return {
|
|
12464
12574
|
host: "claude",
|
|
12465
|
-
serverEntries: [
|
|
12575
|
+
serverEntries: [tendrilPrefix, figmaPrefix],
|
|
12466
12576
|
toolEntries: [
|
|
12467
|
-
...TOOLS.map((t) => t.name).filter((n) => LEGAL_TOOL_NAME.test(n)).map((n) => `${
|
|
12468
|
-
...figmaTools.map((name) => `${
|
|
12577
|
+
...TOOLS.map((t) => t.name).filter((n) => LEGAL_TOOL_NAME.test(n)).map((n) => `${tendrilPrefix}__${n}`),
|
|
12578
|
+
...figmaTools.map((name) => `${figmaPrefix}__${name}`)
|
|
12469
12579
|
],
|
|
12470
12580
|
shellEntries: [...SHELL_READONLY_ENTRIES, ...SHELL_PIPELINE_ENTRIES],
|
|
12471
12581
|
projectFileEntries: PROJECT_FILE_ENTRIES,
|
|
@@ -12473,7 +12583,7 @@ async function buildPermissions(options) {
|
|
|
12473
12583
|
manualCautionEntries: MANUAL_CAUTION_ENTRIES,
|
|
12474
12584
|
shellEntriesNote: "These are CONVENIENCE grants replacing the ~300 prompts a measured run costs \u2014 not a security boundary. What they trade, named: (1) the read verbs (cat/grep/head/\u2026) let the agent read ANY file without a prompt, secrets included \u2014 remove those entries if that is unacceptable; (2) Claude Code Bash patterns are prefix matches and redirection is not a command separator (`cat x > file` rides `Bash(cat:*)`) \u2014 the host's docs call argument patterns best-effort, not an enforcement boundary (pipes and `&&`/`;` chains do decompose, each piece prompting on its own); (3) the per-subcommand CLI grants carry those subcommands' own write surface (recordings, bundles, score history, --out paths) and the npx form trusts future @latest publishes. Never auto-written: a blanket `tendril` grant (an agent could silently widen this very allowlist via `tendril permissions --write`), `find` (arbitrary delete/exec via -delete/-exec), clobber verbs \u2014 those stay a manual, deliberate addition. Write/Edit are project-scoped, written only to the project-local file, with deny entries for ./.claude/** and ./.git/** so the grant can never rewrite this settings file or plant git hooks. Every entry is removable any time.",
|
|
12475
12585
|
serverEntriesCaution: "The server-wide entries allow EVERY tool those servers serve \u2014 for tendril that includes tendril_generate_curated (spends your OpenRouter budget when configured), and for Figma every tool the desktop server exposes, beyond the read tools this pipeline uses. The per-tool list is the recommended default.",
|
|
12476
|
-
directConfigNote: `
|
|
12586
|
+
directConfigNote: `Prefixes default to the plugin install shape. A different install (claude mcp add, the remote Figma server, \u2026) names its tools mcp__<server-name>__<tool> \u2014 pass the real prefixes with --figma-prefix / --tendril-prefix (MCP: figmaPrefix / tendrilPrefix) so the written entries actually match; run 20 measured six Figma entries that could never fire because the session's server was figma-remote.`
|
|
12477
12587
|
};
|
|
12478
12588
|
}
|
|
12479
12589
|
function writeSelection(result, user) {
|
|
@@ -12484,7 +12594,21 @@ async function runPermissions(flags) {
|
|
|
12484
12594
|
printDescription(PERMISSIONS_DESCRIPTION);
|
|
12485
12595
|
return;
|
|
12486
12596
|
}
|
|
12487
|
-
|
|
12597
|
+
let result;
|
|
12598
|
+
try {
|
|
12599
|
+
result = await buildPermissions({
|
|
12600
|
+
...flags.mcpUrl ? { mcpUrl: flags.mcpUrl } : {},
|
|
12601
|
+
...flags.figmaPrefix !== void 0 ? { figmaPrefix: flags.figmaPrefix } : {},
|
|
12602
|
+
...flags.tendrilPrefix !== void 0 ? { tendrilPrefix: flags.tendrilPrefix } : {}
|
|
12603
|
+
});
|
|
12604
|
+
} catch (err) {
|
|
12605
|
+
fail(flags, ExitCode.InputValidation, {
|
|
12606
|
+
error: err instanceof Error ? err.message : String(err),
|
|
12607
|
+
code: "bad-prefix",
|
|
12608
|
+
remediation: "Pass the prefix exactly as the session names its MCP tools: the part before the final __<tool> (e.g. mcp__figma-remote, mcp__plugin_figma_figma)."
|
|
12609
|
+
});
|
|
12610
|
+
return;
|
|
12611
|
+
}
|
|
12488
12612
|
if (flags.write) {
|
|
12489
12613
|
const base = process.env["INIT_CWD"] ?? process.cwd();
|
|
12490
12614
|
const file = flags.user ? path38.join(os8.homedir(), ".claude", "settings.json") : path38.join(base, ".claude", "settings.local.json");
|
|
@@ -12559,7 +12683,7 @@ ${result.directConfigNote}
|
|
|
12559
12683
|
);
|
|
12560
12684
|
});
|
|
12561
12685
|
}
|
|
12562
|
-
var FIGMA_TOOL_FALLBACK, SHELL_READONLY_ENTRIES, SHELL_PIPELINE_ENTRIES, PROJECT_FILE_ENTRIES, PROJECT_DENY_ENTRIES, MANUAL_CAUTION_ENTRIES, PERMISSIONS_DESCRIPTION, TENDRIL_PLUGIN_PREFIX, FIGMA_PLUGIN_PREFIX;
|
|
12686
|
+
var FIGMA_TOOL_FALLBACK, SHELL_READONLY_ENTRIES, SHELL_PIPELINE_ENTRIES, PROJECT_FILE_ENTRIES, PROJECT_DENY_ENTRIES, MANUAL_CAUTION_ENTRIES, PERMISSIONS_DESCRIPTION, TENDRIL_PLUGIN_PREFIX, FIGMA_PLUGIN_PREFIX, LEGAL_MCP_PREFIX;
|
|
12563
12687
|
var init_permissions = __esm({
|
|
12564
12688
|
"packages/cli/src/commands/permissions.ts"() {
|
|
12565
12689
|
"use strict";
|
|
@@ -12610,6 +12734,8 @@ var init_permissions = __esm({
|
|
|
12610
12734
|
{ flag: "--write", description: "Merge the pipeline allowlist (MCP tools + shell surface + project Write/Edit, with deny guards for ./.claude and ./.git) into .claude/settings.local.json in the current project (idempotent; creates the file; never touches other keys)" },
|
|
12611
12735
|
{ flag: "--user", description: "With --write: target ~/.claude/settings.json instead \u2014 MCP tool entries ONLY; shell and Write/Edit grants stay project-local, never every-project" },
|
|
12612
12736
|
{ flag: "--mcp-url <url>", description: "Figma MCP endpoint to list live tool names from", default: DEFAULT_MCP_URL },
|
|
12737
|
+
{ flag: "--figma-prefix <prefix>", description: "The session's real Figma MCP entry prefix (e.g. mcp__figma-remote) \u2014 the plugin-install default cannot match other install shapes" },
|
|
12738
|
+
{ flag: "--tendril-prefix <prefix>", description: "The session's real tendril MCP entry prefix (e.g. mcp__tendril for claude mcp add installs)" },
|
|
12613
12739
|
{ flag: "--json", description: "Machine-readable output" }
|
|
12614
12740
|
],
|
|
12615
12741
|
output: {
|
|
@@ -12624,6 +12750,7 @@ var init_permissions = __esm({
|
|
|
12624
12750
|
};
|
|
12625
12751
|
TENDRIL_PLUGIN_PREFIX = "mcp__plugin_tendril_tendril";
|
|
12626
12752
|
FIGMA_PLUGIN_PREFIX = "mcp__plugin_figma_figma";
|
|
12753
|
+
LEGAL_MCP_PREFIX = /^mcp__[A-Za-z0-9_-]+$/;
|
|
12627
12754
|
}
|
|
12628
12755
|
});
|
|
12629
12756
|
|
|
@@ -14114,7 +14241,7 @@ function buildProgram() {
|
|
|
14114
14241
|
...local["out"] !== void 0 ? { out: local["out"] } : {}
|
|
14115
14242
|
});
|
|
14116
14243
|
});
|
|
14117
|
-
program.command("permissions").description("Install (or print) the Claude Code permission allowlist for the Tendril pipeline (tendril + Figma MCP tools) \u2014 no more transcribing tool names from prompts.").option("--claude", "Claude Code settings format (the default and currently only format)").option("--write", "merge the per-tool entries into .claude/settings.local.json in the current project (idempotent; never touches other keys)").option("--user", "with --write: target ~/.claude/settings.json
|
|
14244
|
+
program.command("permissions").description("Install (or print) the Claude Code permission allowlist for the Tendril pipeline (tendril + Figma MCP tools) \u2014 no more transcribing tool names from prompts.").option("--claude", "Claude Code settings format (the default and currently only format)").option("--write", "merge the per-tool entries into .claude/settings.local.json in the current project (idempotent; never touches other keys)").option("--user", "with --write: target ~/.claude/settings.json \u2014 MCP tool entries only; shell and Write/Edit grants stay project-local").option("--mcp-url <url>", "Figma MCP endpoint to list live tool names from").option("--figma-prefix <prefix>", "the session's real Figma MCP entry prefix (e.g. mcp__figma-remote) when it is not the plugin default").option("--tendril-prefix <prefix>", "the session's real tendril MCP entry prefix (e.g. mcp__tendril for claude mcp add installs)").action(async (_o, cmd) => {
|
|
14118
14245
|
const flags = globalFlags(cmd.parent);
|
|
14119
14246
|
const local = cmd.opts();
|
|
14120
14247
|
const { runPermissions: runPermissions2 } = await Promise.resolve().then(() => (init_permissions(), permissions_exports));
|
|
@@ -14122,7 +14249,9 @@ function buildProgram() {
|
|
|
14122
14249
|
...flags,
|
|
14123
14250
|
...local["write"] !== void 0 ? { write: local["write"] } : {},
|
|
14124
14251
|
...local["user"] !== void 0 ? { user: local["user"] } : {},
|
|
14125
|
-
...local["mcpUrl"] !== void 0 ? { mcpUrl: local["mcpUrl"] } : {}
|
|
14252
|
+
...local["mcpUrl"] !== void 0 ? { mcpUrl: local["mcpUrl"] } : {},
|
|
14253
|
+
...local["figmaPrefix"] !== void 0 ? { figmaPrefix: local["figmaPrefix"] } : {},
|
|
14254
|
+
...local["tendrilPrefix"] !== void 0 ? { tendrilPrefix: local["tendrilPrefix"] } : {}
|
|
14126
14255
|
});
|
|
14127
14256
|
});
|
|
14128
14257
|
program.command("inspect").description("Build an eye-verifiable detail sheet from verify evidence: magnified recorded-vs-rendered crops of every small recorded node (icons, controls, marks) plus full-frame triples, in one static HTML page.").argument("<bundleDir>", "bundle directory with component.json and verify-evidence").option("--set <dir>", "recording set override (default: the bundle's provenance path)").option("--max-area <px2>", "node area ceiling for the detail sweep", "1024").action(async (bundleDir, _o, cmd) => {
|