@wix/mcp 1.0.97 → 1.0.98

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.
@@ -62,4 +62,27 @@ export interface WixelStylePresetSummary {
62
62
  raw?: Record<string, unknown>;
63
63
  }
64
64
  export declare function getStylePresetsByIds({ siteAuthHeaders, stylePresetIds, apiBaseURL, conductorOverride }: GetStylePresetsOptions): Promise<WixelStylePresetSummary[]>;
65
+ export interface AssetExportOptions {
66
+ siteAuthHeaders: Record<string, string>;
67
+ projectId: string;
68
+ assetType: string;
69
+ apiBaseURL?: string;
70
+ conductorOverride?: string;
71
+ }
72
+ export interface AssetExportState {
73
+ complete: boolean;
74
+ progress: number | null;
75
+ url: string | null;
76
+ errorDescription: string | null;
77
+ title: string | null;
78
+ }
79
+ export declare const WIXEL_HOST_EXPORT_ASSET_PATH = "/_api/wixel/v1/export/asset";
80
+ export declare const WIXEL_HOST_EXPORT_STATE_PATH = "/_api/wixel/v1/export/state";
81
+ export declare function startAssetExport({ assetId, params, ...options }: AssetExportOptions & {
82
+ assetId: string;
83
+ params?: Record<string, unknown>;
84
+ }): Promise<string>;
85
+ export declare function getAssetExportState({ jobId, ...options }: AssetExportOptions & {
86
+ jobId: string;
87
+ }): Promise<AssetExportState>;
65
88
  //# sourceMappingURL=api.d.ts.map
@@ -100,4 +100,51 @@ export async function getStylePresetsByIds({ siteAuthHeaders, stylePresetIds, ap
100
100
  };
101
101
  });
102
102
  }
103
+ function wixelHostClient({ siteAuthHeaders, apiBaseURL = WWW_ASSET_V1_ASSET_API_BASE_URL, conductorOverride }) {
104
+ return createHttpClient({
105
+ headers: {
106
+ ...COMMON_HEADERS,
107
+ ...siteAuthHeaders,
108
+ ...(conductorOverride ? { ...CONDUCTOR_OVERRIDE_HEADERS } : {})
109
+ },
110
+ baseURL: apiBaseURL
111
+ });
112
+ }
113
+ // WixelHost.ExportAsset — the same export the editor's Download button starts.
114
+ // The host hands it to the asset type's own export SPI (icons, logo, slides…),
115
+ // which reads `params` (e.g. icons: fileType, size, layout).
116
+ // WixelHost REST paths on manage.wix.com — the same endpoints the editor's
117
+ // Download button reaches through @wix/ambassador-wixel-v1-asset-dynamic-payload.
118
+ // Called directly so the published package keeps zod as its only dependency
119
+ // and no new client library is added.
120
+ export const WIXEL_HOST_EXPORT_ASSET_PATH = '/_api/wixel/v1/export/asset';
121
+ export const WIXEL_HOST_EXPORT_STATE_PATH = '/_api/wixel/v1/export/state';
122
+ export async function startAssetExport({ assetId, params, ...options }) {
123
+ const { data } = await wixelHostClient(options).post(WIXEL_HOST_EXPORT_ASSET_PATH, {
124
+ projectId: options.projectId,
125
+ assetId,
126
+ assetType: options.assetType,
127
+ ...(params ? { params } : {})
128
+ });
129
+ if (!data?.jobId) {
130
+ throw new Error('Wixel did not start an export job for this asset');
131
+ }
132
+ return data.jobId;
133
+ }
134
+ // WixelHost.GetJobState — progress, and the file URL once complete.
135
+ export async function getAssetExportState({ jobId, ...options }) {
136
+ const { data } = await wixelHostClient(options).post(WIXEL_HOST_EXPORT_STATE_PATH, {
137
+ jobId,
138
+ projectId: options.projectId,
139
+ assetType: options.assetType
140
+ });
141
+ const state = data?.state ?? {};
142
+ return {
143
+ complete: state.complete === true,
144
+ progress: state.progress ?? null,
145
+ url: state.url ?? null,
146
+ errorDescription: state.errorDescription ?? null,
147
+ title: state.title ?? null
148
+ };
149
+ }
103
150
  //# sourceMappingURL=api.js.map
@@ -0,0 +1,19 @@
1
+ export interface AppIconFile {
2
+ /** Where the file goes in the app project. */
3
+ path: string;
4
+ /** Download URL (PNG files). */
5
+ url?: string;
6
+ /** Inline content (JSON files). */
7
+ content?: string;
8
+ }
9
+ /** The Wix Media file a URL points at, without any transform; null when it is not one. */
10
+ export declare function wixMediaFileUrl(url: string): string | null;
11
+ /** A square PNG of the file at `size` px, rendered by Wix Media. */
12
+ export declare function wixMediaSquarePng(fileUrl: string, size: number): string;
13
+ /**
14
+ * Every app-icon file for one exported PNG. Returns null when the PNG is not on
15
+ * Wix Media (the sizes cannot be made by URL), so the caller hands back the
16
+ * single file instead.
17
+ */
18
+ export declare function buildAppIconFiles(exportedPngUrl: string, appName: string): AppIconFile[] | null;
19
+ //# sourceMappingURL=app-icon-files.d.ts.map
@@ -0,0 +1,77 @@
1
+ // App-icon size pack for an app icon staged as an IMAGE asset (the app-icon
2
+ // skill stages each tile as a full-bleed 1024 × 1024 IMAGE). The export gives
3
+ // one PNG on Wix Media; every other size is a Wix Media resize of that file
4
+ // (`/v1/fill/w_N,h_N/…`), so the pack is a list of URLs plus the two small
5
+ // JSON files the platforms read — no image processing in the MCP server.
6
+ //
7
+ // ICONS sets get the same pack as a zip from the icons export itself
8
+ // (layout "appIcons"); this is the IMAGE counterpart.
9
+ const WIX_MEDIA_FILE = /^(https:\/\/static\.wixstatic\.com\/media\/[^/?#]+)/;
10
+ const ANDROID_MIPMAPS = [
11
+ ['mdpi', 48],
12
+ ['hdpi', 72],
13
+ ['xhdpi', 96],
14
+ ['xxhdpi', 144],
15
+ ['xxxhdpi', 192]
16
+ ];
17
+ /** The Wix Media file a URL points at, without any transform; null when it is not one. */
18
+ export function wixMediaFileUrl(url) {
19
+ return WIX_MEDIA_FILE.exec(url)?.[1] ?? null;
20
+ }
21
+ /** A square PNG of the file at `size` px, rendered by Wix Media. */
22
+ export function wixMediaSquarePng(fileUrl, size) {
23
+ return `${fileUrl}/v1/fill/w_${size},h_${size},al_c,q_95/icon-${size}.png`;
24
+ }
25
+ /**
26
+ * Every app-icon file for one exported PNG. Returns null when the PNG is not on
27
+ * Wix Media (the sizes cannot be made by URL), so the caller hands back the
28
+ * single file instead.
29
+ */
30
+ export function buildAppIconFiles(exportedPngUrl, appName) {
31
+ const file = wixMediaFileUrl(exportedPngUrl);
32
+ if (!file) {
33
+ return null;
34
+ }
35
+ const png = (path, size) => ({
36
+ path,
37
+ url: wixMediaSquarePng(file, size)
38
+ });
39
+ return [
40
+ png('ios/AppIcon.appiconset/icon-1024.png', 1024),
41
+ {
42
+ path: 'ios/AppIcon.appiconset/Contents.json',
43
+ content: JSON.stringify({
44
+ images: [
45
+ {
46
+ filename: 'icon-1024.png',
47
+ idiom: 'universal',
48
+ platform: 'ios',
49
+ size: '1024x1024'
50
+ }
51
+ ],
52
+ info: { author: 'wixel', version: 1 }
53
+ }, null, 2)
54
+ },
55
+ ...ANDROID_MIPMAPS.map(([density, size]) => png(`android/res/mipmap-${density}/ic_launcher.png`, size)),
56
+ png('android/play-store-512.png', 512),
57
+ png('web/favicon-16.png', 16),
58
+ png('web/favicon-32.png', 32),
59
+ png('web/favicon-48.png', 48),
60
+ png('web/apple-touch-icon.png', 180),
61
+ png('web/icon-192.png', 192),
62
+ png('web/icon-512.png', 512),
63
+ {
64
+ path: 'web/site.webmanifest',
65
+ content: JSON.stringify({
66
+ name: appName,
67
+ short_name: appName.slice(0, 12),
68
+ icons: [
69
+ { src: 'icon-192.png', sizes: '192x192', type: 'image/png' },
70
+ { src: 'icon-512.png', sizes: '512x512', type: 'image/png' }
71
+ ],
72
+ display: 'standalone'
73
+ }, null, 2)
74
+ }
75
+ ];
76
+ }
77
+ //# sourceMappingURL=app-icon-files.js.map
@@ -6,6 +6,7 @@ export { addGetWixelJobStatusTool } from './get-wixel-job-status-tool.js';
6
6
  export { addMarkWixelJobAsFulfilledTool } from './mark-wixel-job-as-fulfilled-tool.js';
7
7
  export { addGetWixelAssetTool } from './get-wixel-asset-tool.js';
8
8
  export { addGetWixelProjectAssetsTool } from './get-wixel-project-assets-tool.js';
9
+ export { addGetWixelAssetFilesTool } from './wixel-asset-files-tool.js';
9
10
  export declare function addWixelTools(server: WixMcpServer, authStrategy: McpAuthenticationStrategy, options?: {
10
11
  conductorOverride?: string;
11
12
  supportsWidgets?: () => boolean;
@@ -3,12 +3,14 @@ import { addMarkWixelJobAsFulfilledTool } from './mark-wixel-job-as-fulfilled-to
3
3
  import { addGetWixelAssetTool } from './get-wixel-asset-tool.js';
4
4
  import { addGetWixelProjectAssetsTool } from './get-wixel-project-assets-tool.js';
5
5
  import { addWixelSessionTools } from './wixel-session-tools.js';
6
+ import { addGetWixelAssetFilesTool } from './wixel-asset-files-tool.js';
6
7
  export { WIXEL_ICON_SVG_URL, WIXEL_ICON_PNG_URL } from './constants.js';
7
8
  export { addWixelSessionTools, addSendWixelMessageTool, addGetWixelTaskTool } from './wixel-session-tools.js';
8
9
  export { addGetWixelJobStatusTool } from './get-wixel-job-status-tool.js';
9
10
  export { addMarkWixelJobAsFulfilledTool } from './mark-wixel-job-as-fulfilled-tool.js';
10
11
  export { addGetWixelAssetTool } from './get-wixel-asset-tool.js';
11
12
  export { addGetWixelProjectAssetsTool } from './get-wixel-project-assets-tool.js';
13
+ export { addGetWixelAssetFilesTool } from './wixel-asset-files-tool.js';
12
14
  export function addWixelTools(server, authStrategy, options = {}) {
13
15
  addGetWixelJobStatusTool(server, authStrategy, options);
14
16
  addMarkWixelJobAsFulfilledTool(server, authStrategy, options);
@@ -16,5 +18,7 @@ export function addWixelTools(server, authStrategy, options = {}) {
16
18
  addGetWixelProjectAssetsTool(server, authStrategy, options);
17
19
  // Conversation tools over the Wixel agent's A2A endpoint
18
20
  addWixelSessionTools(server, authStrategy, options);
21
+ // Engine-ready files of an asset (sprite sheet, app-icon pack, PNGs)
22
+ addGetWixelAssetFilesTool(server, authStrategy, options);
19
23
  }
20
24
  //# sourceMappingURL=index.js.map
@@ -0,0 +1,29 @@
1
+ import { McpAuthenticationStrategy } from '../auth/index.js';
2
+ import { WixMcpServer } from '../wix-mcp-server.js';
3
+ export declare const ASSET_FILE_LAYOUTS: readonly ["files", "spriteSheet", "appIcons"];
4
+ export type AssetFileLayout = (typeof ASSET_FILE_LAYOUTS)[number];
5
+ declare const ICONS_FILE_TYPES: readonly ["png", "pngTransparentBackground", "svg", "svgTransparentBackground"];
6
+ declare const ICONS_SIZES: readonly ["16px", "24px", "32px", "64px", "128px", "256px", "512px"];
7
+ export interface AssetFilesArgs {
8
+ metaSiteId: string;
9
+ sessionId: string;
10
+ assetId?: string;
11
+ layout?: AssetFileLayout;
12
+ fileType?: (typeof ICONS_FILE_TYPES)[number];
13
+ size?: (typeof ICONS_SIZES)[number];
14
+ jobId?: string;
15
+ assetType?: string;
16
+ waitSeconds?: number;
17
+ }
18
+ /**
19
+ * The export params for an asset type. Only ICONS reads layout / fileType /
20
+ * size; other types export with their own defaults. A layout that needs an
21
+ * icon set is refused for any other type instead of silently exporting
22
+ * something else.
23
+ */
24
+ export declare function buildExportParams(assetType: string, args: Pick<AssetFilesArgs, 'layout' | 'fileType' | 'size'>): Record<string, unknown> | undefined;
25
+ export declare function addGetWixelAssetFilesTool(server: WixMcpServer, authStrategy: McpAuthenticationStrategy, options?: {
26
+ conductorOverride?: string;
27
+ }): void;
28
+ export {};
29
+ //# sourceMappingURL=wixel-asset-files-tool.d.ts.map
@@ -0,0 +1,327 @@
1
+ import { rethrowIfUnauthorized } from '../auth/index.js';
2
+ import { setWixelToolBi as setBi } from './wixel-bi.js';
3
+ import { z } from 'zod';
4
+ import { getAsset, getAssetExportState, queryAssets, startAssetExport } from './api.js';
5
+ import { buildAppIconFiles } from './app-icon-files.js';
6
+ import { getWixelSessionProjectId } from './wixel-session-entries.js';
7
+ // get_wixel_asset_files: the files of a Wixel asset, for a coding agent that
8
+ // builds something with them (a game, an app, a site). It runs the same export
9
+ // the editor's Download button runs and returns the file URL.
10
+ //
11
+ // It takes the conversation's metaSiteId + sessionId (both returned by
12
+ // send_wixel_message), so it does not depend on get_wixel_task having reported
13
+ // asset ids: without assetId it lists the project's assets to pick from.
14
+ //
15
+ // ICONS sets have two engine-ready layouts on top of the plain per-tile files:
16
+ // - spriteSheet: one aligned sheet PNG + Aseprite JSON frame map (Phaser
17
+ // createFromAseprite) + the aligned frames — for character pose sets;
18
+ // - appIcons: iOS app icon set, Android mipmaps + Play Store icon, web
19
+ // favicons + manifest — for an app or game icon.
20
+ export const ASSET_FILE_LAYOUTS = ['files', 'spriteSheet', 'appIcons'];
21
+ const ICONS_FILE_TYPES = [
22
+ 'png',
23
+ 'pngTransparentBackground',
24
+ 'svg',
25
+ 'svgTransparentBackground'
26
+ ];
27
+ const ICONS_SIZES = [
28
+ '16px',
29
+ '24px',
30
+ '32px',
31
+ '64px',
32
+ '128px',
33
+ '256px',
34
+ '512px'
35
+ ];
36
+ const POLL_INTERVAL_MS = 2_000;
37
+ const MAX_WAIT_SECONDS = 25;
38
+ /**
39
+ * The export params for an asset type. Only ICONS reads layout / fileType /
40
+ * size; other types export with their own defaults. A layout that needs an
41
+ * icon set is refused for any other type instead of silently exporting
42
+ * something else.
43
+ */
44
+ export function buildExportParams(assetType, args) {
45
+ const layout = args.layout ?? 'files';
46
+ if (assetType === 'IMAGE' && layout === 'appIcons') {
47
+ // An app icon from the app-icon skill is a full-bleed 1024 × 1024 IMAGE:
48
+ // export it as one full-size PNG; the sizes are made from it by URL.
49
+ return { format: 'png', quality: 100, scale: 1 };
50
+ }
51
+ if (assetType !== 'ICONS') {
52
+ if (layout !== 'files') {
53
+ throw new Error(layout === 'appIcons'
54
+ ? `layout "appIcons" needs an ICONS or IMAGE asset (the app icon); this asset is ${assetType}`
55
+ : `layout "${layout}" needs an ICONS asset (an icon / sprite set); this asset is ${assetType}`);
56
+ }
57
+ return undefined;
58
+ }
59
+ if (layout === 'files') {
60
+ return {
61
+ // Engines and app projects want plain transparent PNGs, not the
62
+ // editor's default SVG-wrapped PNG.
63
+ fileType: args.fileType ?? 'pngTransparentBackground',
64
+ ...(args.size ? { size: args.size } : { size: '512px' })
65
+ };
66
+ }
67
+ return {
68
+ layout,
69
+ ...(layout === 'spriteSheet' && args.size ? { size: args.size } : {})
70
+ };
71
+ }
72
+ async function waitForExport(read, waitSeconds) {
73
+ const deadline = Date.now() + waitSeconds * 1000;
74
+ let state = await read();
75
+ while (!state.complete && !state.errorDescription && Date.now() < deadline) {
76
+ await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
77
+ state = await read();
78
+ }
79
+ return state;
80
+ }
81
+ function describeContents(layout, assetType) {
82
+ if (assetType !== 'ICONS') {
83
+ return 'The file is the asset as the editor downloads it.';
84
+ }
85
+ switch (layout) {
86
+ case 'spriteSheet':
87
+ return 'The zip holds <set>-sheet.png (frames on one grid, feet on one baseline), <set>-sheet.json (Aseprite array frame map: frames "0","1"…, one frameTags entry per pose — load with Phaser this.load.aseprite + this.anims.createFromAseprite) and frames/<n>-<pose>.png.';
88
+ case 'appIcons':
89
+ return 'The zip holds ios/AppIcon.appiconset (1024 px + Contents.json, drop into Assets.xcassets), android/res/mipmap-*/ic_launcher.png + android/play-store-512.png, and web/ favicons, apple-touch-icon.png, icon-192/512.png and site.webmanifest.';
90
+ default:
91
+ return 'The zip holds one transparent PNG per tile, named after the tile.';
92
+ }
93
+ }
94
+ export function addGetWixelAssetFilesTool(server, authStrategy, options = {}) {
95
+ const { getSiteAuthHeaders } = authStrategy;
96
+ server.registerTool('get_wixel_asset_files', {
97
+ title: 'Get Wixel asset files',
98
+ description: `Download the files of an asset made in a Wixel conversation, to use them in code (a game, an app, a site). Pass the metaSiteId and sessionId that send_wixel_message returned.
99
+ - Without assetId: lists the project's assets (id, name, type) — pick one.
100
+ - With assetId: exports it and returns a zip URL. For an ICONS asset choose layout:
101
+ "files" (default) one transparent PNG per tile;
102
+ "spriteSheet" for a character's poses / animation frames: one aligned sheet + Aseprite JSON frame map (Phaser createFromAseprite) + aligned frames;
103
+ "appIcons" for an app or game icon: iOS app icon set, Android mipmaps + Play Store 512, web favicons + manifest.
104
+ An IMAGE asset (an app icon tile) also takes layout "appIcons": you get one URL per file instead of a zip.
105
+ - If the export is still running, call again with the returned jobId, assetType, layout and assetId.
106
+ Download the URL and unzip it into the project; never re-draw or re-generate assets the user already approved.`,
107
+ inputSchema: {
108
+ metaSiteId: z
109
+ .string()
110
+ .describe('metaSiteId returned by send_wixel_message'),
111
+ sessionId: z
112
+ .string()
113
+ .describe('sessionId returned by send_wixel_message'),
114
+ assetId: z
115
+ .string()
116
+ .optional()
117
+ .describe('Asset to export; omit to list the project assets'),
118
+ layout: z
119
+ .enum(ASSET_FILE_LAYOUTS)
120
+ .optional()
121
+ .describe('ICONS assets only: files (default), spriteSheet, appIcons'),
122
+ fileType: z
123
+ .enum(ICONS_FILE_TYPES)
124
+ .optional()
125
+ .describe('ICONS layout "files" only; default pngTransparentBackground'),
126
+ size: z
127
+ .enum(ICONS_SIZES)
128
+ .optional()
129
+ .describe('ICONS: tile size for "files" (default 512px), frame cell size for "spriteSheet" (default 256px)'),
130
+ jobId: z
131
+ .string()
132
+ .optional()
133
+ .describe('Continue waiting on an export this tool started'),
134
+ assetType: z
135
+ .string()
136
+ .optional()
137
+ .describe('With jobId: the assetType this tool returned'),
138
+ waitSeconds: z
139
+ .number()
140
+ .min(0)
141
+ .max(MAX_WAIT_SECONDS)
142
+ .optional()
143
+ .describe('How long to wait for the export (default 25)')
144
+ },
145
+ annotations: {
146
+ readOnlyHint: true,
147
+ destructiveHint: false,
148
+ openWorldHint: false
149
+ }
150
+ }, async (args, extra) => {
151
+ setBi(extra, { siteId: args.metaSiteId });
152
+ const siteAuthHeaders = await getSiteAuthHeaders(args.metaSiteId);
153
+ try {
154
+ const projectId = await getWixelSessionProjectId({
155
+ authHeaders: siteAuthHeaders,
156
+ conductorOverride: options.conductorOverride,
157
+ sessionId: args.sessionId
158
+ });
159
+ if (!projectId) {
160
+ throw new Error('This Wixel conversation has no project yet — make something first');
161
+ }
162
+ const base = {
163
+ siteAuthHeaders,
164
+ projectId,
165
+ conductorOverride: options.conductorOverride
166
+ };
167
+ const waitSeconds = args.waitSeconds ?? MAX_WAIT_SECONDS;
168
+ if (args.jobId) {
169
+ if (!args.assetType) {
170
+ throw new Error('Pass the assetType returned with the jobId');
171
+ }
172
+ const assetType = args.assetType;
173
+ const state = await waitForExport(() => getAssetExportState({ ...base, assetType, jobId: args.jobId }), waitSeconds);
174
+ // The app-icon manifest is named after the asset, so a follow-up
175
+ // poll that carries the assetId looks the name up again.
176
+ // Only an IMAGE app-icon pack uses the name, and a failed lookup
177
+ // must not hide an export that is already done.
178
+ const assetName = state.complete &&
179
+ args.assetId &&
180
+ assetType === 'IMAGE' &&
181
+ args.layout === 'appIcons'
182
+ ? await getAsset({ ...base, assetId: args.assetId })
183
+ .then((asset) => asset?.name)
184
+ .catch(() => undefined)
185
+ : undefined;
186
+ return exportResult(state, args, assetType, args.jobId, assetName);
187
+ }
188
+ if (!args.assetId) {
189
+ const { assets } = await queryAssets(base);
190
+ const list = (assets ?? [])
191
+ .filter((asset) => !asset.archived)
192
+ .map((asset) => ({
193
+ id: asset.id,
194
+ name: asset.name ?? null,
195
+ type: asset.type ?? null
196
+ }));
197
+ return {
198
+ isError: false,
199
+ content: [
200
+ {
201
+ type: 'text',
202
+ text: `${list.length} assets in this Wixel project:\n${list
203
+ .map((a) => `- ${a.name ?? '(unnamed)'} [${a.type}] id=${a.id}`)
204
+ .join('\n')}\nCall get_wixel_asset_files again with the assetId to download one.`
205
+ }
206
+ ],
207
+ structuredContent: { success: true, projectId, assets: list }
208
+ };
209
+ }
210
+ const asset = await getAsset({ ...base, assetId: args.assetId });
211
+ const assetType = asset?.type;
212
+ if (!assetType) {
213
+ throw new Error(`Asset ${args.assetId} was not found in this project`);
214
+ }
215
+ const params = buildExportParams(assetType, args);
216
+ const jobId = await startAssetExport({
217
+ ...base,
218
+ assetType,
219
+ assetId: args.assetId,
220
+ params
221
+ });
222
+ const state = await waitForExport(() => getAssetExportState({ ...base, assetType, jobId }), waitSeconds);
223
+ return exportResult(state, args, assetType, jobId, asset?.name);
224
+ }
225
+ catch (error) {
226
+ rethrowIfUnauthorized(error);
227
+ // Log the message and request id only: an HttpError carries the request
228
+ // config, whose headers hold the user's site token.
229
+ console.error('❌ Error getting Wixel asset files:', {
230
+ message: error.message,
231
+ requestId: error.requestId
232
+ });
233
+ const errorMessage = error.message;
234
+ return {
235
+ isError: true,
236
+ content: [{ type: 'text', text: errorMessage }],
237
+ structuredContent: { success: false, error: errorMessage }
238
+ };
239
+ }
240
+ });
241
+ }
242
+ function exportResult(state, args, assetType, jobId, assetName) {
243
+ if (state.errorDescription) {
244
+ return {
245
+ isError: true,
246
+ content: [
247
+ {
248
+ type: 'text',
249
+ text: `Export failed: ${state.errorDescription}`
250
+ }
251
+ ],
252
+ structuredContent: {
253
+ success: false,
254
+ error: state.errorDescription,
255
+ jobId,
256
+ assetType
257
+ }
258
+ };
259
+ }
260
+ // A follow-up poll carries only what this reply tells the caller to pass
261
+ // back, so the layout travels with the jobId: without it the ready reply
262
+ // would describe plain files for a sprite sheet or an app-icon pack.
263
+ const layout = args.layout ?? 'files';
264
+ if (!state.complete || !state.url) {
265
+ return {
266
+ isError: false,
267
+ content: [
268
+ {
269
+ type: 'text',
270
+ text: `Export still running${state.progress != null ? ` (${state.progress}%)` : ''}. Call get_wixel_asset_files again with jobId "${jobId}", assetType "${assetType}", layout "${layout}"${args.assetId ? ` and assetId "${args.assetId}"` : ''}.`
271
+ }
272
+ ],
273
+ structuredContent: {
274
+ success: true,
275
+ status: 'running',
276
+ progress: state.progress,
277
+ jobId,
278
+ assetType,
279
+ layout,
280
+ ...(args.assetId ? { assetId: args.assetId } : {})
281
+ }
282
+ };
283
+ }
284
+ if (assetType === 'IMAGE' && layout === 'appIcons') {
285
+ const files = buildAppIconFiles(state.url, assetName || 'App');
286
+ if (files) {
287
+ return {
288
+ isError: false,
289
+ content: [
290
+ {
291
+ type: 'text',
292
+ text: `App icon files ready — download each URL to its path, write the JSON files as given:\n${files
293
+ .map((f) => `- ${f.path}: ${f.url ?? f.content}`)
294
+ .join('\n')}`
295
+ }
296
+ ],
297
+ structuredContent: {
298
+ success: true,
299
+ status: 'ready',
300
+ url: state.url,
301
+ files,
302
+ jobId,
303
+ assetType,
304
+ layout
305
+ }
306
+ };
307
+ }
308
+ }
309
+ return {
310
+ isError: false,
311
+ content: [
312
+ {
313
+ type: 'text',
314
+ text: `Files ready: ${state.url}\n${describeContents(args.layout, assetType)}`
315
+ }
316
+ ],
317
+ structuredContent: {
318
+ success: true,
319
+ status: 'ready',
320
+ url: state.url,
321
+ jobId,
322
+ assetType,
323
+ layout: args.layout ?? 'files'
324
+ }
325
+ };
326
+ }
327
+ //# sourceMappingURL=wixel-asset-files-tool.js.map
@@ -50,6 +50,28 @@ export interface WixelPendingQuestion {
50
50
  toolCallId: string;
51
51
  turnId?: string;
52
52
  questions: WixelAskUserQuestion[];
53
+ /** Pictures the asking turn made before it asked — the sample a "make the
54
+ * rest in this look?" question is about. Empty when the turn made none. */
55
+ previewUrls?: string[];
56
+ }
57
+ /**
58
+ * The pictures the turn made before its question, newest last: the image
59
+ * tools' results in that turn up to the ask_user. A client that cannot render
60
+ * the stage (Claude Code, Cursor) otherwise approves a look it never sees.
61
+ */
62
+ export declare function collectPreviewUrls(messages: SessionEntryMessage[], askIndex: number, turnId: string | undefined): string[];
63
+ interface SessionEntryPart {
64
+ text?: string;
65
+ functionCall?: {
66
+ id?: string;
67
+ name?: string;
68
+ args?: Record<string, unknown>;
69
+ };
70
+ functionResponse?: {
71
+ id?: string;
72
+ name?: string;
73
+ response?: unknown;
74
+ };
53
75
  }
54
76
  export interface WixelSessionActivity {
55
77
  /** Unanswered ask_user the turn is suspended on (nothing will generate
@@ -66,6 +88,12 @@ export interface WixelSessionActivity {
66
88
  * alone cannot distinguish the two.
67
89
  */
68
90
  export declare function getWixelSessionActivity({ sessionId, ...base }: GetSessionEntriesOptions): Promise<WixelSessionActivity>;
91
+ interface SessionEntryMessage {
92
+ id?: string;
93
+ role?: string;
94
+ turnId?: string;
95
+ parts?: SessionEntryPart[];
96
+ }
69
97
  export declare function getWixelPendingQuestion({ sessionId, requireParked, turnId, ...base }: GetSessionEntriesOptions & {
70
98
  requireParked?: boolean;
71
99
  /** The asking task's turn. A question another turn of the session asked —
@@ -1,5 +1,6 @@
1
1
  import { createHttpClient } from '@wix/http-client';
2
2
  import { WIXEL_AGENT_SESSIONS_API_BASE_URL } from './constants.js';
3
+ import { extractMediaUrls } from './wixel-a2a-client.js';
3
4
  // Reads of the Wixel agent's internal sessions API (wixel-agent-service):
4
5
  // the durable entries log of a session and the sessions listing. A2A tasks
5
6
  // report a turn's state and reply, but a parked ask_user question — the
@@ -41,6 +42,36 @@ export async function getWixelSessionProjectId({ sessionId, ...base }) {
41
42
  const session = data?.sessions?.find((entry) => entry.id === sessionId);
42
43
  return session?.projectId ?? null;
43
44
  }
45
+ /** Image tools whose results are the pictures a question can be about. */
46
+ const PREVIEW_TOOLS = new Set([
47
+ 'generate_image',
48
+ 'edit_image',
49
+ 'remove_image_background'
50
+ ]);
51
+ const MAX_PREVIEW_URLS = 4;
52
+ /**
53
+ * The pictures the turn made before its question, newest last: the image
54
+ * tools' results in that turn up to the ask_user. A client that cannot render
55
+ * the stage (Claude Code, Cursor) otherwise approves a look it never sees.
56
+ */
57
+ export function collectPreviewUrls(messages, askIndex, turnId) {
58
+ const urls = [];
59
+ for (let i = 0; i <= askIndex; i++) {
60
+ const message = messages[i];
61
+ if (turnId && message.turnId !== turnId)
62
+ continue;
63
+ for (const part of message.parts ?? []) {
64
+ const response = part.functionResponse;
65
+ if (!response?.name || !PREVIEW_TOOLS.has(response.name))
66
+ continue;
67
+ for (const url of extractMediaUrls(JSON.stringify(response.response ?? ''))) {
68
+ if (!urls.includes(url))
69
+ urls.push(url);
70
+ }
71
+ }
72
+ }
73
+ return urls.slice(-MAX_PREVIEW_URLS);
74
+ }
44
75
  /**
45
76
  * Classify the session's current state from the durable entries log with a
46
77
  * single read: parked on an ask_user question, actively generating, or idle.
@@ -169,10 +200,12 @@ export async function getWixelPendingQuestion({ sessionId, requireParked = false
169
200
  continue;
170
201
  if (ownTurn && message.turnId !== ownTurn)
171
202
  return null;
203
+ const previewUrls = collectPreviewUrls(messages, i, message.turnId);
172
204
  return {
173
205
  toolCallId: call.id,
174
206
  turnId: message.turnId,
175
- questions
207
+ questions,
208
+ ...(previewUrls.length ? { previewUrls } : {})
176
209
  };
177
210
  }
178
211
  if (part.text || part.functionCall || part.functionResponse) {