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,471 @@
1
+ import { mkdir, mkdtemp, writeFile } from "node:fs/promises";
2
+ import { tmpdir } from "node:os";
3
+ import { isAbsolute, join, resolve } from "node:path";
4
+ import { readAuthToken } from "pi-common/auth";
5
+ import { createHttpClient, type HttpClient } from "pi-common/http-client";
6
+ import { createRateLimiter, type RateLimiter } from "pi-common/rate-limiter";
7
+ import { figmaCache } from "./figma-cache.js";
8
+ import {
9
+ collectAssetCandidates,
10
+ manifestEntryFromFile,
11
+ manifestEntryFromUrl,
12
+ safeFilename,
13
+ type FigmaAssetManifestEntry,
14
+ type FigmaAssetType,
15
+ type FigmaExtractAssetsResult,
16
+ } from "./figma-assets.js";
17
+ import { findCodeConnectMapping, type CodeConnectScanResult } from "./code-connect.js";
18
+ import { buildComponentImplementationHints, type FigmaComponentImplementationHints } from "./figma-component-hints.js";
19
+ import {
20
+ explainNode,
21
+ extractVisibleText,
22
+ getImplementationContext,
23
+ summarizeNode,
24
+ type FigmaImplementationContext,
25
+ type FigmaImplementationContextOptions,
26
+ type FigmaNodeSummary,
27
+ type FigmaRenderedAsset,
28
+ type FigmaSummarizerOptions,
29
+ type FigmaTextExtractionResult,
30
+ } from "./figma-summarizer.js";
31
+ import { findNodesByName, findNodesByText, type FigmaFindNodesOptions, type FigmaNodeSearchResult } from "./figma-search.js";
32
+ import { buildFigmaTokenMap } from "./figma-tokens.js";
33
+
34
+ export interface FigmaClientOptions {
35
+ baseUrl?: string;
36
+ timeoutMs?: number;
37
+ }
38
+
39
+ export interface RenderNodesOptions {
40
+ format?: "png" | "jpg" | "svg" | "pdf";
41
+ scale?: number;
42
+ outputDir?: string;
43
+ download?: boolean;
44
+ cwd: string;
45
+ }
46
+
47
+ export interface FigmaGetNodesOptions {
48
+ depth?: number;
49
+ }
50
+
51
+ export interface ParsedFigmaUrl {
52
+ fileKey: string;
53
+ nodeId?: string;
54
+ }
55
+
56
+ export interface RenderNodesResult {
57
+ images: Record<string, string | null>;
58
+ savedFiles: Array<{ nodeId: string; path: string }>;
59
+ }
60
+
61
+ export interface ExtractAssetsOptions {
62
+ depth?: number;
63
+ assetTypes?: FigmaAssetType[];
64
+ outputDir?: string;
65
+ includeHidden?: boolean;
66
+ maxAssets?: number;
67
+ cwd: string;
68
+ }
69
+
70
+ export interface ComponentHintsOptions extends FigmaImplementationContextOptions {
71
+ includeCodeConnect?: boolean;
72
+ includeSnippet?: boolean;
73
+ rootDir?: string;
74
+ cwd: string;
75
+ }
76
+
77
+ export class FigmaClient {
78
+ private readonly http: HttpClient;
79
+ private readonly limiter: RateLimiter;
80
+
81
+ constructor(options: FigmaClientOptions = {}) {
82
+ this.http = createHttpClient({
83
+ baseUrl: options.baseUrl ?? "https://api.figma.com",
84
+ timeoutMs: options.timeoutMs ?? 30_000,
85
+ service: "Figma",
86
+ headers: async () => ({ "X-Figma-Token": await readFigmaToken() }),
87
+ });
88
+ this.limiter = createRateLimiter({ minIntervalMs: 1_000 });
89
+ }
90
+
91
+ getFile(fileKey: string, depth?: number): Promise<unknown> {
92
+ const query = depth ? `?depth=${depth}` : "";
93
+ return this.cached(`file:${fileKey}:${depth ?? "all"}`, () => this.get(`/v1/files/${fileKey}${query}`));
94
+ }
95
+
96
+ getNodes(fileKey: string, nodeIds: readonly string[], options: FigmaGetNodesOptions = {}): Promise<unknown> {
97
+ const ids = normalizeNodeIds(nodeIds).join(",");
98
+ const depth = options.depth ? clampInteger(options.depth, 1, 4) : undefined;
99
+ const depthQuery = depth ? `&depth=${depth}` : "";
100
+ return this.cached(`nodes:${fileKey}:${ids}:${depth ?? "all"}`, () => this.get(`/v1/files/${fileKey}/nodes?ids=${encodeURIComponent(ids)}${depthQuery}`));
101
+ }
102
+
103
+ getStyles(fileKey: string): Promise<unknown> {
104
+ return this.cached(`styles:${fileKey}`, () => this.get(`/v1/files/${fileKey}/styles`));
105
+ }
106
+
107
+ getComponents(fileKey: string): Promise<unknown> {
108
+ return this.cached(`components:${fileKey}`, () => this.get(`/v1/files/${fileKey}/components`));
109
+ }
110
+
111
+ getComponentSets(fileKey: string): Promise<unknown> {
112
+ return this.cached(`componentSets:${fileKey}`, () => this.get(`/v1/files/${fileKey}/component_sets`));
113
+ }
114
+
115
+ getVariables(fileKey: string): Promise<unknown> {
116
+ return this.cached(`variables:${fileKey}`, () => this.get(`/v1/files/${fileKey}/variables/local`));
117
+ }
118
+
119
+ async searchComponents(fileKey: string, query: string): Promise<unknown> {
120
+ const response = await this.getComponents(fileKey);
121
+ const components = getNestedArray(response, ["meta", "components"]);
122
+ const needle = query.toLowerCase();
123
+ return components.filter((component) => {
124
+ const record = component as Record<string, unknown>;
125
+ return String(record.name ?? "").toLowerCase().includes(needle) || String(record.description ?? "").toLowerCase().includes(needle);
126
+ });
127
+ }
128
+
129
+ async getDesignContext(fileKey: string, nodeId?: string): Promise<unknown> {
130
+ if (nodeId) return this.getTargetDesignContext(fileKey, nodeId);
131
+
132
+ const file = await this.getFile(fileKey, 2);
133
+ const fileRecord = asRecord(file);
134
+ return {
135
+ file: {
136
+ name: fileRecord.name,
137
+ lastModified: fileRecord.lastModified,
138
+ version: fileRecord.version,
139
+ },
140
+ document: {
141
+ name: asRecord(fileRecord.document).name,
142
+ children: collectTopLevelStructure(fileRecord.document),
143
+ },
144
+ metadata: {
145
+ truncated: true,
146
+ note: "Only canvases and top-level frames are returned by default. Pass nodeId and use processed node tools for details.",
147
+ nextSteps: ["Call figma_get_node_summary or figma_explain_node for a specific node.", "Use figma_get_file only for raw debugging."],
148
+ },
149
+ };
150
+ }
151
+
152
+ async getNodeSummary(fileKey: string, nodeId: string, options: FigmaSummarizerOptions = {}): Promise<FigmaNodeSummary> {
153
+ return summarizeNode(await this.getSingleNodeDocument(fileKey, nodeId, options.depth ?? 2), options);
154
+ }
155
+
156
+ async extractText(fileKey: string, nodeId: string, options: FigmaSummarizerOptions = {}): Promise<FigmaTextExtractionResult> {
157
+ return extractVisibleText(await this.getSingleNodeDocument(fileKey, nodeId, options.depth ?? 2), options);
158
+ }
159
+
160
+ async explainNode(fileKey: string, nodeId: string, options: FigmaSummarizerOptions & { assets?: FigmaRenderedAsset[] } = {}): Promise<string> {
161
+ return explainNode(await this.getSingleNodeDocument(fileKey, nodeId, options.depth ?? 2), options);
162
+ }
163
+
164
+ async getImplementationContext(fileKey: string, nodeId: string, options: FigmaImplementationContextOptions = {}): Promise<FigmaImplementationContext> {
165
+ const document = await this.getSingleNodeDocument(fileKey, nodeId, options.depth ?? 2);
166
+ if (options.resolveTokens === false) return getImplementationContext(document, options);
167
+ try {
168
+ const [styles, variables] = await Promise.all([this.getStyles(fileKey), this.getVariables(fileKey)]);
169
+ return getImplementationContext(document, { ...options, tokenMap: buildFigmaTokenMap(styles, variables) });
170
+ } catch {
171
+ return getImplementationContext(document, options);
172
+ }
173
+ }
174
+
175
+ async findNodesByName(fileKey: string, params: FigmaFindNodesOptions & { nodeId?: string }): Promise<FigmaNodeSearchResult> {
176
+ const document = await this.getSearchRoot(fileKey, params);
177
+ const result = findNodesByName(document, params);
178
+ if (!params.nodeId) {
179
+ result.metadata.nextSteps.unshift("Full-file search is depth-limited; pass nodeId from figma_get_design_context to search a specific frame more precisely.");
180
+ }
181
+ return result;
182
+ }
183
+
184
+ async findNodesByText(fileKey: string, params: FigmaFindNodesOptions & { nodeId?: string }): Promise<FigmaNodeSearchResult> {
185
+ const document = await this.getSearchRoot(fileKey, params);
186
+ const result = findNodesByText(document, params);
187
+ if (!params.nodeId) {
188
+ result.metadata.nextSteps.unshift("Full-file search is depth-limited; pass nodeId from figma_get_design_context to search a specific frame more precisely.");
189
+ }
190
+ return result;
191
+ }
192
+
193
+ async getNodeMetadata(fileKey: string, nodeIds: readonly string[]): Promise<unknown> {
194
+ const response = await this.getNodes(fileKey, nodeIds, { depth: 2 });
195
+ const nodes = asRecord(response).nodes;
196
+ return Object.entries(asRecord(nodes)).map(([id, value]) => {
197
+ const document = asRecord(asRecord(value).document);
198
+ return {
199
+ id,
200
+ name: document.name,
201
+ type: document.type,
202
+ boundingBox: document.absoluteBoundingBox,
203
+ constraints: document.constraints,
204
+ layout: {
205
+ layoutMode: document.layoutMode,
206
+ itemSpacing: document.itemSpacing,
207
+ paddingLeft: document.paddingLeft,
208
+ paddingRight: document.paddingRight,
209
+ paddingTop: document.paddingTop,
210
+ paddingBottom: document.paddingBottom,
211
+ primaryAxisAlignItems: document.primaryAxisAlignItems,
212
+ counterAxisAlignItems: document.counterAxisAlignItems,
213
+ },
214
+ cornerRadius: document.cornerRadius,
215
+ opacity: document.opacity,
216
+ effects: document.effects ?? [],
217
+ fills: document.fills ?? [],
218
+ strokes: document.strokes ?? [],
219
+ strokeWeight: document.strokeWeight,
220
+ children: getNestedArray(document, ["children"]).map((child) => {
221
+ const childRecord = asRecord(child);
222
+ return {
223
+ id: childRecord.id,
224
+ name: childRecord.name,
225
+ type: childRecord.type,
226
+ boundingBox: childRecord.absoluteBoundingBox,
227
+ visible: childRecord.visible ?? true,
228
+ };
229
+ }),
230
+ };
231
+ });
232
+ }
233
+
234
+ async renderNodes(fileKey: string, nodeIds: readonly string[], options: RenderNodesOptions): Promise<RenderNodesResult> {
235
+ const ids = normalizeNodeIds(nodeIds).join(",");
236
+ const format = options.format ?? "png";
237
+ const scale = options.scale ?? 2;
238
+ const response = await this.get<{ images?: Record<string, string | null>; err?: string }>(
239
+ `/v1/images/${fileKey}?ids=${encodeURIComponent(ids)}&format=${format}&scale=${scale}`,
240
+ );
241
+ if (response.err) throw new Error(response.err);
242
+
243
+ const images = response.images ?? {};
244
+ const savedFiles: Array<{ nodeId: string; path: string }> = [];
245
+ if (options.download ?? true) {
246
+ const outputDir = await resolveOutputDir(options.cwd, options.outputDir);
247
+ await mkdir(outputDir, { recursive: true });
248
+ for (const [nodeId, url] of Object.entries(images)) {
249
+ if (!url) continue;
250
+ const extension = format === "jpg" ? "jpg" : format;
251
+ const safeNodeId = nodeId.replace(/[^a-z0-9_-]/gi, "_");
252
+ const outputPath = resolve(outputDir, `${fileKey}_${safeNodeId}.${extension}`);
253
+ const bytes = await this.http.download(url);
254
+ await writeFile(outputPath, Buffer.from(bytes));
255
+ savedFiles.push({ nodeId, path: outputPath });
256
+ }
257
+ }
258
+
259
+ return { images, savedFiles };
260
+ }
261
+
262
+ getImageFills(fileKey: string): Promise<Record<string, string>> {
263
+ return this.cached(`imageFills:${fileKey}`, async () => {
264
+ const response = await this.get<{ meta?: { images?: Record<string, string> } }>(`/v1/files/${fileKey}/images`);
265
+ return response.meta?.images ?? {};
266
+ });
267
+ }
268
+
269
+ async extractAssets(fileKey: string, nodeId: string, options: ExtractAssetsOptions): Promise<FigmaExtractAssetsResult> {
270
+ const document = await this.getSingleNodeDocument(fileKey, nodeId, options.depth ?? 3);
271
+ const assetTypes = options.assetTypes?.length ? options.assetTypes : (["svgIcons", "nodeRenders", "imageFills"] as FigmaAssetType[]);
272
+ const collected = collectAssetCandidates(document, { assetTypes, includeHidden: options.includeHidden, maxAssets: options.maxAssets });
273
+ const outputDir = await resolveOutputDir(options.cwd, options.outputDir);
274
+ await mkdir(outputDir, { recursive: true });
275
+ const manifest: FigmaAssetManifestEntry[] = [];
276
+
277
+ const svgCandidates = collected.assets.filter((asset) => asset.kind === "svgIcon" && asset.nodeId);
278
+ if (svgCandidates.length) {
279
+ const rendered = await this.renderNodes(fileKey, svgCandidates.map((asset) => asset.nodeId as string), { cwd: options.cwd, outputDir, format: "svg", download: true });
280
+ for (const candidate of svgCandidates) {
281
+ const file = rendered.savedFiles.find((saved) => saved.nodeId === candidate.nodeId);
282
+ manifest.push(file ? await manifestEntryFromFile(candidate, file.path, rendered.images[candidate.nodeId as string], "svg") : manifestEntryFromUrl(candidate, rendered.images[candidate.nodeId as string], "svg"));
283
+ }
284
+ }
285
+
286
+ const renderCandidates = collected.assets.filter((asset) => asset.kind === "nodeRender" && asset.nodeId);
287
+ if (renderCandidates.length) {
288
+ const rendered = await this.renderNodes(fileKey, renderCandidates.map((asset) => asset.nodeId as string), { cwd: options.cwd, outputDir, format: "png", download: true });
289
+ for (const candidate of renderCandidates) {
290
+ const file = rendered.savedFiles.find((saved) => saved.nodeId === candidate.nodeId);
291
+ manifest.push(file ? await manifestEntryFromFile(candidate, file.path, rendered.images[candidate.nodeId as string], "png") : manifestEntryFromUrl(candidate, rendered.images[candidate.nodeId as string], "png"));
292
+ }
293
+ }
294
+
295
+ const imageFillCandidates = collected.assets.filter((asset) => asset.kind === "imageFill" && asset.imageRef);
296
+ if (imageFillCandidates.length) {
297
+ const images = await this.getImageFills(fileKey);
298
+ for (const candidate of imageFillCandidates) {
299
+ const url = images[candidate.imageRef as string];
300
+ if (!url) {
301
+ manifest.push(manifestEntryFromUrl(candidate, null, "unknown"));
302
+ continue;
303
+ }
304
+ const bytes = await this.http.download(url);
305
+ const extension = url.includes(".webp") ? "webp" : url.includes(".jpg") || url.includes(".jpeg") ? "jpg" : "png";
306
+ const outputPath = resolve(outputDir, `${safeFilename(candidate.suggestedName.replace(/\.[^.]+$/, ""))}-${safeFilename(candidate.imageRef as string).slice(0, 12)}.${extension}`);
307
+ await writeFile(outputPath, Buffer.from(bytes));
308
+ manifest.push(await manifestEntryFromFile(candidate, outputPath, url, extension));
309
+ }
310
+ }
311
+
312
+ const unresolvedFills = manifest.filter((entry) => entry.kind === "imageFill" && !entry.url).length;
313
+ return {
314
+ nodeId: normalizeNodeId(nodeId),
315
+ assetTypes,
316
+ assets: manifest,
317
+ metadata: {
318
+ truncated: collected.metadata.truncated,
319
+ truncatedReasons: [...collected.metadata.truncatedReasons, ...(unresolvedFills ? [`${unresolvedFills} image fill(s) could not be resolved to downloadable URLs.`] : [])],
320
+ nextSteps: [...collected.metadata.nextSteps, "Use manifest nodePath values to map downloaded files back to source Figma layers."],
321
+ },
322
+ };
323
+ }
324
+
325
+ findCodeConnectMapping(options: { fileKey: string; nodeId?: string; componentKey?: string; rootDir?: string; maxMatches?: number; cwd: string }): Promise<CodeConnectScanResult> {
326
+ return findCodeConnectMapping(options);
327
+ }
328
+
329
+ async getComponentImplementationHints(fileKey: string, nodeId: string, options: ComponentHintsOptions): Promise<FigmaComponentImplementationHints> {
330
+ const summary = await this.getNodeSummary(fileKey, nodeId, options);
331
+ const implementation = await this.getImplementationContext(fileKey, nodeId, { ...options, includeCodeSnippets: options.includeSnippet ?? options.includeCodeSnippets });
332
+ const codeConnect = options.includeCodeConnect === false ? undefined : await this.findCodeConnectMapping({ fileKey, nodeId, rootDir: options.rootDir, cwd: options.cwd });
333
+ return buildComponentImplementationHints(summary, implementation, { framework: options.framework, styling: options.styling, includeSnippet: options.includeSnippet ?? options.includeCodeSnippets, includeCodeConnect: options.includeCodeConnect !== false }, codeConnect);
334
+ }
335
+
336
+ private get<T = unknown>(path: string): Promise<T> {
337
+ return this.limiter.schedule(() => this.http.get<T>(path));
338
+ }
339
+
340
+ private async getTargetDesignContext(fileKey: string, nodeId: string): Promise<unknown> {
341
+ const [file, targetSummary] = await Promise.all([this.getFile(fileKey, 2), this.getNodeSummary(fileKey, nodeId, { depth: 2 })]);
342
+ const fileRecord = asRecord(file);
343
+ const normalizedNodeId = normalizeNodeId(nodeId);
344
+ const shallowStructure = collectTopLevelStructure(fileRecord.document);
345
+ const targetLocation = findShallowLocation(fileRecord.document, normalizedNodeId);
346
+
347
+ return {
348
+ file: {
349
+ name: fileRecord.name,
350
+ lastModified: fileRecord.lastModified,
351
+ version: fileRecord.version,
352
+ },
353
+ targetNode: targetSummary,
354
+ location: targetLocation ?? {
355
+ targetNodeId: normalizedNodeId,
356
+ note: "Target node is not present in the shallow file tree, so ancestors/siblings are unavailable without raw debugging.",
357
+ },
358
+ document: {
359
+ name: asRecord(fileRecord.document).name,
360
+ children: shallowStructure,
361
+ },
362
+ metadata: {
363
+ truncated: targetSummary.metadata?.truncated ?? true,
364
+ note: "Design context is compact: target summary plus shallow file structure only.",
365
+ nextSteps: targetSummary.metadata?.nextSteps?.length
366
+ ? targetSummary.metadata.nextSteps
367
+ : ["Call figma_explain_node for a human-readable explanation.", "Call figma_get_implementation_context for coding details."],
368
+ },
369
+ };
370
+ }
371
+
372
+ private async getSingleNodeDocument(fileKey: string, nodeId: string, depth: number): Promise<unknown> {
373
+ const normalizedNodeId = normalizeNodeId(nodeId);
374
+ const response = await this.getNodes(fileKey, [normalizedNodeId], { depth });
375
+ const document = asRecord(asRecord(asRecord(response).nodes)[normalizedNodeId]).document;
376
+ if (!document) throw new Error(`Figma node ${normalizedNodeId} was not found in file ${fileKey}.`);
377
+ return document;
378
+ }
379
+
380
+ private async getSearchRoot(fileKey: string, params: FigmaFindNodesOptions & { nodeId?: string }): Promise<unknown> {
381
+ const depth = params.depth ? clampInteger(params.depth, 1, 4) : 4;
382
+ if (params.nodeId) return this.getSingleNodeDocument(fileKey, params.nodeId, depth);
383
+ const file = await this.getFile(fileKey, depth);
384
+ return asRecord(file).document;
385
+ }
386
+
387
+ private cached<T>(key: string, load: () => Promise<T>): Promise<T> {
388
+ return figmaCache.getOrSet(key, load) as Promise<T>;
389
+ }
390
+ }
391
+
392
+ export function readFigmaToken(): Promise<string> {
393
+ return readAuthToken({ envName: "FIGMA_TOKEN", authPath: ["figma", "token"] });
394
+ }
395
+
396
+ export function parseFigmaUrl(url: string): ParsedFigmaUrl {
397
+ const parsed = new URL(url);
398
+ const parts = parsed.pathname.split("/").filter(Boolean);
399
+ const fileKey = parts[1];
400
+ if (!fileKey || !["design", "file", "proto"].includes(parts[0] ?? "")) {
401
+ throw new Error("Expected a Figma URL like https://www.figma.com/design/<fileKey>/...");
402
+ }
403
+ const nodeId = parsed.searchParams.get("node-id") ?? undefined;
404
+ return { fileKey, nodeId: nodeId ? normalizeNodeId(nodeId) : undefined };
405
+ }
406
+
407
+ export function normalizeNodeId(nodeId: string): string {
408
+ return nodeId.replace(/-/g, ":");
409
+ }
410
+
411
+ export function normalizeNodeIds(nodeIds: readonly string[]): string[] {
412
+ return nodeIds.map(normalizeNodeId);
413
+ }
414
+
415
+ async function resolveOutputDir(cwd: string, outputDir?: string): Promise<string> {
416
+ if (!outputDir) return mkdtemp(join(tmpdir(), "pi-figma-assets-"));
417
+ return isAbsolute(outputDir) ? outputDir : resolve(cwd, outputDir);
418
+ }
419
+
420
+ function asRecord(value: unknown): Record<string, unknown> {
421
+ return value && typeof value === "object" ? (value as Record<string, unknown>) : {};
422
+ }
423
+
424
+ function getChildren(value: unknown): unknown[] {
425
+ const children = asRecord(value).children;
426
+ return Array.isArray(children) ? children : [];
427
+ }
428
+
429
+ function getNestedArray(value: unknown, path: readonly string[]): unknown[] {
430
+ let current = value;
431
+ for (const segment of path) current = asRecord(current)[segment];
432
+ return Array.isArray(current) ? current : [];
433
+ }
434
+
435
+ function collectTopLevelStructure(value: unknown): Array<{ id: unknown; name: unknown; type: unknown; children?: Array<{ id: unknown; name: unknown; type: unknown }> }> {
436
+ const document = asRecord(value);
437
+ return getChildren(document).map((page) => {
438
+ const pageRecord = asRecord(page);
439
+ return {
440
+ id: pageRecord.id,
441
+ name: pageRecord.name,
442
+ type: pageRecord.type,
443
+ children: getChildren(pageRecord).slice(0, 100).map((child) => {
444
+ const childRecord = asRecord(child);
445
+ return { id: childRecord.id, name: childRecord.name, type: childRecord.type };
446
+ }),
447
+ };
448
+ });
449
+ }
450
+
451
+ function findShallowLocation(value: unknown, nodeId: string): unknown {
452
+ for (const page of getChildren(value)) {
453
+ const pageRecord = asRecord(page);
454
+ const pageChildren = getChildren(pageRecord);
455
+ for (const child of pageChildren) {
456
+ const childRecord = asRecord(child);
457
+ if (childRecord.id === nodeId) {
458
+ return {
459
+ page: { id: pageRecord.id, name: pageRecord.name, type: pageRecord.type },
460
+ ancestors: [{ id: pageRecord.id, name: pageRecord.name, type: pageRecord.type }],
461
+ siblingNames: pageChildren.filter((sibling) => asRecord(sibling).id !== nodeId).slice(0, 100).map((sibling) => asRecord(sibling).name),
462
+ };
463
+ }
464
+ }
465
+ }
466
+ return null;
467
+ }
468
+
469
+ function clampInteger(value: number, min: number, max: number): number {
470
+ return Math.max(min, Math.min(max, Math.trunc(value)));
471
+ }
@@ -0,0 +1,87 @@
1
+ import { buildFrameworkHints, type FigmaFramework, type FigmaStyling } from "./figma-implementation.js";
2
+ import type { FigmaImplementationContext, FigmaNodeSummary } from "./figma-summarizer.js";
3
+ import type { CodeConnectScanResult } from "./code-connect.js";
4
+
5
+ export interface FigmaComponentHintOptions {
6
+ framework?: FigmaFramework;
7
+ styling?: FigmaStyling;
8
+ includeSnippet?: boolean;
9
+ includeCodeConnect?: boolean;
10
+ }
11
+
12
+ export interface FigmaComponentImplementationHints {
13
+ componentName: string;
14
+ suggestedFiles: string[];
15
+ suggestedProps: Array<Record<string, unknown>>;
16
+ statesAndVariants: Array<Record<string, unknown>>;
17
+ accessibilityRequirements: Array<Record<string, unknown>>;
18
+ tokenDependencies: Array<Record<string, unknown>>;
19
+ assetDependencies: Array<Record<string, unknown>>;
20
+ codeConnect?: CodeConnectScanResult;
21
+ frameworkHints?: Record<string, unknown>;
22
+ metadata: { truncated: boolean; truncatedReasons: string[]; nextSteps: string[] };
23
+ }
24
+
25
+ export function buildComponentImplementationHints(
26
+ summary: FigmaNodeSummary,
27
+ implementationContext: FigmaImplementationContext,
28
+ options: FigmaComponentHintOptions = {},
29
+ codeConnect?: CodeConnectScanResult,
30
+ ): FigmaComponentImplementationHints {
31
+ const componentName = toPascalCase(summary.name || "FigmaComponent");
32
+ const suggestedProps = inferProps(summary);
33
+ const statesAndVariants = collectVariants(summary);
34
+ const frameworkHints = buildFrameworkHints(summary, { framework: options.framework, styling: options.styling, includeCodeSnippets: options.includeSnippet });
35
+ const nextSteps = ["Use these hints as starter guidance; map to existing app components and design tokens before coding."];
36
+ if (!codeConnect?.matches.length && options.includeCodeConnect) nextSteps.push("No Code Connect match was found; search local component names before creating a new component.");
37
+ return {
38
+ componentName,
39
+ suggestedFiles: frameworkHints?.fileHints as string[] | undefined ?? [`${componentName}.tsx`],
40
+ suggestedProps,
41
+ statesAndVariants,
42
+ accessibilityRequirements: (implementationContext.accessibility ?? []).slice(0, 40),
43
+ tokenDependencies: ((implementationContext.designTokens?.resolved as Array<Record<string, unknown>> | undefined) ?? []).slice(0, 40),
44
+ assetDependencies: (implementationContext.assets ?? []).slice(0, 40).map((asset) => ({ ...asset })),
45
+ codeConnect: options.includeCodeConnect ? codeConnect : undefined,
46
+ frameworkHints,
47
+ metadata: { truncated: false, truncatedReasons: [], nextSteps },
48
+ };
49
+ }
50
+
51
+ function inferProps(summary: FigmaNodeSummary): Array<Record<string, unknown>> {
52
+ const props: Array<Record<string, unknown>> = [];
53
+ const texts = summary.visibleText ?? [];
54
+ if (texts.length) props.push({ name: "children", type: "ReactNode/string", source: "visible text", example: texts[0] });
55
+ if (hasRole(summary, "button")) props.push({ name: "onClick", type: "() => void", source: "button-like layer" });
56
+ if (hasRole(summary, "form-control")) props.push({ name: "value", type: "string", source: "form-control-like layer" });
57
+ return props;
58
+ }
59
+
60
+ function collectVariants(summary: FigmaNodeSummary): Array<Record<string, unknown>> {
61
+ const out: Array<Record<string, unknown>> = [];
62
+ for (const node of flatten(summary)) {
63
+ const properties = node.component?.componentProperties as Record<string, unknown> | undefined;
64
+ if (!properties) continue;
65
+ for (const [name, raw] of Object.entries(properties)) out.push({ nodeId: node.id, nodeName: node.name, name, ...(raw as Record<string, unknown>) });
66
+ }
67
+ return out.slice(0, 40);
68
+ }
69
+
70
+ function hasRole(summary: FigmaNodeSummary, role: string): boolean {
71
+ return flatten(summary).some((node) => node.roleGuess === role || String(node.name).toLowerCase().includes(role));
72
+ }
73
+
74
+ function flatten(summary: FigmaNodeSummary): FigmaNodeSummary[] {
75
+ const out: FigmaNodeSummary[] = [];
76
+ function visit(node: FigmaNodeSummary): void {
77
+ out.push(node);
78
+ for (const child of node.children ?? []) visit(child);
79
+ }
80
+ visit(summary);
81
+ return out;
82
+ }
83
+
84
+ function toPascalCase(value: string): string {
85
+ const result = value.replace(/[^a-z0-9]+/gi, " ").trim().split(/\s+/).filter(Boolean).map((part) => part[0]?.toUpperCase() + part.slice(1)).join("");
86
+ return /^[A-Z]/.test(result) ? result : `Figma${result || "Component"}`;
87
+ }