pi-mono-all 1.0.0

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.
Files changed (161) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/LICENCE.md +7 -0
  3. package/node_modules/pi-common/package.json +22 -0
  4. package/node_modules/pi-common/src/auth-config.ts +290 -0
  5. package/node_modules/pi-common/src/auth.ts +63 -0
  6. package/node_modules/pi-common/src/cache.ts +60 -0
  7. package/node_modules/pi-common/src/errors.ts +47 -0
  8. package/node_modules/pi-common/src/http-client.ts +118 -0
  9. package/node_modules/pi-common/src/index.ts +7 -0
  10. package/node_modules/pi-common/src/rate-limiter.ts +32 -0
  11. package/node_modules/pi-common/src/tool-result.ts +27 -0
  12. package/node_modules/pi-mono-ask-user-question/CHANGELOG.md +185 -0
  13. package/node_modules/pi-mono-ask-user-question/README.md +226 -0
  14. package/node_modules/pi-mono-ask-user-question/index.ts +923 -0
  15. package/node_modules/pi-mono-ask-user-question/package.json +29 -0
  16. package/node_modules/pi-mono-auto-fix/CHANGELOG.md +59 -0
  17. package/node_modules/pi-mono-auto-fix/README.md +77 -0
  18. package/node_modules/pi-mono-auto-fix/index.ts +488 -0
  19. package/node_modules/pi-mono-auto-fix/package.json +23 -0
  20. package/node_modules/pi-mono-btw/CHANGELOG.md +180 -0
  21. package/node_modules/pi-mono-btw/README.md +24 -0
  22. package/node_modules/pi-mono-btw/index.ts +499 -0
  23. package/node_modules/pi-mono-btw/package.json +29 -0
  24. package/node_modules/pi-mono-clear/CHANGELOG.md +180 -0
  25. package/node_modules/pi-mono-clear/README.md +40 -0
  26. package/node_modules/pi-mono-clear/index.ts +45 -0
  27. package/node_modules/pi-mono-clear/package.json +29 -0
  28. package/node_modules/pi-mono-context/CHANGELOG.md +12 -0
  29. package/node_modules/pi-mono-context/README.md +74 -0
  30. package/node_modules/pi-mono-context/index.ts +641 -0
  31. package/node_modules/pi-mono-context/package.json +29 -0
  32. package/node_modules/pi-mono-context-guard/CHANGELOG.md +195 -0
  33. package/node_modules/pi-mono-context-guard/README.md +81 -0
  34. package/node_modules/pi-mono-context-guard/index.ts +212 -0
  35. package/node_modules/pi-mono-context-guard/package.json +23 -0
  36. package/node_modules/pi-mono-figma/CHANGELOG.md +59 -0
  37. package/node_modules/pi-mono-figma/README.md +236 -0
  38. package/node_modules/pi-mono-figma/__tests__/code-connect.test.ts +32 -0
  39. package/node_modules/pi-mono-figma/__tests__/figma-assets.test.ts +38 -0
  40. package/node_modules/pi-mono-figma/__tests__/figma-component-hints.test.ts +23 -0
  41. package/node_modules/pi-mono-figma/__tests__/figma-implementation-layout.test.ts +47 -0
  42. package/node_modules/pi-mono-figma/__tests__/figma-search.test.ts +51 -0
  43. package/node_modules/pi-mono-figma/__tests__/figma-summarizer.test.ts +65 -0
  44. package/node_modules/pi-mono-figma/__tests__/fixtures/complex-auto-layout.json +115 -0
  45. package/node_modules/pi-mono-figma/__tests__/fixtures/component-instance.json +50 -0
  46. package/node_modules/pi-mono-figma/__tests__/fixtures/hidden-and-vectors.json +28 -0
  47. package/node_modules/pi-mono-figma/__tests__/fixtures/variables-and-styles.json +40 -0
  48. package/node_modules/pi-mono-figma/docs/live-selection-bridge.md +16 -0
  49. package/node_modules/pi-mono-figma/index.ts +6 -0
  50. package/node_modules/pi-mono-figma/package.json +33 -0
  51. package/node_modules/pi-mono-figma/skills/figma/SKILL.md +143 -0
  52. package/node_modules/pi-mono-figma/src/code-connect.ts +110 -0
  53. package/node_modules/pi-mono-figma/src/figma-assets.ts +146 -0
  54. package/node_modules/pi-mono-figma/src/figma-cache.ts +6 -0
  55. package/node_modules/pi-mono-figma/src/figma-client.ts +471 -0
  56. package/node_modules/pi-mono-figma/src/figma-component-hints.ts +87 -0
  57. package/node_modules/pi-mono-figma/src/figma-implementation.ts +264 -0
  58. package/node_modules/pi-mono-figma/src/figma-schemas.ts +139 -0
  59. package/node_modules/pi-mono-figma/src/figma-search.ts +195 -0
  60. package/node_modules/pi-mono-figma/src/figma-summarizer.ts +673 -0
  61. package/node_modules/pi-mono-figma/src/figma-tokens.ts +57 -0
  62. package/node_modules/pi-mono-figma/src/figma-tools.ts +352 -0
  63. package/node_modules/pi-mono-linear/CHANGELOG.md +44 -0
  64. package/node_modules/pi-mono-linear/README.md +159 -0
  65. package/node_modules/pi-mono-linear/index.ts +6 -0
  66. package/node_modules/pi-mono-linear/package.json +30 -0
  67. package/node_modules/pi-mono-linear/skills/linear/SKILL.md +107 -0
  68. package/node_modules/pi-mono-linear/src/linear-client.ts +339 -0
  69. package/node_modules/pi-mono-linear/src/linear-queries.ts +101 -0
  70. package/node_modules/pi-mono-linear/src/linear-schemas.ts +90 -0
  71. package/node_modules/pi-mono-linear/src/linear-tools.ts +362 -0
  72. package/node_modules/pi-mono-loop/CHANGELOG.md +163 -0
  73. package/node_modules/pi-mono-loop/README.md +54 -0
  74. package/node_modules/pi-mono-loop/index.ts +291 -0
  75. package/node_modules/pi-mono-loop/package.json +26 -0
  76. package/node_modules/pi-mono-multi-edit/CHANGELOG.md +232 -0
  77. package/node_modules/pi-mono-multi-edit/README.md +244 -0
  78. package/node_modules/pi-mono-multi-edit/__tests__/classic.test.ts +277 -0
  79. package/node_modules/pi-mono-multi-edit/__tests__/diff.test.ts +77 -0
  80. package/node_modules/pi-mono-multi-edit/__tests__/patch.test.ts +287 -0
  81. package/node_modules/pi-mono-multi-edit/benchmark-edits.ts +966 -0
  82. package/node_modules/pi-mono-multi-edit/classic.ts +435 -0
  83. package/node_modules/pi-mono-multi-edit/diff.ts +143 -0
  84. package/node_modules/pi-mono-multi-edit/index.ts +266 -0
  85. package/node_modules/pi-mono-multi-edit/package.json +37 -0
  86. package/node_modules/pi-mono-multi-edit/patch.ts +463 -0
  87. package/node_modules/pi-mono-multi-edit/types.ts +53 -0
  88. package/node_modules/pi-mono-multi-edit/workspace.ts +85 -0
  89. package/node_modules/pi-mono-review/CHANGELOG.md +190 -0
  90. package/node_modules/pi-mono-review/README.md +30 -0
  91. package/node_modules/pi-mono-review/common.ts +930 -0
  92. package/node_modules/pi-mono-review/index.ts +8 -0
  93. package/node_modules/pi-mono-review/package.json +29 -0
  94. package/node_modules/pi-mono-review/review-tui.ts +194 -0
  95. package/node_modules/pi-mono-review/review.ts +119 -0
  96. package/node_modules/pi-mono-review/reviewer.ts +339 -0
  97. package/node_modules/pi-mono-sentinel/CHANGELOG.md +158 -0
  98. package/node_modules/pi-mono-sentinel/README.md +87 -0
  99. package/node_modules/pi-mono-sentinel/__tests__/output-scanner.test.ts +109 -0
  100. package/node_modules/pi-mono-sentinel/__tests__/permissions.test.ts +202 -0
  101. package/node_modules/pi-mono-sentinel/__tests__/whitelist.test.ts +59 -0
  102. package/node_modules/pi-mono-sentinel/guards/execution-tracker.ts +281 -0
  103. package/node_modules/pi-mono-sentinel/guards/output-scanner.ts +232 -0
  104. package/node_modules/pi-mono-sentinel/guards/permission-gate.ts +170 -0
  105. package/node_modules/pi-mono-sentinel/index.ts +43 -0
  106. package/node_modules/pi-mono-sentinel/package.json +26 -0
  107. package/node_modules/pi-mono-sentinel/patterns/permissions.ts +175 -0
  108. package/node_modules/pi-mono-sentinel/patterns/read-targets.ts +104 -0
  109. package/node_modules/pi-mono-sentinel/patterns/secrets.ts +143 -0
  110. package/node_modules/pi-mono-sentinel/session.ts +95 -0
  111. package/node_modules/pi-mono-sentinel/specs/2026/04/sentinel/001-permission-gate.md +145 -0
  112. package/node_modules/pi-mono-sentinel/types.ts +39 -0
  113. package/node_modules/pi-mono-sentinel/whitelist.ts +86 -0
  114. package/node_modules/pi-mono-simplify/CHANGELOG.md +163 -0
  115. package/node_modules/pi-mono-simplify/README.md +56 -0
  116. package/node_modules/pi-mono-simplify/index.ts +78 -0
  117. package/node_modules/pi-mono-simplify/package.json +29 -0
  118. package/node_modules/pi-mono-status-line/CHANGELOG.md +180 -0
  119. package/node_modules/pi-mono-status-line/README.md +96 -0
  120. package/node_modules/pi-mono-status-line/basic.ts +89 -0
  121. package/node_modules/pi-mono-status-line/expert.ts +689 -0
  122. package/node_modules/pi-mono-status-line/index.ts +54 -0
  123. package/node_modules/pi-mono-status-line/package.json +29 -0
  124. package/node_modules/pi-mono-team-mode/CHANGELOG.md +278 -0
  125. package/node_modules/pi-mono-team-mode/README.md +246 -0
  126. package/node_modules/pi-mono-team-mode/__tests__/agent-manager-transient.test.ts +75 -0
  127. package/node_modules/pi-mono-team-mode/__tests__/delegation-manager.test.ts +118 -0
  128. package/node_modules/pi-mono-team-mode/__tests__/formatters.test.ts +104 -0
  129. package/node_modules/pi-mono-team-mode/__tests__/model-config.test.ts +272 -0
  130. package/node_modules/pi-mono-team-mode/__tests__/notification-box.test.ts +34 -0
  131. package/node_modules/pi-mono-team-mode/__tests__/parallel-utils.test.ts +32 -0
  132. package/node_modules/pi-mono-team-mode/__tests__/pi-stream-parser.test.ts +64 -0
  133. package/node_modules/pi-mono-team-mode/__tests__/prompts.test.ts +106 -0
  134. package/node_modules/pi-mono-team-mode/__tests__/store.test.ts +164 -0
  135. package/node_modules/pi-mono-team-mode/__tests__/tasks.test.ts +267 -0
  136. package/node_modules/pi-mono-team-mode/__tests__/teammate-specs.test.ts +114 -0
  137. package/node_modules/pi-mono-team-mode/__tests__/widget.test.ts +41 -0
  138. package/node_modules/pi-mono-team-mode/__tests__/worktree.test.ts +78 -0
  139. package/node_modules/pi-mono-team-mode/core/chain-utils.ts +90 -0
  140. package/node_modules/pi-mono-team-mode/core/fs-utils.ts +44 -0
  141. package/node_modules/pi-mono-team-mode/core/model-config.ts +432 -0
  142. package/node_modules/pi-mono-team-mode/core/parallel-utils.ts +48 -0
  143. package/node_modules/pi-mono-team-mode/core/prompts.ts +158 -0
  144. package/node_modules/pi-mono-team-mode/core/store.ts +156 -0
  145. package/node_modules/pi-mono-team-mode/core/tasks.ts +99 -0
  146. package/node_modules/pi-mono-team-mode/core/teammate-specs.ts +124 -0
  147. package/node_modules/pi-mono-team-mode/core/types.ts +160 -0
  148. package/node_modules/pi-mono-team-mode/index.ts +825 -0
  149. package/node_modules/pi-mono-team-mode/managers/agent-manager.ts +654 -0
  150. package/node_modules/pi-mono-team-mode/managers/delegation-manager.ts +211 -0
  151. package/node_modules/pi-mono-team-mode/managers/task-manager.ts +238 -0
  152. package/node_modules/pi-mono-team-mode/managers/team-manager.ts +59 -0
  153. package/node_modules/pi-mono-team-mode/package.json +33 -0
  154. package/node_modules/pi-mono-team-mode/runtime/pi-stream-parser.ts +194 -0
  155. package/node_modules/pi-mono-team-mode/runtime/subprocess.ts +183 -0
  156. package/node_modules/pi-mono-team-mode/runtime/transient-session.ts +196 -0
  157. package/node_modules/pi-mono-team-mode/runtime/worktree.ts +90 -0
  158. package/node_modules/pi-mono-team-mode/ui/formatters.ts +149 -0
  159. package/node_modules/pi-mono-team-mode/ui/notification-box.ts +55 -0
  160. package/node_modules/pi-mono-team-mode/ui/widget.ts +94 -0
  161. package/package.json +76 -0
@@ -0,0 +1,264 @@
1
+ import type { FigmaNodeSummary } from "./figma-summarizer.js";
2
+ import type { FigmaTokenMap } from "./figma-tokens.js";
3
+
4
+ export type FigmaFramework = "react" | "html" | "vue" | "angular" | "react-native";
5
+ export type FigmaStyling = "css" | "css-modules" | "styled-components" | "tailwind" | "inline";
6
+
7
+ export interface FigmaImplementationOptions {
8
+ framework?: FigmaFramework;
9
+ styling?: FigmaStyling;
10
+ resolveTokens?: boolean;
11
+ includeCodeSnippets?: boolean;
12
+ tokenMap?: FigmaTokenMap;
13
+ }
14
+
15
+ export function buildCssLayoutHints(node: unknown): Record<string, unknown> {
16
+ const record = asRecord(node);
17
+ const mode = stringValue(record.layoutMode);
18
+ const css: Record<string, unknown> = {};
19
+ const notes: string[] = [];
20
+ if (mode === "HORIZONTAL" || mode === "VERTICAL") {
21
+ css.display = "flex";
22
+ css.flexDirection = mode === "HORIZONTAL" ? "row" : "column";
23
+ if (numberValue(record.itemSpacing) !== undefined) css.gap = px(numberValue(record.itemSpacing));
24
+ const padding = paddingValue(record);
25
+ if (padding) css.padding = padding;
26
+ const justify = mapPrimary(record.primaryAxisAlignItems);
27
+ const align = mapCounter(record.counterAxisAlignItems);
28
+ if (justify) css.justifyContent = justify;
29
+ if (align) css.alignItems = align;
30
+ if (record.layoutWrap === "WRAP") css.flexWrap = "wrap";
31
+ } else {
32
+ css.position = "relative";
33
+ notes.push("No auto-layout mode detected; absolute positioning may be needed for overlapping/freeform children.");
34
+ }
35
+ const grids = compactLayoutGrids(record.layoutGrids);
36
+ if (grids.length) {
37
+ css.layoutGrids = grids;
38
+ notes.push("Figma layout grids can map to CSS grid columns or container guides.");
39
+ }
40
+ const sizing = compactObject({
41
+ horizontal: record.layoutSizingHorizontal,
42
+ vertical: record.layoutSizingVertical,
43
+ width: numberValue(asRecord(record.absoluteBoundingBox).width),
44
+ height: numberValue(asRecord(record.absoluteBoundingBox).height),
45
+ });
46
+ return compactObject({ nodeId: record.id, nodeName: record.name, figma: compactObject({ layoutMode: mode, layoutWrap: record.layoutWrap }), css, sizing, notes }) ?? {};
47
+ }
48
+
49
+ export function buildResponsiveHints(node: unknown): Array<Record<string, unknown>> {
50
+ const hints: Array<Record<string, unknown>> = [];
51
+ walk(node, (record, path) => {
52
+ const recommendations: string[] = [];
53
+ const constraints = asRecord(record.constraints);
54
+ if (constraints.horizontal === "SCALE" || constraints.horizontal === "LEFT_RIGHT") recommendations.push("Use width: 100% or stretch within the parent container.");
55
+ if (constraints.vertical === "SCALE" || constraints.vertical === "TOP_BOTTOM") recommendations.push("Let height stretch or derive it from content plus min-height.");
56
+ if (record.layoutGrow === 1) recommendations.push("Use flex: 1 for fill-container sizing.");
57
+ if (record.layoutAlign === "STRETCH" || record.layoutSizingHorizontal === "FILL") recommendations.push("Use align-self: stretch or width: 100%.");
58
+ if (record.layoutSizingHorizontal === "HUG" || record.layoutSizingVertical === "HUG") recommendations.push("Prefer intrinsic/content sizing; avoid hard-coded dimensions unless necessary.");
59
+ if (record.layoutSizingHorizontal === "FIXED") recommendations.push("Fixed width from Figma may need max-width or responsive breakpoints.");
60
+ if (record.layoutWrap === "WRAP") recommendations.push("Enable flex-wrap and test narrow container widths.");
61
+ if (numberValue(record.minWidth) !== undefined) recommendations.push(`Respect min-width: ${px(numberValue(record.minWidth))}.`);
62
+ if (numberValue(record.maxWidth) !== undefined) recommendations.push(`Respect max-width: ${px(numberValue(record.maxWidth))}.`);
63
+ if (recommendations.length) {
64
+ hints.push({ id: record.id, name: record.name, type: record.type, path, constraints: compactObject(constraints), recommendations: uniqueStrings(recommendations) });
65
+ }
66
+ });
67
+ return hints.slice(0, 60);
68
+ }
69
+
70
+ export function buildAccessibilityHints(summary: FigmaNodeSummary): Array<Record<string, unknown>> {
71
+ const hints: Array<Record<string, unknown>> = [];
72
+ for (const node of flatten(summary)) {
73
+ const text = (node.text ?? []).join(" ");
74
+ const haystack = `${node.name} ${text} ${node.roleGuess ?? ""}`.toLowerCase();
75
+ let role: string | undefined;
76
+ let tag: string | undefined;
77
+ const notes: string[] = [];
78
+ if (/button|submit|save|continue|cancel|next|back/.test(haystack)) {
79
+ role = "button";
80
+ tag = "button";
81
+ notes.push("Ensure it is keyboard-focusable and supports Enter/Space activation.");
82
+ } else if (/link|learn more|view details/.test(haystack)) {
83
+ role = "link";
84
+ tag = "a";
85
+ notes.push("Use an href when navigation is intended.");
86
+ } else if (/input|field|placeholder|select|dropdown/.test(haystack)) {
87
+ role = /select|dropdown/.test(haystack) ? "combobox" : "textbox";
88
+ tag = /select|dropdown/.test(haystack) ? "select" : "input";
89
+ notes.push("Associate a visible label or aria-label with the control.");
90
+ } else if (/checkbox|toggle/.test(haystack)) {
91
+ role = "checkbox";
92
+ tag = "input";
93
+ notes.push("Expose checked state and keyboard toggling.");
94
+ } else if (/radio/.test(haystack)) {
95
+ role = "radio";
96
+ tag = "input";
97
+ notes.push("Group radios with fieldset/legend or radiogroup labeling.");
98
+ } else if (/modal|dialog/.test(haystack)) {
99
+ role = "dialog";
100
+ tag = "dialog";
101
+ notes.push("Trap focus, restore focus on close, and label with aria-labelledby.");
102
+ } else if (/tab/.test(haystack)) {
103
+ role = "tab";
104
+ tag = "button";
105
+ notes.push("Implement roving tabindex and aria-selected.");
106
+ } else if (/icon|image|avatar|photo/.test(haystack)) {
107
+ role = "img";
108
+ tag = "img";
109
+ notes.push(/icon/.test(haystack) ? "Use aria-hidden for decorative icons or provide accessible name when meaningful." : "Provide descriptive alt text.");
110
+ } else if (/title|heading|header/.test(haystack)) {
111
+ role = "heading";
112
+ tag = "h2";
113
+ }
114
+ if (role || node.roleGuess) hints.push({ id: node.id, name: node.name, role: role ?? node.roleGuess, suggestedTag: tag, labelSource: text ? "visible text" : "node name", accessibleName: text || node.name, notes });
115
+ }
116
+ return hints.slice(0, 60);
117
+ }
118
+
119
+ export function buildDesignTokenHints(node: unknown, tokenMap?: FigmaTokenMap): Record<string, unknown> {
120
+ const resolved: Array<Record<string, unknown>> = [];
121
+ const unresolved: string[] = [];
122
+ walk(node, (record, path) => {
123
+ const styles = asRecord(record.styles);
124
+ for (const [property, id] of Object.entries(styles)) {
125
+ const styleId = String(id);
126
+ const name = tokenMap?.styles[styleId]?.name;
127
+ if (name) resolved.push({ path, property, id: styleId, kind: "style", name, type: tokenMap?.styles[styleId]?.type });
128
+ else unresolved.push(styleId);
129
+ }
130
+ for (const binding of collectVariableBindings(record.boundVariables)) {
131
+ const variable = tokenMap?.variables[binding.id];
132
+ if (variable) resolved.push({ path, property: binding.property, id: binding.id, kind: "variable", name: variable.name, collection: variable.collectionName });
133
+ else unresolved.push(binding.id);
134
+ }
135
+ });
136
+ return { resolved: resolved.slice(0, 80), unresolved: uniqueStrings(unresolved).slice(0, 80), warnings: tokenMap ? [] : ["Token resolution requires Figma styles/variables metadata; unresolved IDs are still reported when present."] };
137
+ }
138
+
139
+ export function buildFrameworkHints(summary: FigmaNodeSummary, options: FigmaImplementationOptions): Record<string, unknown> | undefined {
140
+ if (!options.framework && !options.styling && !options.includeCodeSnippets) return undefined;
141
+ const framework = options.framework ?? "react";
142
+ const styling = options.styling ?? "css";
143
+ const componentName = toPascalCase(summary.name || "FigmaComponent");
144
+ const className = toKebabCase(summary.name || "figma-component");
145
+ return compactObject({
146
+ framework,
147
+ styling,
148
+ componentName,
149
+ fileHints: framework === "react" ? [`${componentName}.tsx`, `${componentName}.styles.ts`] : [`${className}.html`, `${className}.css`],
150
+ notes: ["Starter snippets are heuristic scaffolds, not production-ready generated code.", "Map Figma text/layers to semantic elements before final implementation."],
151
+ snippet: options.includeCodeSnippets ? snippetFor(summary, framework, styling, componentName, className) : undefined,
152
+ }) as Record<string, unknown> | undefined;
153
+ }
154
+
155
+ function snippetFor(summary: FigmaNodeSummary, framework: FigmaFramework, styling: FigmaStyling, componentName: string, className: string): string {
156
+ const text = summary.visibleText?.slice(0, 3).join(" / ") || summary.name;
157
+ if (framework === "react-native") return `export function ${componentName}() {\n return <View><Text>${escapeSnippet(text)}</Text></View>;\n}`;
158
+ if (framework === "vue") return `<template>\n <section class=\"${className}\">${escapeSnippet(text)}</section>\n</template>`;
159
+ if (framework === "angular") return `<section class=\"${className}\">${escapeSnippet(text)}</section>`;
160
+ if (framework === "html") return `<section class=\"${className}\">${escapeSnippet(text)}</section>`;
161
+ if (styling === "styled-components") return `const ${componentName}Root = styled.section\`\n display: flex;\n\`;\n\nexport function ${componentName}() {\n return <${componentName}Root>${escapeSnippet(text)}</${componentName}Root>;\n}`;
162
+ return `export function ${componentName}() {\n return <section className=\"${className}\">${escapeSnippet(text)}</section>;\n}`;
163
+ }
164
+
165
+ function flatten(summary: FigmaNodeSummary): FigmaNodeSummary[] {
166
+ const out: FigmaNodeSummary[] = [];
167
+ function visit(node: FigmaNodeSummary): void {
168
+ out.push(node);
169
+ for (const child of node.children ?? []) visit(child);
170
+ }
171
+ visit(summary);
172
+ return out;
173
+ }
174
+
175
+ function walk(node: unknown, visit: (record: Record<string, unknown>, path: string) => void, path = ""): void {
176
+ const record = asRecord(node);
177
+ const name = String(record.name ?? "Unnamed node");
178
+ const nextPath = path ? `${path} > ${name}` : name;
179
+ visit(record, nextPath);
180
+ for (const child of Array.isArray(record.children) ? record.children : []) walk(child, visit, nextPath);
181
+ }
182
+
183
+ function collectVariableBindings(value: unknown): Array<{ property: string; id: string }> {
184
+ const out: Array<{ property: string; id: string }> = [];
185
+ function visit(raw: unknown, property: string): void {
186
+ if (Array.isArray(raw)) raw.forEach((item, index) => visit(item, `${property}[${index}]`));
187
+ else {
188
+ const record = asRecord(raw);
189
+ if (typeof record.id === "string") out.push({ property, id: record.id });
190
+ for (const [key, child] of Object.entries(record)) if (key !== "id" && key !== "type") visit(child, property ? `${property}.${key}` : key);
191
+ }
192
+ }
193
+ visit(value, "");
194
+ return out;
195
+ }
196
+
197
+ function compactLayoutGrids(value: unknown): Array<Record<string, unknown>> {
198
+ if (!Array.isArray(value)) return [];
199
+ return value.slice(0, 4).map((grid) => {
200
+ const record = asRecord(grid);
201
+ return compactObject({ pattern: record.pattern, count: record.count, gutterSize: record.gutterSize, sectionSize: record.sectionSize, alignment: record.alignment }) ?? {};
202
+ });
203
+ }
204
+
205
+ function paddingValue(record: Record<string, unknown>): string | undefined {
206
+ const top = numberValue(record.paddingTop);
207
+ const right = numberValue(record.paddingRight);
208
+ const bottom = numberValue(record.paddingBottom);
209
+ const left = numberValue(record.paddingLeft);
210
+ if ([top, right, bottom, left].every((value) => value === undefined)) return undefined;
211
+ return [top ?? 0, right ?? 0, bottom ?? 0, left ?? 0].map(px).join(" ");
212
+ }
213
+
214
+ function mapPrimary(value: unknown): string | undefined {
215
+ return ({ MIN: "flex-start", CENTER: "center", MAX: "flex-end", SPACE_BETWEEN: "space-between" } as Record<string, string>)[String(value)];
216
+ }
217
+
218
+ function mapCounter(value: unknown): string | undefined {
219
+ return ({ MIN: "flex-start", CENTER: "center", MAX: "flex-end", BASELINE: "baseline", STRETCH: "stretch" } as Record<string, string>)[String(value)];
220
+ }
221
+
222
+ function asRecord(value: unknown): Record<string, unknown> {
223
+ return value && typeof value === "object" && !Array.isArray(value) ? (value as Record<string, unknown>) : {};
224
+ }
225
+
226
+ function numberValue(value: unknown): number | undefined {
227
+ return typeof value === "number" && Number.isFinite(value) ? value : undefined;
228
+ }
229
+
230
+ function stringValue(value: unknown): string | undefined {
231
+ return typeof value === "string" ? value : undefined;
232
+ }
233
+
234
+ function compactObject(value: Record<string, unknown>): Record<string, unknown> | undefined {
235
+ const out: Record<string, unknown> = {};
236
+ for (const [key, raw] of Object.entries(value)) {
237
+ if (raw === undefined || raw === null) continue;
238
+ if (Array.isArray(raw) && raw.length === 0) continue;
239
+ if (typeof raw === "object" && !Array.isArray(raw) && Object.keys(raw).length === 0) continue;
240
+ out[key] = raw;
241
+ }
242
+ return Object.keys(out).length ? out : undefined;
243
+ }
244
+
245
+ function px(value: number | undefined): string {
246
+ return `${value ?? 0}px`;
247
+ }
248
+
249
+ function uniqueStrings(values: string[]): string[] {
250
+ return [...new Set(values)];
251
+ }
252
+
253
+ function toPascalCase(value: string): string {
254
+ const result = value.replace(/[^a-z0-9]+/gi, " ").trim().split(/\s+/).filter(Boolean).map((part) => part[0]?.toUpperCase() + part.slice(1)).join("");
255
+ return /^[A-Z]/.test(result) ? result : `Figma${result || "Component"}`;
256
+ }
257
+
258
+ function toKebabCase(value: string): string {
259
+ return value.replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/[^a-z0-9]+/gi, "-").replace(/^-|-$/g, "").toLowerCase() || "figma-component";
260
+ }
261
+
262
+ function escapeSnippet(value: string): string {
263
+ return value.replace(/[<>]/g, "");
264
+ }
@@ -0,0 +1,139 @@
1
+ import { Type } from "@sinclair/typebox";
2
+
3
+ export const MaxResponseCharsSchema = Type.Optional(
4
+ Type.Number({ description: "Maximum characters returned to the model before truncation", minimum: 1 }),
5
+ );
6
+
7
+ export const FileKeySchema = Type.String({ description: "Figma file key from a Figma URL" });
8
+ export const NodeIdSchema = Type.String({ description: "Figma node ID, either 1:2 API format or 1-2 URL format" });
9
+ export const NodeIdsSchema = Type.Array(NodeIdSchema, {
10
+ description: "One or more Figma node IDs. Batch related nodes in one call.",
11
+ minItems: 1,
12
+ });
13
+
14
+ export const FigmaGetFileParams = Type.Object({
15
+ fileKey: FileKeySchema,
16
+ depth: Type.Optional(Type.Number({ description: "Optional Figma file depth query parameter", minimum: 1 })),
17
+ maxResponseChars: MaxResponseCharsSchema,
18
+ });
19
+
20
+ export const FigmaGetDesignContextParams = Type.Object({
21
+ fileKey: FileKeySchema,
22
+ nodeId: Type.Optional(NodeIdSchema),
23
+ maxResponseChars: MaxResponseCharsSchema,
24
+ });
25
+
26
+ export const FigmaGetNodesParams = Type.Object({
27
+ fileKey: FileKeySchema,
28
+ nodeIds: NodeIdsSchema,
29
+ maxResponseChars: MaxResponseCharsSchema,
30
+ });
31
+
32
+ const FigmaNodeProcessingOptions = {
33
+ depth: Type.Optional(Type.Number({ description: "How many levels of node hierarchy to include. Defaults to 2 and is capped at 4.", minimum: 1, maximum: 4 })),
34
+ includeHidden: Type.Optional(Type.Boolean({ description: "Include nodes where visible=false. Defaults to false." })),
35
+ includeVectors: Type.Optional(Type.Boolean({ description: "Include vector/icon internals. Defaults to false." })),
36
+ includeComponentInternals: Type.Optional(Type.Boolean({ description: "Expand component instance internals. Defaults to false." })),
37
+ };
38
+
39
+ const FigmaOptionalRenderOptions = {
40
+ renderImage: Type.Optional(Type.Boolean({ description: "Render the node and include image URL/local path in the response. Defaults to false." })),
41
+ outputDir: Type.Optional(Type.String({ description: "Optional directory for downloaded rendered image files. Omit unless the user requested persistent files; by default downloads go to an OS temp directory." })),
42
+ format: Type.Optional(Type.Unsafe<"png" | "jpg" | "svg" | "pdf">({ type: "string", enum: ["png", "jpg", "svg", "pdf"] })),
43
+ scale: Type.Optional(Type.Number({ description: "Render scale for bitmap formats", minimum: 0.01, maximum: 4 })),
44
+ };
45
+
46
+ export const FigmaProcessedNodeParams = Type.Object({
47
+ fileKey: FileKeySchema,
48
+ nodeId: NodeIdSchema,
49
+ ...FigmaNodeProcessingOptions,
50
+ maxResponseChars: MaxResponseCharsSchema,
51
+ });
52
+
53
+ export const FigmaProcessedNodeWithRenderParams = Type.Object({
54
+ fileKey: FileKeySchema,
55
+ nodeId: NodeIdSchema,
56
+ ...FigmaNodeProcessingOptions,
57
+ ...FigmaOptionalRenderOptions,
58
+ maxResponseChars: MaxResponseCharsSchema,
59
+ });
60
+
61
+ export const FigmaImplementationContextParams = Type.Object({
62
+ fileKey: FileKeySchema,
63
+ nodeId: NodeIdSchema,
64
+ ...FigmaNodeProcessingOptions,
65
+ ...FigmaOptionalRenderOptions,
66
+ framework: Type.Optional(Type.Unsafe<"react" | "html" | "vue" | "angular" | "react-native">({ type: "string", enum: ["react", "html", "vue", "angular", "react-native"] })),
67
+ styling: Type.Optional(Type.Unsafe<"css" | "css-modules" | "styled-components" | "tailwind" | "inline">({ type: "string", enum: ["css", "css-modules", "styled-components", "tailwind", "inline"] })),
68
+ resolveTokens: Type.Optional(Type.Boolean({ description: "Resolve style and variable IDs into token names when possible. Defaults to true." })),
69
+ includeCodeSnippets: Type.Optional(Type.Boolean({ description: "Include compact starter snippets for the selected framework/styling target. Defaults to false." })),
70
+ maxResponseChars: MaxResponseCharsSchema,
71
+ });
72
+
73
+ export const FigmaSingleFileParams = Type.Object({
74
+ fileKey: FileKeySchema,
75
+ maxResponseChars: MaxResponseCharsSchema,
76
+ });
77
+
78
+ export const FigmaSearchComponentsParams = Type.Object({
79
+ fileKey: FileKeySchema,
80
+ query: Type.String({ description: "Case-insensitive component name/description search term" }),
81
+ maxResponseChars: MaxResponseCharsSchema,
82
+ });
83
+
84
+ export const FigmaFindNodesParams = Type.Object({
85
+ fileKey: FileKeySchema,
86
+ query: Type.String({ description: "Layer name or visible text query" }),
87
+ nodeId: Type.Optional(NodeIdSchema),
88
+ ...FigmaNodeProcessingOptions,
89
+ exact: Type.Optional(Type.Boolean({ description: "Require an exact match instead of substring matching. Defaults to false." })),
90
+ caseSensitive: Type.Optional(Type.Boolean({ description: "Use case-sensitive matching. Defaults to false." })),
91
+ maxResults: Type.Optional(Type.Number({ description: "Maximum matches to return. Defaults to 50 and is capped at 200.", minimum: 1, maximum: 200 })),
92
+ maxResponseChars: MaxResponseCharsSchema,
93
+ });
94
+
95
+ export const FigmaRenderNodesParams = Type.Object({
96
+ fileKey: FileKeySchema,
97
+ nodeIds: NodeIdsSchema,
98
+ outputDir: Type.Optional(Type.String({ description: "Optional directory for downloaded image files. Omit unless the user requested persistent files; if omitted, an OS temp directory is created." })),
99
+ format: Type.Optional(Type.Unsafe<"png" | "jpg" | "svg" | "pdf">({ type: "string", enum: ["png", "jpg", "svg", "pdf"] })),
100
+ scale: Type.Optional(Type.Number({ description: "Render scale for bitmap formats", minimum: 0.01, maximum: 4 })),
101
+ download: Type.Optional(Type.Boolean({ description: "Download rendered assets locally. Defaults to true." })),
102
+ maxResponseChars: MaxResponseCharsSchema,
103
+ });
104
+
105
+ export const FigmaExtractAssetsParams = Type.Object({
106
+ fileKey: FileKeySchema,
107
+ nodeId: NodeIdSchema,
108
+ depth: Type.Optional(Type.Number({ description: "How many levels of node hierarchy to inspect for assets. Defaults to 3 and is capped at 4.", minimum: 1, maximum: 4 })),
109
+ assetTypes: Type.Optional(Type.Array(Type.Unsafe<"svgIcons" | "nodeRenders" | "imageFills">({ type: "string", enum: ["svgIcons", "nodeRenders", "imageFills"] }), { description: "Asset categories to extract. Defaults to all supported categories." })),
110
+ outputDir: Type.Optional(Type.String({ description: "Optional directory for downloaded asset files. Omit unless the user requested persistent files; by default files go to an OS temp directory." })),
111
+ includeHidden: Type.Optional(Type.Boolean({ description: "Include hidden nodes while discovering assets. Defaults to false." })),
112
+ maxAssets: Type.Optional(Type.Number({ description: "Maximum assets to include in the manifest. Defaults to 80.", minimum: 1, maximum: 500 })),
113
+ maxResponseChars: MaxResponseCharsSchema,
114
+ });
115
+
116
+ export const FigmaFindCodeConnectMappingParams = Type.Object({
117
+ fileKey: FileKeySchema,
118
+ nodeId: Type.Optional(NodeIdSchema),
119
+ componentKey: Type.Optional(Type.String({ description: "Optional Figma component key to search for locally." })),
120
+ rootDir: Type.Optional(Type.String({ description: "Optional directory under the current repo to scan. Defaults to cwd." })),
121
+ maxMatches: Type.Optional(Type.Number({ description: "Maximum local mapping matches to return. Defaults to 40.", minimum: 1, maximum: 200 })),
122
+ maxResponseChars: MaxResponseCharsSchema,
123
+ });
124
+
125
+ export const FigmaComponentImplementationHintsParams = Type.Object({
126
+ fileKey: FileKeySchema,
127
+ nodeId: NodeIdSchema,
128
+ ...FigmaNodeProcessingOptions,
129
+ framework: Type.Optional(Type.Unsafe<"react" | "html" | "vue" | "angular" | "react-native">({ type: "string", enum: ["react", "html", "vue", "angular", "react-native"] })),
130
+ styling: Type.Optional(Type.Unsafe<"css" | "css-modules" | "styled-components" | "tailwind" | "inline">({ type: "string", enum: ["css", "css-modules", "styled-components", "tailwind", "inline"] })),
131
+ includeCodeConnect: Type.Optional(Type.Boolean({ description: "Scan the local repo for Figma Code Connect mappings. Defaults to true." })),
132
+ includeSnippet: Type.Optional(Type.Boolean({ description: "Include starter framework snippet. Defaults to false." })),
133
+ rootDir: Type.Optional(Type.String({ description: "Optional local repo subdirectory for Code Connect scanning." })),
134
+ maxResponseChars: MaxResponseCharsSchema,
135
+ });
136
+
137
+ export const FigmaParseUrlParams = Type.Object({
138
+ url: Type.String({ description: "Figma design/file URL" }),
139
+ });
@@ -0,0 +1,195 @@
1
+ export interface FigmaFindNodesOptions {
2
+ query: string;
3
+ depth?: number;
4
+ exact?: boolean;
5
+ caseSensitive?: boolean;
6
+ includeHidden?: boolean;
7
+ includeVectors?: boolean;
8
+ includeComponentInternals?: boolean;
9
+ maxResults?: number;
10
+ }
11
+
12
+ export interface FigmaNodeSearchMatch {
13
+ id?: string;
14
+ name: string;
15
+ type: string;
16
+ path: string;
17
+ visible: boolean;
18
+ text?: string;
19
+ parent?: { id?: string; name: string; type: string; path: string };
20
+ roleHint?: string;
21
+ }
22
+
23
+ export interface FigmaNodeSearchResult {
24
+ query: string;
25
+ matchType: "name" | "text";
26
+ matches: FigmaNodeSearchMatch[];
27
+ metadata: {
28
+ truncated: boolean;
29
+ truncatedReasons: string[];
30
+ nextSteps: string[];
31
+ };
32
+ }
33
+
34
+ interface NormalizedSearchOptions extends Required<Omit<FigmaFindNodesOptions, "query">> {
35
+ query: string;
36
+ }
37
+
38
+ const VECTOR_TYPES = new Set(["VECTOR", "BOOLEAN_OPERATION", "STAR", "LINE", "ELLIPSE", "POLYGON", "REGULAR_POLYGON"]);
39
+ const DEFAULT_DEPTH = 4;
40
+ const DEFAULT_MAX_RESULTS = 50;
41
+ const MAX_DEPTH = 12;
42
+ const MAX_RESULTS = 200;
43
+
44
+ export function findNodesByName(node: unknown, options: FigmaFindNodesOptions): FigmaNodeSearchResult {
45
+ return findNodes(node, normalizeOptions(options), "name");
46
+ }
47
+
48
+ export function findNodesByText(node: unknown, options: FigmaFindNodesOptions): FigmaNodeSearchResult {
49
+ return findNodes(node, normalizeOptions(options), "text");
50
+ }
51
+
52
+ function findNodes(root: unknown, options: NormalizedSearchOptions, matchType: "name" | "text"): FigmaNodeSearchResult {
53
+ const matches: FigmaNodeSearchMatch[] = [];
54
+ const truncatedReasons: string[] = [];
55
+ let visited = 0;
56
+ let skippedHidden = 0;
57
+ let skippedVectors = 0;
58
+ let skippedInstances = 0;
59
+
60
+ function visit(node: unknown, level: number, path: string, parent?: FigmaNodeSearchMatch["parent"]): void {
61
+ const record = asRecord(node);
62
+ if (!Object.keys(record).length) return;
63
+ const visible = record.visible !== false;
64
+ if (!options.includeHidden && !visible) {
65
+ skippedHidden += 1;
66
+ return;
67
+ }
68
+ const type = String(record.type ?? "UNKNOWN");
69
+ const isVector = VECTOR_TYPES.has(type);
70
+ if (isVector && !options.includeVectors && level > 0) {
71
+ skippedVectors += 1;
72
+ return;
73
+ }
74
+ const name = String(record.name ?? "Unnamed node");
75
+ const nextPath = path ? `${path} > ${name}` : name;
76
+ visited += 1;
77
+
78
+ const candidate = matchType === "name" ? name : normalizeText(record.characters) ?? "";
79
+ if (candidate && isMatch(candidate, options)) {
80
+ if (matches.length < options.maxResults) {
81
+ matches.push({
82
+ id: stringValue(record.id),
83
+ name,
84
+ type,
85
+ path: nextPath,
86
+ visible,
87
+ text: matchType === "text" ? candidate : normalizeText(record.characters),
88
+ parent,
89
+ roleHint: roleHint(name, candidate, type),
90
+ });
91
+ } else if (!truncatedReasons.some((reason) => reason.includes("maxResults"))) {
92
+ truncatedReasons.push(`Reached maxResults ${options.maxResults}; additional matches were omitted.`);
93
+ }
94
+ }
95
+
96
+ if (level >= options.depth) {
97
+ if (getChildren(record).length && !truncatedReasons.some((reason) => reason.includes("depth limit"))) {
98
+ truncatedReasons.push(`Reached depth limit ${options.depth}; deeper descendants were not searched.`);
99
+ }
100
+ return;
101
+ }
102
+ if (type === "INSTANCE" && !options.includeComponentInternals && level > 0) {
103
+ skippedInstances += 1;
104
+ // Text labels inside instances remain useful search targets without exposing full structure.
105
+ for (const child of getChildren(record)) {
106
+ const childRecord = asRecord(child);
107
+ if (childRecord.type === "TEXT") visit(child, level + 1, nextPath, compactParent(record, nextPath));
108
+ }
109
+ return;
110
+ }
111
+ for (const child of getChildren(record)) visit(child, level + 1, nextPath, compactParent(record, nextPath));
112
+ }
113
+
114
+ visit(root, 0, "");
115
+
116
+ if (skippedHidden) truncatedReasons.push(`Skipped ${skippedHidden} hidden node(s). Set includeHidden=true to include them.`);
117
+ if (skippedVectors) truncatedReasons.push(`Skipped ${skippedVectors} vector/icon node(s). Set includeVectors=true to include them.`);
118
+ if (skippedInstances) truncatedReasons.push(`Collapsed ${skippedInstances} component instance subtree/subtrees. Set includeComponentInternals=true for internals.`);
119
+
120
+ const nextSteps = new Set<string>();
121
+ if (!matches.length) nextSteps.add("Try a broader query, disable exact matching, or search visible text instead of names.");
122
+ if (truncatedReasons.some((reason) => reason.includes("maxResults"))) nextSteps.add("Raise maxResults or narrow the search with nodeId/depth.");
123
+ if (truncatedReasons.some((reason) => reason.includes("depth limit")) && options.depth < MAX_DEPTH) nextSteps.add(`Increase depth to ${options.depth + 1} or search within a more specific nodeId.`);
124
+ if (skippedInstances) nextSteps.add("Set includeComponentInternals=true only for a focused component instance if internal layer matches matter.");
125
+
126
+ return {
127
+ query: options.query,
128
+ matchType,
129
+ matches,
130
+ metadata: {
131
+ truncated: truncatedReasons.some((reason) => !reason.startsWith("Skipped")) || matches.length >= options.maxResults,
132
+ truncatedReasons: uniqueStrings(truncatedReasons),
133
+ nextSteps: [...nextSteps],
134
+ },
135
+ };
136
+ }
137
+
138
+ function normalizeOptions(options: FigmaFindNodesOptions): NormalizedSearchOptions {
139
+ return {
140
+ query: options.query,
141
+ depth: clampInteger(options.depth ?? DEFAULT_DEPTH, 1, MAX_DEPTH),
142
+ exact: options.exact ?? false,
143
+ caseSensitive: options.caseSensitive ?? false,
144
+ includeHidden: options.includeHidden ?? false,
145
+ includeVectors: options.includeVectors ?? false,
146
+ includeComponentInternals: options.includeComponentInternals ?? false,
147
+ maxResults: clampInteger(options.maxResults ?? DEFAULT_MAX_RESULTS, 1, MAX_RESULTS),
148
+ };
149
+ }
150
+
151
+ function isMatch(candidate: string, options: NormalizedSearchOptions): boolean {
152
+ const haystack = options.caseSensitive ? candidate : candidate.toLowerCase();
153
+ const needle = options.caseSensitive ? options.query : options.query.toLowerCase();
154
+ return options.exact ? haystack === needle : haystack.includes(needle);
155
+ }
156
+
157
+ function compactParent(record: Record<string, unknown>, path: string): FigmaNodeSearchMatch["parent"] {
158
+ return { id: stringValue(record.id), name: String(record.name ?? "Unnamed node"), type: String(record.type ?? "UNKNOWN"), path };
159
+ }
160
+
161
+ function roleHint(name: string, text: string, type: string): string | undefined {
162
+ const haystack = `${name} ${text}`.toLowerCase();
163
+ if (/button|submit|save|continue|cancel|next|back/.test(haystack)) return "button";
164
+ if (/input|field|placeholder|select|dropdown/.test(haystack)) return "form-control";
165
+ if (/modal|dialog/.test(haystack)) return "dialog";
166
+ if (/icon/.test(haystack) || VECTOR_TYPES.has(type)) return "icon";
167
+ if (/title|heading|header/.test(haystack)) return "heading";
168
+ return undefined;
169
+ }
170
+
171
+ function getChildren(record: Record<string, unknown>): unknown[] {
172
+ return Array.isArray(record.children) ? record.children : [];
173
+ }
174
+
175
+ function asRecord(value: unknown): Record<string, unknown> {
176
+ return value && typeof value === "object" && !Array.isArray(value) ? (value as Record<string, unknown>) : {};
177
+ }
178
+
179
+ function normalizeText(value: unknown): string | undefined {
180
+ if (typeof value !== "string") return undefined;
181
+ const text = value.replace(/\s+/g, " ").trim();
182
+ return text || undefined;
183
+ }
184
+
185
+ function stringValue(value: unknown): string | undefined {
186
+ return typeof value === "string" ? value : undefined;
187
+ }
188
+
189
+ function clampInteger(value: number, min: number, max: number): number {
190
+ return Math.max(min, Math.min(max, Math.trunc(value)));
191
+ }
192
+
193
+ function uniqueStrings(values: string[]): string[] {
194
+ return [...new Set(values)];
195
+ }