@tendrilapp/cli 0.1.28 → 0.1.29
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 +23 -12
- package/dist/tendril-mcp.js +12 -4
- package/dist/tendril.js +162 -38
- 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,
|
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);
|
|
@@ -3615,9 +3655,12 @@ function weightFromSubfamily(subfamily) {
|
|
|
3615
3655
|
return void 0;
|
|
3616
3656
|
}
|
|
3617
3657
|
function systemFaceRefusal(family) {
|
|
3618
|
-
if (/^\.?SF([ -]|$)/i.test(family) || /^San Francisco/i.test(family) || /^\.?Apple/i.test(family) || /^\.?New York
|
|
3658
|
+
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
3659
|
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
3660
|
}
|
|
3661
|
+
if (/^\./.test(family)) {
|
|
3662
|
+
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).";
|
|
3663
|
+
}
|
|
3621
3664
|
if (/^Segoe UI Variable/i.test(family)) {
|
|
3622
3665
|
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
3666
|
}
|
|
@@ -3943,7 +3986,7 @@ function verifiedFontFamilies(cacheDir = DEFAULT_FONT_CACHE) {
|
|
|
3943
3986
|
return [...byFamily.entries()].map(([family, w]) => ({ family, weights: [...w].sort((a, b) => a - b) }));
|
|
3944
3987
|
}
|
|
3945
3988
|
function addSystemFamily(family, opts = {}) {
|
|
3946
|
-
const refusal = systemFaceRefusal(family);
|
|
3989
|
+
const refusal = systemFaceRefusal(family.trim());
|
|
3947
3990
|
if (refusal !== void 0) return { added: [], skipped: [], overwrote: [], refusal };
|
|
3948
3991
|
const plainStyle = (sub) => weightFromSubfamily(sub) !== void 0 || /^(regular|plain|)$/i.test(sub.trim());
|
|
3949
3992
|
const faces = [...systemFacesForFamily(family, opts.dirs)].sort((a, b) => {
|
|
@@ -3961,6 +4004,11 @@ function addSystemFamily(family, opts = {}) {
|
|
|
3961
4004
|
const taken = /* @__PURE__ */ new Set();
|
|
3962
4005
|
for (const face of faces) {
|
|
3963
4006
|
const skip = (reason) => skipped.push({ subfamily: face.subfamily, weight: face.weight, reason });
|
|
4007
|
+
const faceRefusal = systemFaceRefusal(face.family.trim());
|
|
4008
|
+
if (faceRefusal !== void 0) {
|
|
4009
|
+
skip(`refusal-class family \u2014 ${faceRefusal}`);
|
|
4010
|
+
continue;
|
|
4011
|
+
}
|
|
3964
4012
|
if (opts.weights !== void 0 && !opts.weights.includes(face.weight)) continue;
|
|
3965
4013
|
if (face.italic) {
|
|
3966
4014
|
skip("italic \u2014 the cache cannot represent italic faces end to end yet; the upright face serves the family");
|
|
@@ -8129,10 +8177,11 @@ function nextPayload(setDir) {
|
|
|
8129
8177
|
const instruction = nextInstruction(setDir);
|
|
8130
8178
|
const status = sessionStatus(setDir);
|
|
8131
8179
|
const progress = { recordedReps: status.reps.filter((x) => x.missing.length === 0).length, totalReps: status.reps.length };
|
|
8132
|
-
if (instruction === null && !
|
|
8180
|
+
if (instruction === null && !status.setTokenMapRecorded) {
|
|
8133
8181
|
const manifest = loadManifest(setDir);
|
|
8134
8182
|
const frameNode = Object.keys(manifest.sourceFrames ?? {})[0] ?? manifest.reps[0]?.nodeId ?? "";
|
|
8135
|
-
|
|
8183
|
+
const invalidNote = status.setTokenMapInvalid !== void 0 ? ` (the existing set-level file is UNUSABLE \u2014 ${status.setTokenMapInvalid} \u2014 re-record it)` : "";
|
|
8184
|
+
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
8185
|
}
|
|
8137
8186
|
return instruction === null ? { complete: true, progress } : {
|
|
8138
8187
|
...instruction,
|
|
@@ -8152,7 +8201,7 @@ function runRecordNext(opts) {
|
|
|
8152
8201
|
const payload = nextPayload(opts.setDir);
|
|
8153
8202
|
emitData(opts, payload, () => {
|
|
8154
8203
|
if (payload["complete"] === true) {
|
|
8155
|
-
process.stdout.write("set complete \u2014 every planned rep has its required envelopes\n");
|
|
8204
|
+
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
8205
|
return;
|
|
8157
8206
|
}
|
|
8158
8207
|
const progress = payload["progress"];
|
|
@@ -8446,10 +8495,21 @@ function runRecordStatus(opts) {
|
|
|
8446
8495
|
for (const r of status.reps) {
|
|
8447
8496
|
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
8497
|
`);
|
|
8449
|
-
|
|
8450
|
-
|
|
8451
|
-
${status.reps.filter((r) => r.missing.length > 0).length} rep(s) pending
|
|
8498
|
+
for (const inv of r.invalid ?? []) {
|
|
8499
|
+
process.stdout.write(` ${r.slug}/${inv.tool}: recorded file is UNUSABLE \u2014 ${inv.reason} (re-record this call)
|
|
8452
8500
|
`);
|
|
8501
|
+
}
|
|
8502
|
+
}
|
|
8503
|
+
const pending = status.reps.filter((r) => r.missing.length > 0).length;
|
|
8504
|
+
process.stdout.write(
|
|
8505
|
+
status.complete ? "\nset complete\n" : pending > 0 ? `
|
|
8506
|
+
${pending} rep(s) pending
|
|
8507
|
+
` : status.setTokenMapInvalid !== void 0 ? `
|
|
8508
|
+
all reps recorded, but the SET-LEVEL token map file is UNUSABLE (${status.setTokenMapInvalid}) \u2014 re-record it via \`record next\`
|
|
8509
|
+
` : `
|
|
8510
|
+
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)
|
|
8511
|
+
`
|
|
8512
|
+
);
|
|
8453
8513
|
});
|
|
8454
8514
|
}
|
|
8455
8515
|
function narrowedRoles(derived, override) {
|
|
@@ -9260,6 +9320,9 @@ function authorBehaviors(api, extras = {}) {
|
|
|
9260
9320
|
function envelopeText(file) {
|
|
9261
9321
|
return envelopeFirstTextPart(JSON.parse(readFileSync19(file, "utf8")));
|
|
9262
9322
|
}
|
|
9323
|
+
function metadataText(file) {
|
|
9324
|
+
return envelopeTextContent(JSON.parse(readFileSync19(file, "utf8")));
|
|
9325
|
+
}
|
|
9263
9326
|
function dismissEvidence(setDir, repSlugs) {
|
|
9264
9327
|
for (const slug of repSlugs) {
|
|
9265
9328
|
const f = path29.join(setDir, slug, "get_design_context.json");
|
|
@@ -9381,7 +9444,7 @@ function recordedTextSlots(setDir, repSlugs) {
|
|
|
9381
9444
|
for (const slug of repSlugs) {
|
|
9382
9445
|
const metaFile = path29.join(setDir, slug, "get_metadata.json");
|
|
9383
9446
|
if (!existsSync23(metaFile)) continue;
|
|
9384
|
-
const name = symbolName(
|
|
9447
|
+
const name = symbolName(metadataText(metaFile));
|
|
9385
9448
|
if (name === void 0) continue;
|
|
9386
9449
|
const values = /* @__PURE__ */ new Set();
|
|
9387
9450
|
for (const part of name.split(",")) {
|
|
@@ -9484,7 +9547,7 @@ function authorTaskFromSet(setDir, opts = {}) {
|
|
|
9484
9547
|
missing.push(rep.slug);
|
|
9485
9548
|
continue;
|
|
9486
9549
|
}
|
|
9487
|
-
const name = symbolName(
|
|
9550
|
+
const name = symbolName(metadataText(metaFile));
|
|
9488
9551
|
if (name === void 0) {
|
|
9489
9552
|
missing.push(rep.slug);
|
|
9490
9553
|
continue;
|
|
@@ -9495,7 +9558,7 @@ function authorTaskFromSet(setDir, opts = {}) {
|
|
|
9495
9558
|
throw new Error(`recording set ${setDir} is missing metadata for ${missing.length} rep(s): ${missing.slice(0, 5).join(", ")}${missing.length > 5 ? ", \u2026" : ""}`);
|
|
9496
9559
|
}
|
|
9497
9560
|
const setMeta = path29.join(setDir, "get_metadata.json");
|
|
9498
|
-
const latticeNames = manifest.latticeNames ?? (existsSync23(setMeta) ? [...
|
|
9561
|
+
const latticeNames = manifest.latticeNames ?? (existsSync23(setMeta) ? [...metadataText(setMeta).matchAll(/name="([^"]*)"/g)].map((m) => decodeXmlEntities(m[1])) : void 0);
|
|
9499
9562
|
const defaults = { ...manifest.defaults ?? {}, ...opts.defaults ?? {} };
|
|
9500
9563
|
const recordedFonts = recordedFontFamilies(setDir);
|
|
9501
9564
|
const textSlots = recordedTextSlots(setDir, manifest.reps.map((r) => r.slug));
|
|
@@ -9951,7 +10014,7 @@ function fontsCssFor(faces, bundleDir, cacheDir = DEFAULT_FONT_CACHE, substitute
|
|
|
9951
10014
|
const family = face.family.replace(/['\\]/g, "").replace(/\*\//g, "");
|
|
9952
10015
|
const decl = `@font-face { font-family: '${family}'; font-weight: ${face.weight}; font-style: normal; src: url(${target}) format('${format}'); }`;
|
|
9953
10016
|
const license = normalizeFontLicense(face.license);
|
|
9954
|
-
const terms = face.source.startsWith("system:") ? void 0 : REDISTRIBUTABLE.get(license);
|
|
10017
|
+
const terms = face.source.startsWith("system:") || face.source.startsWith("local:") ? void 0 : REDISTRIBUTABLE.get(license);
|
|
9955
10018
|
if (terms === void 0) {
|
|
9956
10019
|
if (face.source.startsWith("system:")) {
|
|
9957
10020
|
lines.push(
|
|
@@ -10677,11 +10740,17 @@ import { existsSync as existsSync26, readFileSync as readFileSync22 } from "node
|
|
|
10677
10740
|
import path33 from "node:path";
|
|
10678
10741
|
async function runFontsResolve(opts) {
|
|
10679
10742
|
const result = await resolveFonts(opts.family, opts.weights, opts.cacheDir);
|
|
10680
|
-
|
|
10743
|
+
const queryRefusal = systemFaceRefusal(opts.family.trim());
|
|
10744
|
+
emitData(opts, { ...result, ...queryRefusal !== void 0 ? { queryRefusal } : {} }, () => {
|
|
10681
10745
|
for (const f of result.resolved) process.stdout.write(`resolved ${f.family} ${f.weight} (${f.sha256.slice(0, 12)}\u2026)
|
|
10682
10746
|
`);
|
|
10683
10747
|
for (const f of result.failures) process.stdout.write(`FAILED ${f.family} ${f.weight}: ${f.reason}
|
|
10684
10748
|
`);
|
|
10749
|
+
if (queryRefusal !== void 0 && result.failures.length > 0) {
|
|
10750
|
+
process.stdout.write(` "${opts.family}" is a refusal-class family: ${queryRefusal}
|
|
10751
|
+
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.
|
|
10752
|
+
`);
|
|
10753
|
+
}
|
|
10685
10754
|
});
|
|
10686
10755
|
if (result.failures.length > 0) {
|
|
10687
10756
|
warn(opts, `${result.failures.length} face(s) unresolved \u2014 verification will refuse to score under substitution`);
|
|
@@ -10739,7 +10808,8 @@ async function runFontsResolveSet(opts) {
|
|
|
10739
10808
|
unlicensed.push(...result.failures);
|
|
10740
10809
|
}
|
|
10741
10810
|
}
|
|
10742
|
-
|
|
10811
|
+
const refusals = [...new Set(failures.map((f) => f.family))].map((family) => ({ family, reason: systemFaceRefusal(family.trim()) })).filter((r) => r.reason !== void 0);
|
|
10812
|
+
emitData(opts, { set: setDir, needs, cached: cached2, resolved, relicensed, unlicensed, failures, ...refusals.length > 0 ? { refusals } : {} }, () => {
|
|
10743
10813
|
process.stdout.write(`set declares: ${needs.map((n) => `${n.family} (${n.weights.join(", ")})`).join(" \xB7 ")}
|
|
10744
10814
|
`);
|
|
10745
10815
|
for (const f of cached2) process.stdout.write(`cached ${f.family} ${f.weight}
|
|
@@ -10753,8 +10823,11 @@ async function runFontsResolveSet(opts) {
|
|
|
10753
10823
|
for (const f of failures) process.stdout.write(`FAILED ${f.family} ${f.weight}: ${f.reason}
|
|
10754
10824
|
`);
|
|
10755
10825
|
for (const fam of [...new Set(failures.map((f) => f.family))]) {
|
|
10826
|
+
const refusal = systemFaceRefusal(fam);
|
|
10756
10827
|
process.stdout.write(
|
|
10757
|
-
|
|
10828
|
+
refusal !== void 0 ? ` "${fam}" is a refusal-class family: ${refusal}
|
|
10829
|
+
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.
|
|
10830
|
+
` : ` 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
10831
|
`
|
|
10759
10832
|
);
|
|
10760
10833
|
}
|
|
@@ -10855,12 +10928,20 @@ function runFontsAdd(opts) {
|
|
|
10855
10928
|
});
|
|
10856
10929
|
}
|
|
10857
10930
|
function runFontsDiscover(opts) {
|
|
10858
|
-
const faces = opts.family !== void 0 ? systemFacesForFamily(opts.family) : discoverSystemFaces();
|
|
10931
|
+
const faces = opts.family !== void 0 ? systemFacesForFamily(opts.family, opts.dirs) : discoverSystemFaces(opts.dirs);
|
|
10859
10932
|
const rows = faces.map((f) => ({ ...f, ...systemFaceRefusal(f.family) !== void 0 ? { refused: systemFaceRefusal(f.family) } : {} }));
|
|
10860
|
-
|
|
10933
|
+
const queryRefusal = opts.family !== void 0 ? systemFaceRefusal(opts.family) : void 0;
|
|
10934
|
+
emitData(opts, { faces: rows, dirs: opts.dirs ?? systemFontDirs(), ...queryRefusal !== void 0 ? { queryRefusal } : {} }, () => {
|
|
10861
10935
|
if (rows.length === 0) {
|
|
10862
|
-
process.stdout.write(`no matching system faces found (scanned: ${systemFontDirs().join(", ")})
|
|
10936
|
+
process.stdout.write(`no matching system faces found (scanned: ${(opts.dirs ?? systemFontDirs()).join(", ")})
|
|
10863
10937
|
`);
|
|
10938
|
+
if (queryRefusal !== void 0) {
|
|
10939
|
+
process.stdout.write(
|
|
10940
|
+
`note: "${opts.family}" is a refusal-class family regardless \u2014 ${queryRefusal}
|
|
10941
|
+
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.
|
|
10942
|
+
`
|
|
10943
|
+
);
|
|
10944
|
+
}
|
|
10864
10945
|
return;
|
|
10865
10946
|
}
|
|
10866
10947
|
for (const f of rows) {
|
|
@@ -11472,6 +11553,9 @@ ${certified}/${statuses.length} certified \xB7 ${report.coverage.pass}/${statuse
|
|
|
11472
11553
|
`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
11554
|
`
|
|
11474
11555
|
);
|
|
11556
|
+
} else if (typeof cov.latticeConfigs === "number") {
|
|
11557
|
+
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
|
|
11558
|
+
`);
|
|
11475
11559
|
} else if (cov.latticeConfigs === null) {
|
|
11476
11560
|
process.stdout.write(`COVERAGE denominator unknown (set predates lattice tracking) \u2014 scored configs are verified; completeness is not established
|
|
11477
11561
|
`);
|
|
@@ -11696,7 +11780,10 @@ ${segments}`;
|
|
|
11696
11780
|
);
|
|
11697
11781
|
}
|
|
11698
11782
|
function appendScoreHistory(candidateDir, entry) {
|
|
11699
|
-
|
|
11783
|
+
const file = path35.join(candidateDir, "score-history.jsonl");
|
|
11784
|
+
const starts = existsSync28(file) ? readFileSync24(file, "utf8").split("\n").filter((l) => l.includes('"event":"round-start"')).length : 0;
|
|
11785
|
+
const round = entry.event === "round-start" ? starts + 1 : Math.max(1, starts);
|
|
11786
|
+
appendFileSync(file, `${JSON.stringify({ at: (/* @__PURE__ */ new Date()).toISOString(), round, ...entry })}
|
|
11700
11787
|
`);
|
|
11701
11788
|
}
|
|
11702
11789
|
async function runEngineScore(opts) {
|
|
@@ -11784,7 +11871,10 @@ ${absentFindings.join("\n")}` : "";
|
|
|
11784
11871
|
|
|
11785
11872
|
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
11873
|
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
|
|
11874
|
+
const stampNotice = `
|
|
11875
|
+
|
|
11876
|
+
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.`;
|
|
11877
|
+
const feedback = buildFeedback(scores, behaviors, bar, "files") + certificationFeedback + stampNotice + qualityFeedback;
|
|
11788
11878
|
const emitted = emitBundleV1({
|
|
11789
11879
|
bundleDir: candidateDir,
|
|
11790
11880
|
task,
|
|
@@ -11849,7 +11939,7 @@ METRIC DEADBAND (read before iterating on near-misses): the scored similarity/in
|
|
|
11849
11939
|
parityCoverage,
|
|
11850
11940
|
evidenceDir,
|
|
11851
11941
|
bundleManifest: emitted.written[0],
|
|
11852
|
-
note: "
|
|
11942
|
+
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
11943
|
allPass,
|
|
11854
11944
|
environment: { ...environmentStamp(taskFontFamilies(task.set)), ruler: cliVersion() },
|
|
11855
11945
|
...lcdTextEnabled() ? { environmentOverrides: ["enable-lcd-text"] } : {},
|
|
@@ -11977,7 +12067,7 @@ function runCodeConnect(opts) {
|
|
|
11977
12067
|
const meta = path36.join(setDir, r.slug, "get_metadata.json");
|
|
11978
12068
|
if (!existsSync29(meta)) return void 0;
|
|
11979
12069
|
try {
|
|
11980
|
-
return /name="([^"]*)"/.exec(
|
|
12070
|
+
return /name="([^"]*)"/.exec(envelopeTextContent(JSON.parse(readFileSync25(meta, "utf8"))))?.[1];
|
|
11981
12071
|
} catch {
|
|
11982
12072
|
return void 0;
|
|
11983
12073
|
}
|
|
@@ -12164,11 +12254,19 @@ var init_server = __esm({
|
|
|
12164
12254
|
},
|
|
12165
12255
|
{
|
|
12166
12256
|
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>__*
|
|
12257
|
+
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
12258
|
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")
|
|
12259
|
+
write: z12.boolean().optional().describe("true = install into .claude/settings.local.json (the point of this tool); false/absent = just return the entries"),
|
|
12260
|
+
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)."),
|
|
12261
|
+
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
12262
|
}),
|
|
12171
|
-
argv: (i) => [
|
|
12263
|
+
argv: (i) => [
|
|
12264
|
+
"permissions",
|
|
12265
|
+
"--claude",
|
|
12266
|
+
...i["write"] === true ? ["--write"] : [],
|
|
12267
|
+
...typeof i["figmaPrefix"] === "string" ? ["--figma-prefix", i["figmaPrefix"]] : [],
|
|
12268
|
+
...typeof i["tendrilPrefix"] === "string" ? ["--tendril-prefix", i["tendrilPrefix"]] : []
|
|
12269
|
+
]
|
|
12172
12270
|
},
|
|
12173
12271
|
{
|
|
12174
12272
|
name: "tendril_doctor",
|
|
@@ -12293,7 +12391,7 @@ var init_server = __esm({
|
|
|
12293
12391
|
{
|
|
12294
12392
|
name: "tendril_record_status",
|
|
12295
12393
|
annotations: { readOnlyHint: true },
|
|
12296
|
-
description: "Recording-set completeness: per-rep recorded/missing tools.",
|
|
12394
|
+
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
12395
|
schema: z12.object({ setDir: str("recording set directory") }),
|
|
12298
12396
|
argv: (i) => ["record", "status", "--set", i["setDir"]]
|
|
12299
12397
|
},
|
|
@@ -12451,6 +12549,13 @@ function mergeAllowlist(file, entries, denyEntries = []) {
|
|
|
12451
12549
|
}
|
|
12452
12550
|
async function buildPermissions(options) {
|
|
12453
12551
|
const LEGAL_TOOL_NAME = /^[A-Za-z0-9_-]+$/;
|
|
12552
|
+
for (const [flag, value] of [["--figma-prefix", options.figmaPrefix], ["--tendril-prefix", options.tendrilPrefix]]) {
|
|
12553
|
+
if (value !== void 0 && !LEGAL_MCP_PREFIX.test(value)) {
|
|
12554
|
+
throw new Error(`${flag} must look like mcp__<server-name> (letters, digits, _ and - only) \u2014 got ${JSON.stringify(value)}`);
|
|
12555
|
+
}
|
|
12556
|
+
}
|
|
12557
|
+
const figmaPrefix = options.figmaPrefix ?? FIGMA_PLUGIN_PREFIX;
|
|
12558
|
+
const tendrilPrefix = options.tendrilPrefix ?? TENDRIL_PLUGIN_PREFIX;
|
|
12454
12559
|
const pipeline = new Set(FIGMA_TOOL_FALLBACK);
|
|
12455
12560
|
let figmaTools = FIGMA_TOOL_FALLBACK;
|
|
12456
12561
|
try {
|
|
@@ -12462,10 +12567,10 @@ async function buildPermissions(options) {
|
|
|
12462
12567
|
}
|
|
12463
12568
|
return {
|
|
12464
12569
|
host: "claude",
|
|
12465
|
-
serverEntries: [
|
|
12570
|
+
serverEntries: [tendrilPrefix, figmaPrefix],
|
|
12466
12571
|
toolEntries: [
|
|
12467
|
-
...TOOLS.map((t) => t.name).filter((n) => LEGAL_TOOL_NAME.test(n)).map((n) => `${
|
|
12468
|
-
...figmaTools.map((name) => `${
|
|
12572
|
+
...TOOLS.map((t) => t.name).filter((n) => LEGAL_TOOL_NAME.test(n)).map((n) => `${tendrilPrefix}__${n}`),
|
|
12573
|
+
...figmaTools.map((name) => `${figmaPrefix}__${name}`)
|
|
12469
12574
|
],
|
|
12470
12575
|
shellEntries: [...SHELL_READONLY_ENTRIES, ...SHELL_PIPELINE_ENTRIES],
|
|
12471
12576
|
projectFileEntries: PROJECT_FILE_ENTRIES,
|
|
@@ -12473,7 +12578,7 @@ async function buildPermissions(options) {
|
|
|
12473
12578
|
manualCautionEntries: MANUAL_CAUTION_ENTRIES,
|
|
12474
12579
|
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
12580
|
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: `
|
|
12581
|
+
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
12582
|
};
|
|
12478
12583
|
}
|
|
12479
12584
|
function writeSelection(result, user) {
|
|
@@ -12484,7 +12589,21 @@ async function runPermissions(flags) {
|
|
|
12484
12589
|
printDescription(PERMISSIONS_DESCRIPTION);
|
|
12485
12590
|
return;
|
|
12486
12591
|
}
|
|
12487
|
-
|
|
12592
|
+
let result;
|
|
12593
|
+
try {
|
|
12594
|
+
result = await buildPermissions({
|
|
12595
|
+
...flags.mcpUrl ? { mcpUrl: flags.mcpUrl } : {},
|
|
12596
|
+
...flags.figmaPrefix !== void 0 ? { figmaPrefix: flags.figmaPrefix } : {},
|
|
12597
|
+
...flags.tendrilPrefix !== void 0 ? { tendrilPrefix: flags.tendrilPrefix } : {}
|
|
12598
|
+
});
|
|
12599
|
+
} catch (err) {
|
|
12600
|
+
fail(flags, ExitCode.InputValidation, {
|
|
12601
|
+
error: err instanceof Error ? err.message : String(err),
|
|
12602
|
+
code: "bad-prefix",
|
|
12603
|
+
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)."
|
|
12604
|
+
});
|
|
12605
|
+
return;
|
|
12606
|
+
}
|
|
12488
12607
|
if (flags.write) {
|
|
12489
12608
|
const base = process.env["INIT_CWD"] ?? process.cwd();
|
|
12490
12609
|
const file = flags.user ? path38.join(os8.homedir(), ".claude", "settings.json") : path38.join(base, ".claude", "settings.local.json");
|
|
@@ -12559,7 +12678,7 @@ ${result.directConfigNote}
|
|
|
12559
12678
|
);
|
|
12560
12679
|
});
|
|
12561
12680
|
}
|
|
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;
|
|
12681
|
+
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
12682
|
var init_permissions = __esm({
|
|
12564
12683
|
"packages/cli/src/commands/permissions.ts"() {
|
|
12565
12684
|
"use strict";
|
|
@@ -12610,6 +12729,8 @@ var init_permissions = __esm({
|
|
|
12610
12729
|
{ 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
12730
|
{ 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
12731
|
{ flag: "--mcp-url <url>", description: "Figma MCP endpoint to list live tool names from", default: DEFAULT_MCP_URL },
|
|
12732
|
+
{ 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" },
|
|
12733
|
+
{ flag: "--tendril-prefix <prefix>", description: "The session's real tendril MCP entry prefix (e.g. mcp__tendril for claude mcp add installs)" },
|
|
12613
12734
|
{ flag: "--json", description: "Machine-readable output" }
|
|
12614
12735
|
],
|
|
12615
12736
|
output: {
|
|
@@ -12624,6 +12745,7 @@ var init_permissions = __esm({
|
|
|
12624
12745
|
};
|
|
12625
12746
|
TENDRIL_PLUGIN_PREFIX = "mcp__plugin_tendril_tendril";
|
|
12626
12747
|
FIGMA_PLUGIN_PREFIX = "mcp__plugin_figma_figma";
|
|
12748
|
+
LEGAL_MCP_PREFIX = /^mcp__[A-Za-z0-9_-]+$/;
|
|
12627
12749
|
}
|
|
12628
12750
|
});
|
|
12629
12751
|
|
|
@@ -14114,7 +14236,7 @@ function buildProgram() {
|
|
|
14114
14236
|
...local["out"] !== void 0 ? { out: local["out"] } : {}
|
|
14115
14237
|
});
|
|
14116
14238
|
});
|
|
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
|
|
14239
|
+
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
14240
|
const flags = globalFlags(cmd.parent);
|
|
14119
14241
|
const local = cmd.opts();
|
|
14120
14242
|
const { runPermissions: runPermissions2 } = await Promise.resolve().then(() => (init_permissions(), permissions_exports));
|
|
@@ -14122,7 +14244,9 @@ function buildProgram() {
|
|
|
14122
14244
|
...flags,
|
|
14123
14245
|
...local["write"] !== void 0 ? { write: local["write"] } : {},
|
|
14124
14246
|
...local["user"] !== void 0 ? { user: local["user"] } : {},
|
|
14125
|
-
...local["mcpUrl"] !== void 0 ? { mcpUrl: local["mcpUrl"] } : {}
|
|
14247
|
+
...local["mcpUrl"] !== void 0 ? { mcpUrl: local["mcpUrl"] } : {},
|
|
14248
|
+
...local["figmaPrefix"] !== void 0 ? { figmaPrefix: local["figmaPrefix"] } : {},
|
|
14249
|
+
...local["tendrilPrefix"] !== void 0 ? { tendrilPrefix: local["tendrilPrefix"] } : {}
|
|
14126
14250
|
});
|
|
14127
14251
|
});
|
|
14128
14252
|
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) => {
|