@wix/mcp 1.0.93 → 1.0.95
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/build/bin-standalone.js +1200 -455
- package/build/cjs/index.cjs +3237 -1264
- package/build/cjs/toolkit.cjs +2177 -469
- package/build/dts/api-call/headless-release.d.ts +56 -0
- package/build/dts/api-call/headless-release.js +135 -0
- package/build/dts/api-call/index.d.ts +9 -1
- package/build/dts/api-call/index.js +19 -10
- package/build/dts/api-call/site-files.d.ts +42 -0
- package/build/dts/api-call/site-files.js +290 -0
- package/build/dts/api-call/upload-headless-website-files.d.ts +118 -0
- package/build/dts/api-call/upload-headless-website-files.js +624 -0
- package/build/dts/api-call/upload-image-tool.d.ts +2 -0
- package/build/dts/api-call/upload-image-tool.js +8 -4
- package/build/dts/bin.d.ts +1 -1
- package/build/dts/bin.js +4 -5
- package/build/dts/code-mode/attachments.js +1 -1
- package/build/dts/code-mode/index.d.ts +6 -1
- package/build/dts/code-mode/index.js +47 -13
- package/build/dts/code-mode/truncate-response.js +5 -1
- package/build/dts/config/default-config.d.ts +35 -9
- package/build/dts/config/default-config.js +348 -207
- package/build/dts/config/file-params.d.ts +22 -0
- package/build/dts/config/file-params.js +24 -0
- package/build/dts/config/param-descriptions.d.ts +14 -2
- package/build/dts/config/param-descriptions.js +34 -15
- package/build/dts/constants.d.ts +1 -0
- package/build/dts/constants.js +1 -0
- package/build/dts/docs/docs.d.ts +16 -3
- package/build/dts/docs/docs.js +211 -39
- package/build/dts/docs/fetch-article.d.ts +21 -0
- package/build/dts/docs/fetch-article.js +70 -5
- package/build/dts/docs/get-to-know.d.ts +7 -2
- package/build/dts/docs/get-to-know.js +37 -15
- package/build/dts/docs/mintlify-docs.d.ts +121 -0
- package/build/dts/docs/mintlify-docs.js +391 -0
- package/build/dts/docs/semanticSearch.d.ts +2 -1
- package/build/dts/docs/semanticSearch.js +14 -7
- package/build/dts/docs/start-deploy-website.d.ts +7 -6
- package/build/dts/docs/start-deploy-website.js +13 -8
- package/build/dts/docs/start-headless-build.d.ts +17 -0
- package/build/dts/docs/start-headless-build.js +60 -0
- package/build/dts/graphql/index.js +3 -1
- package/build/dts/import-claude-design-tool/index.js +2 -2
- package/build/dts/index.d.ts +3 -2
- package/build/dts/index.js +3 -2
- package/build/dts/infra/redact-personal-data.d.ts +4 -0
- package/build/dts/infra/redact-personal-data.js +79 -0
- package/build/dts/site-widget-tools/constants.d.ts +49 -1
- package/build/dts/site-widget-tools/constants.js +64 -7
- package/build/dts/site-widget-tools/index.d.ts +1 -0
- package/build/dts/site-widget-tools/index.js +1 -0
- package/build/dts/site-widget-tools/pick-images-tool/index.d.ts +14 -0
- package/build/dts/site-widget-tools/pick-images-tool/index.js +138 -0
- package/build/dts/site-widget-tools/search-site-templates-tool/index.js +33 -9
- package/build/dts/site-widget-tools/site-builder-tool/index.js +22 -2
- package/build/dts/site-widget-tools/site-creation-tool/index.d.ts +7 -1
- package/build/dts/site-widget-tools/site-creation-tool/index.js +22 -51
- package/build/dts/site-widget-tools/suggested-domain-tool/index.js +6 -3
- package/build/dts/support/index.d.ts +4 -0
- package/build/dts/support/index.js +13 -7
- package/build/dts/toolkit-entry.d.cts +19 -12
- package/build/dts/toolkit-entry.d.ts +19 -12
- package/build/dts/toolkit.d.ts +2 -2
- package/build/dts/toolkit.js +23 -7
- package/build/dts/wix-mcp-server.d.ts +11 -0
- package/build/dts/wix-mcp-server.js +28 -6
- package/build/dts/wixel/constants.d.ts +8 -1
- package/build/dts/wixel/constants.js +8 -1
- package/build/dts/wixel/index.d.ts +1 -1
- package/build/dts/wixel/index.js +2 -2
- package/build/dts/wixel/wixel-a2a-client.d.ts +83 -0
- package/build/dts/wixel/wixel-a2a-client.js +166 -0
- package/build/dts/wixel/wixel-bi.d.ts +2 -1
- package/build/dts/wixel/wixel-bi.js +2 -1
- package/build/dts/wixel/{external-agent-api.d.ts → wixel-session-entries.d.ts} +11 -33
- package/build/dts/wixel/{external-agent-api.js → wixel-session-entries.js} +33 -54
- package/build/dts/wixel/wixel-session-tools.d.ts +21 -26
- package/build/dts/wixel/wixel-session-tools.js +494 -599
- package/build/esm/index.js +3335 -1376
- package/build/esm/toolkit.js +2273 -565
- package/package.json +4 -4
- package/build/dts/docs/create-site-guide.d.ts +0 -3
- package/build/dts/docs/create-site-guide.js +0 -109
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A Wix Astro build among the files UploadHeadlessWebsiteFiles received (the output
|
|
3
|
+
* of `astro build` with the Wix hosting adapter, zipped from the project root):
|
|
4
|
+
* recognised here, released by headless-business-setup's bundle routes (the tool
|
|
5
|
+
* PUTs the zip and calls `release-bundle`), not dropped as static files.
|
|
6
|
+
*
|
|
7
|
+
* The build is recognised by `.wix/build-metadata.json`, which the adapter writes
|
|
8
|
+
* beside `dist/`: it names the client folder (served as static files), the server
|
|
9
|
+
* folder (run on the hosting runtime) and the app manifest. The folder that holds
|
|
10
|
+
* `.wix/` is the project root; the project's other files there are its source.
|
|
11
|
+
*
|
|
12
|
+
* Runs in the MCP server (a Cloudflare Worker): no `node:` imports, only web APIs.
|
|
13
|
+
*/
|
|
14
|
+
import type { SiteFile } from './site-files.js';
|
|
15
|
+
export declare const BUILD_METADATA_SUFFIX = ".wix/build-metadata.json";
|
|
16
|
+
export interface BuildMetadata {
|
|
17
|
+
appManifestPath: string;
|
|
18
|
+
clientDir?: string;
|
|
19
|
+
serverDir?: string;
|
|
20
|
+
staticsUrlPlaceholder?: string;
|
|
21
|
+
serverUrlPlaceholder?: string;
|
|
22
|
+
generateAppDeploymentGroupId?: boolean;
|
|
23
|
+
}
|
|
24
|
+
export interface HeadlessBuild {
|
|
25
|
+
metadata: BuildMetadata;
|
|
26
|
+
/** The project root in the upload: `''`, or the one folder that wraps the project (`proj/`). */
|
|
27
|
+
root: string;
|
|
28
|
+
/** The manifest as the build wrote it (`{ appId, components }`). */
|
|
29
|
+
manifestText: string;
|
|
30
|
+
clientFiles: SiteFile[];
|
|
31
|
+
serverFiles: SiteFile[];
|
|
32
|
+
/** The client folder in the upload (`dist/client/`), when the metadata names one. */
|
|
33
|
+
clientDir?: string;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* The upload without the build's empty static files: the hosting refuses a static file
|
|
37
|
+
* of size 0 (`size got 0, expected 1 or more`), and an empty file serves nothing.
|
|
38
|
+
*/
|
|
39
|
+
export declare function withoutEmptyStatics(files: SiteFile[], build: HeadlessBuild): {
|
|
40
|
+
files: SiteFile[];
|
|
41
|
+
dropped: string[];
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* Recognise a Wix Astro build among the uploaded files: the file set holds
|
|
45
|
+
* `.wix/build-metadata.json` (at the root, or under one folder that wraps the project).
|
|
46
|
+
* Returns null for a static site, so the caller drops it as before. Throws an agent-facing
|
|
47
|
+
* error for a build whose metadata or files are not usable.
|
|
48
|
+
*/
|
|
49
|
+
export declare function findHeadlessBuild(files: SiteFile[]): HeadlessBuild | null;
|
|
50
|
+
/**
|
|
51
|
+
* The project's source among the uploaded files: every file under the build's
|
|
52
|
+
* project root outside `dist/`, `.wix/`, `.astro/`, `.agents/`, `node_modules/` and
|
|
53
|
+
* `.env*`, with paths relative to the root. Empty when only the build was uploaded.
|
|
54
|
+
*/
|
|
55
|
+
export declare function buildSourceFiles(files: SiteFile[], build: HeadlessBuild): SiteFile[];
|
|
56
|
+
//# sourceMappingURL=headless-release.d.ts.map
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
export const BUILD_METADATA_SUFFIX = '.wix/build-metadata.json';
|
|
2
|
+
/**
|
|
3
|
+
* The upload without the build's empty static files: the hosting refuses a static file
|
|
4
|
+
* of size 0 (`size got 0, expected 1 or more`), and an empty file serves nothing.
|
|
5
|
+
*/
|
|
6
|
+
export function withoutEmptyStatics(files, build) {
|
|
7
|
+
const dir = build.clientDir;
|
|
8
|
+
if (dir === undefined)
|
|
9
|
+
return { files, dropped: [] };
|
|
10
|
+
const dropped = [];
|
|
11
|
+
const kept = files.filter((f) => {
|
|
12
|
+
const path = normalizePath(f.path);
|
|
13
|
+
if (f.bytes.byteLength === 0 && path.startsWith(dir)) {
|
|
14
|
+
dropped.push(path.slice(dir.length));
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
return true;
|
|
18
|
+
});
|
|
19
|
+
return { files: kept, dropped };
|
|
20
|
+
}
|
|
21
|
+
const normalizePath = (p) => p.replace(/\\/g, '/').replace(/^\.?\//, '');
|
|
22
|
+
/**
|
|
23
|
+
* Recognise a Wix Astro build among the uploaded files: the file set holds
|
|
24
|
+
* `.wix/build-metadata.json` (at the root, or under one folder that wraps the project).
|
|
25
|
+
* Returns null for a static site, so the caller drops it as before. Throws an agent-facing
|
|
26
|
+
* error for a build whose metadata or files are not usable.
|
|
27
|
+
*/
|
|
28
|
+
export function findHeadlessBuild(files) {
|
|
29
|
+
const entries = new Map(files.map((f) => [normalizePath(f.path), f.bytes]));
|
|
30
|
+
const metadataEntry = [...entries.keys()].find((name) => name === BUILD_METADATA_SUFFIX ||
|
|
31
|
+
name.endsWith(`/${BUILD_METADATA_SUFFIX}`));
|
|
32
|
+
if (metadataEntry === undefined)
|
|
33
|
+
return null;
|
|
34
|
+
const zipRoot = metadataEntry.slice(0, metadataEntry.length - BUILD_METADATA_SUFFIX.length);
|
|
35
|
+
let metadata;
|
|
36
|
+
try {
|
|
37
|
+
metadata = JSON.parse(new TextDecoder().decode(entries.get(metadataEntry)));
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
throw new Error(`${BUILD_METADATA_SUFFIX} is not valid JSON.`);
|
|
41
|
+
}
|
|
42
|
+
if (typeof metadata.appManifestPath !== 'string') {
|
|
43
|
+
throw new Error(`${BUILD_METADATA_SUFFIX} has no appManifestPath; send the build as the Wix hosting adapter wrote it (.wix/ and dist/ zipped from the project root).`);
|
|
44
|
+
}
|
|
45
|
+
// The metadata holds the builder's absolute paths; the files in the zip are relative to the
|
|
46
|
+
// project root. The manifest is found by the longest tail of its path that exists, and the
|
|
47
|
+
// part of the path before that tail is the builder's project root.
|
|
48
|
+
const manifestPath = resolveZipPath(metadata.appManifestPath, zipRoot, entries);
|
|
49
|
+
if (manifestPath === undefined) {
|
|
50
|
+
throw new Error(`The build's app manifest (${metadata.appManifestPath}) is not in the upload; zip .wix/ and dist/ together from the project root after \`astro build\`.`);
|
|
51
|
+
}
|
|
52
|
+
const builderRoot = metadata.appManifestPath.slice(0, metadata.appManifestPath.length - (manifestPath.length - zipRoot.length));
|
|
53
|
+
const clientDir = metadata.clientDir === undefined
|
|
54
|
+
? undefined
|
|
55
|
+
: toZipDir(metadata.clientDir, builderRoot, zipRoot);
|
|
56
|
+
const serverDir = metadata.serverDir === undefined
|
|
57
|
+
? undefined
|
|
58
|
+
: toZipDir(metadata.serverDir, builderRoot, zipRoot);
|
|
59
|
+
const clientFiles = [];
|
|
60
|
+
const serverFiles = [];
|
|
61
|
+
for (const [name, bytes] of entries) {
|
|
62
|
+
if (name === manifestPath)
|
|
63
|
+
continue;
|
|
64
|
+
if (serverDir !== undefined && name.startsWith(serverDir)) {
|
|
65
|
+
serverFiles.push({ path: name.slice(serverDir.length), bytes });
|
|
66
|
+
}
|
|
67
|
+
else if (clientDir !== undefined && name.startsWith(clientDir)) {
|
|
68
|
+
clientFiles.push({ path: name.slice(clientDir.length), bytes });
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (clientFiles.length + serverFiles.length === 0) {
|
|
72
|
+
throw new Error('The build has no client or server output files; run `astro build` first and zip .wix/ and dist/ from the project root.');
|
|
73
|
+
}
|
|
74
|
+
if (serverFiles.length > 0 &&
|
|
75
|
+
!serverFiles.some((f) => f.path === 'entry.mjs')) {
|
|
76
|
+
throw new Error("The server build has no entry.mjs, which the Wix hosting adapter (@wix/astro-wix-hosting-adapter) writes into the server folder on every build. Build with the project's own `astro build` (the adapter in astro.config) and zip .wix/ and dist/ from the project root as the build left them.");
|
|
77
|
+
}
|
|
78
|
+
const manifestText = new TextDecoder().decode(entries.get(manifestPath));
|
|
79
|
+
return {
|
|
80
|
+
metadata,
|
|
81
|
+
root: zipRoot,
|
|
82
|
+
manifestText,
|
|
83
|
+
clientFiles,
|
|
84
|
+
serverFiles,
|
|
85
|
+
clientDir
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
// What the source zip leaves out, relative to the project root: the build output, the
|
|
89
|
+
// adapter's and Astro's caches, the agent's own folder at the top; dependencies and
|
|
90
|
+
// environment files at any depth.
|
|
91
|
+
const NOT_SOURCE_TOP = new Set(['dist', '.wix', '.astro', '.agents']);
|
|
92
|
+
const isSourcePath = (relative) => {
|
|
93
|
+
const segments = relative.split('/');
|
|
94
|
+
return (!NOT_SOURCE_TOP.has(segments[0]) &&
|
|
95
|
+
!segments.some((s) => s === 'node_modules' || s.startsWith('.env')));
|
|
96
|
+
};
|
|
97
|
+
/**
|
|
98
|
+
* The project's source among the uploaded files: every file under the build's
|
|
99
|
+
* project root outside `dist/`, `.wix/`, `.astro/`, `.agents/`, `node_modules/` and
|
|
100
|
+
* `.env*`, with paths relative to the root. Empty when only the build was uploaded.
|
|
101
|
+
*/
|
|
102
|
+
export function buildSourceFiles(files, build) {
|
|
103
|
+
return files.flatMap((f) => {
|
|
104
|
+
const path = normalizePath(f.path);
|
|
105
|
+
if (!path.startsWith(build.root))
|
|
106
|
+
return [];
|
|
107
|
+
const relative = path.slice(build.root.length);
|
|
108
|
+
return relative && isSourcePath(relative)
|
|
109
|
+
? [{ path: relative, bytes: f.bytes }]
|
|
110
|
+
: [];
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
function resolveZipPath(builderPath, zipRoot, entries) {
|
|
114
|
+
const segments = normalizePath(builderPath)
|
|
115
|
+
.split('/')
|
|
116
|
+
.filter((s) => s.length > 0);
|
|
117
|
+
for (let k = 0; k < segments.length; k++) {
|
|
118
|
+
const candidate = zipRoot + segments.slice(k).join('/');
|
|
119
|
+
if (entries.has(candidate))
|
|
120
|
+
return candidate;
|
|
121
|
+
}
|
|
122
|
+
return undefined;
|
|
123
|
+
}
|
|
124
|
+
function toZipDir(builderDir, builderRoot, zipRoot) {
|
|
125
|
+
const normalized = normalizePath(builderDir);
|
|
126
|
+
const normalizedRoot = normalizePath(builderRoot);
|
|
127
|
+
const relative = builderDir.startsWith(builderRoot)
|
|
128
|
+
? builderDir.slice(builderRoot.length)
|
|
129
|
+
: normalized.startsWith(normalizedRoot)
|
|
130
|
+
? normalized.slice(normalizedRoot.length)
|
|
131
|
+
: normalized;
|
|
132
|
+
const dir = zipRoot + normalizePath(relative);
|
|
133
|
+
return dir.endsWith('/') ? dir : `${dir}/`;
|
|
134
|
+
}
|
|
135
|
+
//# sourceMappingURL=headless-release.js.map
|
|
@@ -6,5 +6,13 @@ export interface ToolHintsConfig {
|
|
|
6
6
|
openWorldHint?: boolean;
|
|
7
7
|
}
|
|
8
8
|
export { addUploadImageTool } from './upload-image-tool.js';
|
|
9
|
-
export declare function addApiCallTool(server: WixMcpServer, strategy: McpAuthenticationStrategy, allowedTools?: string[], disableTools?: string[], enableTools?: string[], toolDescriptions?: Record<string, string>, toolHints?: Record<string, ToolHintsConfig>, maxResponseSize?: number
|
|
9
|
+
export declare function addApiCallTool(server: WixMcpServer, strategy: McpAuthenticationStrategy, allowedTools?: string[], disableTools?: string[], enableTools?: string[], toolDescriptions?: Record<string, string>, toolHints?: Record<string, ToolHintsConfig>, maxResponseSize?: number, options?: {
|
|
10
|
+
imagePickerToolName?: string;
|
|
11
|
+
/**
|
|
12
|
+
* This server serves ExecuteWixAPI. Only then does CallWixSiteAPI tell the user to reconnect when
|
|
13
|
+
* the client lists no ExecuteWixAPI (an old or cached tool list); where it is off on purpose, the
|
|
14
|
+
* agent just uses CallWixSiteAPI.
|
|
15
|
+
*/
|
|
16
|
+
executeWixAPIServed?: boolean;
|
|
17
|
+
}): void;
|
|
10
18
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -5,8 +5,8 @@ import { handleWixAPIResponse } from '../tool-utils.js';
|
|
|
5
5
|
import { rethrowIfUnauthorized } from '../auth/index.js';
|
|
6
6
|
import { isApiUrlAllowed } from './validation.js';
|
|
7
7
|
import { addUploadImageTool } from './upload-image-tool.js';
|
|
8
|
-
import { defaultToolDescriptions } from '../config/default-config.js';
|
|
9
|
-
import { desc } from '../config/param-descriptions.js';
|
|
8
|
+
import { CALL_WIX_SITE_API_NO_EXECUTE_LINE, CALL_WIX_SITE_API_RECONNECT_LINE, defaultToolDescriptions } from '../config/default-config.js';
|
|
9
|
+
import { desc, WHY_NOT_EXECUTE_NO_EXECUTE, WHY_NOT_EXECUTE_RECONNECT } from '../config/param-descriptions.js';
|
|
10
10
|
import { MAX_ALLOWED_RESPONSE_CHARS } from '../infra/response-size-limits.js';
|
|
11
11
|
export { addUploadImageTool } from './upload-image-tool.js';
|
|
12
12
|
const DEFAULT_MAX_RESPONSE_SIZE = 50000; // ~50KB cap
|
|
@@ -56,7 +56,8 @@ function formatApiResponse(responseData, maxResponseSize = DEFAULT_MAX_RESPONSE_
|
|
|
56
56
|
}
|
|
57
57
|
return { text: responseText, isError: false, originalLength, returnedRows };
|
|
58
58
|
}
|
|
59
|
-
export function addApiCallTool(server, strategy, allowedTools = ['CallWixSiteAPI', 'ListWixSites', 'ManageWixSite'], disableTools = [], enableTools = [], toolDescriptions, toolHints, maxResponseSize) {
|
|
59
|
+
export function addApiCallTool(server, strategy, allowedTools = ['CallWixSiteAPI', 'ListWixSites', 'ManageWixSite'], disableTools = [], enableTools = [], toolDescriptions, toolHints, maxResponseSize, options) {
|
|
60
|
+
const executeAdvice = (text, reconnect, plain) => options?.executeWixAPIServed ? text : text.replace(reconnect, plain);
|
|
60
61
|
// Compute effective allowed tools: (default + enableTools) - disableTools
|
|
61
62
|
const effectiveAllowedTools = [
|
|
62
63
|
...new Set([...allowedTools, ...enableTools])
|
|
@@ -71,7 +72,7 @@ export function addApiCallTool(server, strategy, allowedTools = ['CallWixSiteAPI
|
|
|
71
72
|
const getDescription = (toolName, defaultDesc) => toolDescriptions?.[toolName] ?? defaultDesc;
|
|
72
73
|
if (effectiveAllowedTools.includes('CallWixSiteAPI') &&
|
|
73
74
|
!disableTools.includes('CallWixSiteAPI')) {
|
|
74
|
-
server.tool('CallWixSiteAPI', getDescription('CallWixSiteAPI', defaultToolDescriptions.CallWixSiteAPI), (() => {
|
|
75
|
+
server.tool('CallWixSiteAPI', executeAdvice(getDescription('CallWixSiteAPI', defaultToolDescriptions.CallWixSiteAPI), CALL_WIX_SITE_API_RECONNECT_LINE, CALL_WIX_SITE_API_NO_EXECUTE_LINE), (() => {
|
|
75
76
|
const d = desc('CallWixSiteAPI');
|
|
76
77
|
return {
|
|
77
78
|
siteId: z.string().describe(d.siteId),
|
|
@@ -82,14 +83,21 @@ export function addApiCallTool(server, strategy, allowedTools = ['CallWixSiteAPI
|
|
|
82
83
|
.optional()
|
|
83
84
|
.describe(d.body),
|
|
84
85
|
reason: z.string().describe(d.reason),
|
|
86
|
+
whyNotExecuteWixAPI: z
|
|
87
|
+
.string()
|
|
88
|
+
.optional()
|
|
89
|
+
.describe(executeAdvice(d.whyNotExecuteWixAPI, WHY_NOT_EXECUTE_RECONNECT, WHY_NOT_EXECUTE_NO_EXECUTE)),
|
|
85
90
|
sourceDocUrl: z.string().describe(d.sourceDocUrl)
|
|
86
91
|
};
|
|
87
|
-
})(),
|
|
92
|
+
})(),
|
|
93
|
+
// openWorldHint true: a free-form URL into the whole Wix REST surface is an
|
|
94
|
+
// open-ended target, even though auth is bounded to the user's account.
|
|
95
|
+
getHints('CallWixSiteAPI', {
|
|
88
96
|
readOnlyHint: false,
|
|
89
97
|
destructiveHint: true,
|
|
90
|
-
openWorldHint:
|
|
91
|
-
}), async ({ url, body, method, siteId, reason, sourceDocUrl }, { httpClient, setToolBiParams }) => {
|
|
92
|
-
logger.log(`[CallWixSiteAPI] siteId=${siteId}, method=${method}, url=${url}, sourceDocUrl=${sourceDocUrl}, reason="${reason}", body=${JSON.stringify(body)}`);
|
|
98
|
+
openWorldHint: true
|
|
99
|
+
}), async ({ url, body, method, siteId, reason, whyNotExecuteWixAPI, sourceDocUrl }, { httpClient, setToolBiParams }) => {
|
|
100
|
+
logger.log(`[CallWixSiteAPI] siteId=${siteId}, method=${method}, url=${url}, sourceDocUrl=${sourceDocUrl}, reason="${reason}", whyNotExecuteWixAPI="${whyNotExecuteWixAPI ?? ''}", body=${JSON.stringify(body)}`);
|
|
93
101
|
const requestHeaders = {
|
|
94
102
|
// Ask the Wix API edge for JSON error bodies (it serves a branded HTML
|
|
95
103
|
// error page on 4xx otherwise); */* keeps non-JSON responses working.
|
|
@@ -255,7 +263,7 @@ export function addApiCallTool(server, strategy, allowedTools = ['CallWixSiteAPI
|
|
|
255
263
|
})(), getHints('ManageWixSite', {
|
|
256
264
|
readOnlyHint: false,
|
|
257
265
|
destructiveHint: true,
|
|
258
|
-
openWorldHint:
|
|
266
|
+
openWorldHint: true
|
|
259
267
|
}), async ({ url, body, method, reason }, { httpClient, setToolBiParams }) => {
|
|
260
268
|
logger.log(`Calling Wix Account level API: ${url}, reason="${reason}", body: ${JSON.stringify(body)}`);
|
|
261
269
|
const { isAllowed, error: urlError } = isApiUrlAllowed(url);
|
|
@@ -325,7 +333,8 @@ export function addApiCallTool(server, strategy, allowedTools = ['CallWixSiteAPI
|
|
|
325
333
|
!disableTools.includes('UploadImageToWixSite')) {
|
|
326
334
|
addUploadImageTool(server, strategy, {
|
|
327
335
|
description: getDescription('UploadImageToWixSite', ''),
|
|
328
|
-
hints: toolHints?.['UploadImageToWixSite']
|
|
336
|
+
hints: toolHints?.['UploadImageToWixSite'],
|
|
337
|
+
imagePickerToolName: options?.imagePickerToolName
|
|
329
338
|
});
|
|
330
339
|
}
|
|
331
340
|
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Site files for UploadHeadlessWebsiteFiles, handled directly in the tool (no sandbox):
|
|
3
|
+
* the `=== FILE: … ===` text bundle, zip extraction, site-shape checks and the
|
|
4
|
+
* multipart body for the drop endpoint. Same formats the code-mode runtime
|
|
5
|
+
* accepts, so an agent can switch between ExecuteWixAPI and this tool freely.
|
|
6
|
+
*/
|
|
7
|
+
export interface SiteFile {
|
|
8
|
+
path: string;
|
|
9
|
+
bytes: Uint8Array;
|
|
10
|
+
}
|
|
11
|
+
export declare const MAX_FILES_BUNDLE_CHARS = 4000000;
|
|
12
|
+
export declare const MAX_UNZIP_BYTES: number;
|
|
13
|
+
export declare const MAX_SITE_FILES = 2000;
|
|
14
|
+
export declare function base64ToBytes(b64: string): Uint8Array;
|
|
15
|
+
/** Parse the `=== FILE: <path> ===` / `=== FILE: <path> base64 ===` bundle into bytes. */
|
|
16
|
+
export declare function parseFileBundle(bundle: string | undefined): SiteFile[];
|
|
17
|
+
/** Unzip (stored + deflate entries) into raw bytes. Validates the EOCD and the declared sizes. */
|
|
18
|
+
export declare function unzip(z: Uint8Array): Promise<SiteFile[]>;
|
|
19
|
+
export declare function crc32(bytes: Uint8Array): number;
|
|
20
|
+
/** A zip of the files, every entry stored (no compression), names in UTF-8. No zip64: under 4 GB and 65,535 entries. */
|
|
21
|
+
export declare function zipStored(files: SiteFile[]): Uint8Array;
|
|
22
|
+
export declare const isZip: (f: SiteFile) => boolean;
|
|
23
|
+
/**
|
|
24
|
+
* The file set as uploaded: zips unpacked, paths cleaned, junk dropped, within the file
|
|
25
|
+
* count and the byte budget for the whole upload. No site-shape rule yet: a Wix Astro
|
|
26
|
+
* build is told apart from a static site on this list.
|
|
27
|
+
*/
|
|
28
|
+
export declare function expandSiteFiles(input: SiteFile[]): Promise<SiteFile[]>;
|
|
29
|
+
/**
|
|
30
|
+
* Turn the raw inputs into the file set to drop: unpack zips, drop junk, strip a
|
|
31
|
+
* single wrapping folder, require a root index.html. Throws an agent-facing error.
|
|
32
|
+
*/
|
|
33
|
+
export declare function normalizeSiteFiles(input: SiteFile[]): Promise<SiteFile[]>;
|
|
34
|
+
/**
|
|
35
|
+
* The static-site rules on a list `expandSiteFiles` already made (its zips are not
|
|
36
|
+
* unpacked again): strip a single wrapping folder, require a root index.html.
|
|
37
|
+
*/
|
|
38
|
+
export declare function staticSiteFiles(expanded: SiteFile[]): SiteFile[];
|
|
39
|
+
export declare function guessContentType(path: string): string;
|
|
40
|
+
/** The drop endpoint's body: one `files` part per file, the path as the filename. */
|
|
41
|
+
export declare function buildFormData(files: SiteFile[]): FormData;
|
|
42
|
+
//# sourceMappingURL=site-files.d.ts.map
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Site files for UploadHeadlessWebsiteFiles, handled directly in the tool (no sandbox):
|
|
3
|
+
* the `=== FILE: … ===` text bundle, zip extraction, site-shape checks and the
|
|
4
|
+
* multipart body for the drop endpoint. Same formats the code-mode runtime
|
|
5
|
+
* accepts, so an agent can switch between ExecuteWixAPI and this tool freely.
|
|
6
|
+
*/
|
|
7
|
+
// Keep the bundle to text + small base64 assets; large media belongs behind a URL.
|
|
8
|
+
export const MAX_FILES_BUNDLE_CHARS = 4_000_000;
|
|
9
|
+
// A dropped site is capped at 20 MB; anything expanding near this is a bomb.
|
|
10
|
+
export const MAX_UNZIP_BYTES = 64 * 1024 * 1024;
|
|
11
|
+
export const MAX_SITE_FILES = 2_000;
|
|
12
|
+
const textEncoder = new TextEncoder();
|
|
13
|
+
export function base64ToBytes(b64) {
|
|
14
|
+
const clean = b64.replace(/^data:[^,]*,/, '').replace(/\s+/g, '');
|
|
15
|
+
return Uint8Array.from(atob(clean), (c) => c.charCodeAt(0));
|
|
16
|
+
}
|
|
17
|
+
/** Parse the `=== FILE: <path> ===` / `=== FILE: <path> base64 ===` bundle into bytes. */
|
|
18
|
+
export function parseFileBundle(bundle) {
|
|
19
|
+
if (!bundle || !bundle.trim())
|
|
20
|
+
return [];
|
|
21
|
+
if (bundle.length > MAX_FILES_BUNDLE_CHARS) {
|
|
22
|
+
throw new Error(`files bundle is too large (${bundle.length} chars; max ${MAX_FILES_BUNDLE_CHARS}). Keep it to text and small base64 assets; pass larger files through attachments or reference media by URL.`);
|
|
23
|
+
}
|
|
24
|
+
const delimiter = /^=== FILE: (.+?)(?: (base64))? ===[ \t]*$/gm;
|
|
25
|
+
const matches = [...bundle.matchAll(delimiter)];
|
|
26
|
+
if (matches.length === 0) {
|
|
27
|
+
throw new Error('files bundle has no `=== FILE: <path> ===` sections. Start each file with that line (add ` base64` before the closing === for binary content).');
|
|
28
|
+
}
|
|
29
|
+
return matches.map((match, i) => {
|
|
30
|
+
const path = match[1].trim();
|
|
31
|
+
const start = (match.index ?? 0) + match[0].length;
|
|
32
|
+
const end = i + 1 < matches.length
|
|
33
|
+
? (matches[i + 1].index ?? bundle.length)
|
|
34
|
+
: bundle.length;
|
|
35
|
+
const content = bundle
|
|
36
|
+
.slice(start, end)
|
|
37
|
+
.replace(/^\r?\n/, '')
|
|
38
|
+
.replace(/\r?\n$/, '');
|
|
39
|
+
return {
|
|
40
|
+
path,
|
|
41
|
+
bytes: match[2] === 'base64'
|
|
42
|
+
? base64ToBytes(content)
|
|
43
|
+
: textEncoder.encode(content)
|
|
44
|
+
};
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
/** Unzip (stored + deflate entries) into raw bytes. Validates the EOCD and the declared sizes. */
|
|
48
|
+
export async function unzip(z) {
|
|
49
|
+
const u16 = (o) => z[o] | (z[o + 1] << 8);
|
|
50
|
+
const u32 = (o) => (u16(o) | (u16(o + 2) << 16)) >>> 0;
|
|
51
|
+
let end = z.length - 22;
|
|
52
|
+
while (end >= 0 &&
|
|
53
|
+
!(u32(end) === 0x06054b50 && end + 22 + u16(end + 20) === z.length))
|
|
54
|
+
end--;
|
|
55
|
+
if (end < 0)
|
|
56
|
+
throw new Error('not a zip file (no end-of-central-directory)');
|
|
57
|
+
const out = [];
|
|
58
|
+
let p = u32(end + 16);
|
|
59
|
+
let total = 0;
|
|
60
|
+
const decoder = new TextDecoder();
|
|
61
|
+
const n = u16(end + 10);
|
|
62
|
+
if (n > MAX_SITE_FILES) {
|
|
63
|
+
throw new Error(`zip holds ${n} entries; a site upload is at most ${MAX_SITE_FILES} files`);
|
|
64
|
+
}
|
|
65
|
+
for (let i = 0; i < n; i++) {
|
|
66
|
+
const path = decoder.decode(z.slice(p + 46, p + 46 + u16(p + 28)));
|
|
67
|
+
const method = u16(p + 10);
|
|
68
|
+
const size = u32(p + 24);
|
|
69
|
+
total += size;
|
|
70
|
+
if (total > MAX_UNZIP_BYTES) {
|
|
71
|
+
throw new Error(`zip expands past ${MAX_UNZIP_BYTES} bytes; a static site is at most 20 MB`);
|
|
72
|
+
}
|
|
73
|
+
const at = u32(p + 42);
|
|
74
|
+
const start = at + 30 + u16(at + 26) + u16(at + 28);
|
|
75
|
+
const comp = z.slice(start, start + u32(p + 20));
|
|
76
|
+
p += 46 + u16(p + 28) + u16(p + 30) + u16(p + 32);
|
|
77
|
+
if (path.endsWith('/'))
|
|
78
|
+
continue; // directory entry
|
|
79
|
+
let bytes;
|
|
80
|
+
if (method === 0)
|
|
81
|
+
bytes = comp;
|
|
82
|
+
else if (method === 8) {
|
|
83
|
+
bytes = new Uint8Array(await new Response(new Blob([comp])
|
|
84
|
+
.stream()
|
|
85
|
+
.pipeThrough(new DecompressionStream('deflate-raw'))).arrayBuffer());
|
|
86
|
+
}
|
|
87
|
+
else
|
|
88
|
+
throw new Error(`zip entry '${path}' uses unsupported compression method ${method}`);
|
|
89
|
+
if (bytes.length !== size) {
|
|
90
|
+
throw new Error(`zip entry '${path}' declares ${size} bytes but holds ${bytes.length}`);
|
|
91
|
+
}
|
|
92
|
+
out.push({ path, bytes });
|
|
93
|
+
}
|
|
94
|
+
return out;
|
|
95
|
+
}
|
|
96
|
+
const CRC_TABLE = (() => {
|
|
97
|
+
const t = new Uint32Array(256);
|
|
98
|
+
for (let n = 0; n < 256; n++) {
|
|
99
|
+
let c = n;
|
|
100
|
+
for (let k = 0; k < 8; k++)
|
|
101
|
+
c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
|
|
102
|
+
t[n] = c >>> 0;
|
|
103
|
+
}
|
|
104
|
+
return t;
|
|
105
|
+
})();
|
|
106
|
+
export function crc32(bytes) {
|
|
107
|
+
let c = 0xffffffff;
|
|
108
|
+
for (let i = 0; i < bytes.length; i++)
|
|
109
|
+
c = CRC_TABLE[(c ^ bytes[i]) & 0xff] ^ (c >>> 8);
|
|
110
|
+
return (c ^ 0xffffffff) >>> 0;
|
|
111
|
+
}
|
|
112
|
+
/** A zip of the files, every entry stored (no compression), names in UTF-8. No zip64: under 4 GB and 65,535 entries. */
|
|
113
|
+
export function zipStored(files) {
|
|
114
|
+
const encoder = new TextEncoder();
|
|
115
|
+
const entries = files.map((f) => ({
|
|
116
|
+
name: encoder.encode(f.path),
|
|
117
|
+
bytes: f.bytes,
|
|
118
|
+
crc: crc32(f.bytes)
|
|
119
|
+
}));
|
|
120
|
+
const localSize = entries.reduce((sum, e) => sum + 30 + e.name.length + e.bytes.length, 0);
|
|
121
|
+
const centralSize = entries.reduce((sum, e) => sum + 46 + e.name.length, 0);
|
|
122
|
+
const out = new Uint8Array(localSize + centralSize + 22);
|
|
123
|
+
const view = new DataView(out.buffer);
|
|
124
|
+
const UTF8_FLAG = 0x0800;
|
|
125
|
+
let p = 0;
|
|
126
|
+
const offsets = [];
|
|
127
|
+
for (const e of entries) {
|
|
128
|
+
offsets.push(p);
|
|
129
|
+
view.setUint32(p, 0x04034b50, true);
|
|
130
|
+
view.setUint16(p + 4, 20, true); // version needed
|
|
131
|
+
view.setUint16(p + 6, UTF8_FLAG, true);
|
|
132
|
+
view.setUint16(p + 8, 0, true); // stored
|
|
133
|
+
view.setUint32(p + 10, 0, true); // time, date
|
|
134
|
+
view.setUint32(p + 14, e.crc, true);
|
|
135
|
+
view.setUint32(p + 18, e.bytes.length, true);
|
|
136
|
+
view.setUint32(p + 22, e.bytes.length, true);
|
|
137
|
+
view.setUint16(p + 26, e.name.length, true);
|
|
138
|
+
view.setUint16(p + 28, 0, true);
|
|
139
|
+
out.set(e.name, p + 30);
|
|
140
|
+
out.set(e.bytes, p + 30 + e.name.length);
|
|
141
|
+
p += 30 + e.name.length + e.bytes.length;
|
|
142
|
+
}
|
|
143
|
+
const centralStart = p;
|
|
144
|
+
entries.forEach((e, i) => {
|
|
145
|
+
view.setUint32(p, 0x02014b50, true);
|
|
146
|
+
view.setUint16(p + 4, 20, true); // version made by
|
|
147
|
+
view.setUint16(p + 6, 20, true); // version needed
|
|
148
|
+
view.setUint16(p + 8, UTF8_FLAG, true);
|
|
149
|
+
view.setUint16(p + 10, 0, true);
|
|
150
|
+
view.setUint32(p + 12, 0, true);
|
|
151
|
+
view.setUint32(p + 16, e.crc, true);
|
|
152
|
+
view.setUint32(p + 20, e.bytes.length, true);
|
|
153
|
+
view.setUint32(p + 24, e.bytes.length, true);
|
|
154
|
+
view.setUint16(p + 28, e.name.length, true);
|
|
155
|
+
// extra, comment, disk, internal attrs, external attrs: zero
|
|
156
|
+
view.setUint32(p + 42, offsets[i], true);
|
|
157
|
+
out.set(e.name, p + 46);
|
|
158
|
+
p += 46 + e.name.length;
|
|
159
|
+
});
|
|
160
|
+
view.setUint32(p, 0x06054b50, true);
|
|
161
|
+
view.setUint16(p + 8, entries.length, true);
|
|
162
|
+
view.setUint16(p + 10, entries.length, true);
|
|
163
|
+
view.setUint32(p + 12, p - centralStart, true);
|
|
164
|
+
view.setUint32(p + 16, centralStart, true);
|
|
165
|
+
return out;
|
|
166
|
+
}
|
|
167
|
+
// A zip is recognised by its bytes (local-file-header or empty-archive signature), not its name:
|
|
168
|
+
// an attachment can arrive without a file name, and a zip's path then carries no extension.
|
|
169
|
+
export const isZip = (f) => (f.bytes.length >= 4 &&
|
|
170
|
+
f.bytes[0] === 0x50 &&
|
|
171
|
+
f.bytes[1] === 0x4b &&
|
|
172
|
+
((f.bytes[2] === 0x03 && f.bytes[3] === 0x04) ||
|
|
173
|
+
(f.bytes[2] === 0x05 && f.bytes[3] === 0x06))) ||
|
|
174
|
+
/\.zip$/i.test(f.path);
|
|
175
|
+
const isRootIndex = (p) => /^index\.html?$/i.test(p);
|
|
176
|
+
/**
|
|
177
|
+
* The file set as uploaded: zips unpacked, paths cleaned, junk dropped, within the file
|
|
178
|
+
* count and the byte budget for the whole upload. No site-shape rule yet: a Wix Astro
|
|
179
|
+
* build is told apart from a static site on this list.
|
|
180
|
+
*/
|
|
181
|
+
export async function expandSiteFiles(input) {
|
|
182
|
+
const list = [];
|
|
183
|
+
let bytes = 0;
|
|
184
|
+
for (const f of input) {
|
|
185
|
+
const added = isZip(f) ? await unzip(f.bytes) : [f];
|
|
186
|
+
list.push(...added);
|
|
187
|
+
bytes += added.reduce((sum, a) => sum + a.bytes.byteLength, 0);
|
|
188
|
+
if (list.length > MAX_SITE_FILES) {
|
|
189
|
+
throw new Error(`Too many files (more than ${MAX_SITE_FILES}); a site upload is at most ${MAX_SITE_FILES}.`);
|
|
190
|
+
}
|
|
191
|
+
if (bytes > MAX_UNZIP_BYTES) {
|
|
192
|
+
throw new Error(`The upload expands past ${MAX_UNZIP_BYTES} bytes; send only the site's files.`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return list
|
|
196
|
+
.map((f) => ({
|
|
197
|
+
...f,
|
|
198
|
+
path: f.path.replace(/\\/g, '/').replace(/^\.?\//, '')
|
|
199
|
+
}))
|
|
200
|
+
.filter((f) => f.path &&
|
|
201
|
+
!f.path.startsWith('__MACOSX/') &&
|
|
202
|
+
!/(^|\/)\.DS_Store$/.test(f.path) &&
|
|
203
|
+
!f.path.split('/').includes('..'));
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Turn the raw inputs into the file set to drop: unpack zips, drop junk, strip a
|
|
207
|
+
* single wrapping folder, require a root index.html. Throws an agent-facing error.
|
|
208
|
+
*/
|
|
209
|
+
export async function normalizeSiteFiles(input) {
|
|
210
|
+
return staticSiteFiles(await expandSiteFiles(input));
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* The static-site rules on a list `expandSiteFiles` already made (its zips are not
|
|
214
|
+
* unpacked again): strip a single wrapping folder, require a root index.html.
|
|
215
|
+
*/
|
|
216
|
+
export function staticSiteFiles(expanded) {
|
|
217
|
+
let list = expanded;
|
|
218
|
+
if (list.length === 0) {
|
|
219
|
+
throw new Error('No files to upload: pass the site in `files` or `attachments`.');
|
|
220
|
+
}
|
|
221
|
+
if (list.length > MAX_SITE_FILES) {
|
|
222
|
+
throw new Error(`Too many files (${list.length}); a site upload is at most ${MAX_SITE_FILES}.`);
|
|
223
|
+
}
|
|
224
|
+
// A zip of a folder: strip the single top-level directory so index.html sits at the root.
|
|
225
|
+
if (!list.some((f) => isRootIndex(f.path))) {
|
|
226
|
+
const first = list[0].path.split('/')[0];
|
|
227
|
+
if (list.every((f) => f.path.includes('/') && f.path.split('/')[0] === first)) {
|
|
228
|
+
list = list.map((f) => ({
|
|
229
|
+
...f,
|
|
230
|
+
path: f.path.split('/').slice(1).join('/')
|
|
231
|
+
}));
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
// A single root HTML file is the homepage whatever its name — the drop endpoint's
|
|
235
|
+
// own rule (headless-business-setup renameToIndexHtml), applied here so the
|
|
236
|
+
// shape check below doesn't reject what the endpoint would accept.
|
|
237
|
+
const rootHtml = list.filter((f) => !f.path.includes('/') && /\.html?$/i.test(f.path));
|
|
238
|
+
if (!list.some((f) => isRootIndex(f.path)) && rootHtml.length === 1) {
|
|
239
|
+
list = list.map((f) => f === rootHtml[0] ? { ...f, path: 'index.html' } : f);
|
|
240
|
+
}
|
|
241
|
+
if (!list.some((f) => isRootIndex(f.path))) {
|
|
242
|
+
const shown = list
|
|
243
|
+
.slice(0, 8)
|
|
244
|
+
.map((f) => f.path)
|
|
245
|
+
.join(', ');
|
|
246
|
+
throw new Error(`The site needs an index.html at its root. Files received: ${shown}${list.length > 8 ? ', …' : ''}`);
|
|
247
|
+
}
|
|
248
|
+
return list;
|
|
249
|
+
}
|
|
250
|
+
export function guessContentType(path) {
|
|
251
|
+
const ext = (/\.([a-zA-Z0-9]+)$/.exec(path)?.[1] ?? '').toLowerCase();
|
|
252
|
+
const map = {
|
|
253
|
+
html: 'text/html',
|
|
254
|
+
htm: 'text/html',
|
|
255
|
+
css: 'text/css',
|
|
256
|
+
js: 'text/javascript',
|
|
257
|
+
mjs: 'text/javascript',
|
|
258
|
+
json: 'application/json',
|
|
259
|
+
svg: 'image/svg+xml',
|
|
260
|
+
txt: 'text/plain',
|
|
261
|
+
md: 'text/markdown',
|
|
262
|
+
xml: 'application/xml',
|
|
263
|
+
webmanifest: 'application/manifest+json',
|
|
264
|
+
ico: 'image/x-icon',
|
|
265
|
+
png: 'image/png',
|
|
266
|
+
jpg: 'image/jpeg',
|
|
267
|
+
jpeg: 'image/jpeg',
|
|
268
|
+
gif: 'image/gif',
|
|
269
|
+
webp: 'image/webp',
|
|
270
|
+
avif: 'image/avif',
|
|
271
|
+
woff: 'font/woff',
|
|
272
|
+
woff2: 'font/woff2',
|
|
273
|
+
ttf: 'font/ttf',
|
|
274
|
+
otf: 'font/otf',
|
|
275
|
+
pdf: 'application/pdf',
|
|
276
|
+
mp4: 'video/mp4',
|
|
277
|
+
webm: 'video/webm',
|
|
278
|
+
mp3: 'audio/mpeg'
|
|
279
|
+
};
|
|
280
|
+
return map[ext] ?? 'application/octet-stream';
|
|
281
|
+
}
|
|
282
|
+
/** The drop endpoint's body: one `files` part per file, the path as the filename. */
|
|
283
|
+
export function buildFormData(files) {
|
|
284
|
+
const fd = new FormData();
|
|
285
|
+
for (const f of files) {
|
|
286
|
+
fd.append('files', new Blob([f.bytes], { type: guessContentType(f.path) }), f.path);
|
|
287
|
+
}
|
|
288
|
+
return fd;
|
|
289
|
+
}
|
|
290
|
+
//# sourceMappingURL=site-files.js.map
|