html2pptx-local-mcp 1.1.20 → 1.1.21

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 (32) hide show
  1. package/app/docs/content.js +50 -16
  2. package/cli/dist/commands/edit.d.ts +1 -1
  3. package/cli/dist/commands/edit.js +30 -13
  4. package/cli/dist/index.js +0 -0
  5. package/lib/local-editor-server.js +316 -0
  6. package/lib/local-editor-state.js +45 -0
  7. package/lib/local-slide-editor-launcher.js +19 -18
  8. package/lib/pptx-studio-mcp-core.js +15 -9
  9. package/local-editor-app/app/api/edit-slide/local-health/route.js +16 -0
  10. package/local-editor-app/app/edit-slide/edit-slide-client.jsx +13153 -0
  11. package/local-editor-app/app/edit-slide/page.jsx +13 -0
  12. package/local-editor-app/app/globals.css +4 -0
  13. package/local-editor-app/app/layout.jsx +14 -0
  14. package/local-editor-app/components/studio/edit-property-panel.jsx +1061 -0
  15. package/local-editor-app/lib/edit-panel-value-normalizer.js +97 -0
  16. package/local-editor-app/lib/edit-slide-editor-helpers.js +120 -0
  17. package/local-editor-app/lib/edit-slide-url-security.js +247 -0
  18. package/local-editor-app/next.config.mjs +31 -0
  19. package/local-editor-app/package.json +7 -0
  20. package/mcp/pptx-studio-mcp-server.mjs +1 -1
  21. package/package.json +13 -2
  22. package/public/skills/html2pptx/SKILL.md +635 -0
  23. package/public/skills/html2pptx/references/automation-contract.md +68 -0
  24. package/public/skills/html2pptx/references/input-contract.md +107 -0
  25. package/public/skills/html2pptx/references/japanese-slide-design.md +273 -0
  26. package/public/skills/html2pptx/references/rewrite-patterns.md +218 -0
  27. package/public/skills/icon-generator/SKILL.md +133 -0
  28. package/public/skills/open-slide/SKILL.md +160 -0
  29. package/public/skills/publish-template/SKILL.md +215 -0
  30. package/public/skills/register-template/SKILL.md +142 -0
  31. package/scripts/install-mcp.mjs +28 -2
  32. package/scripts/install-skills.mjs +82 -0
@@ -0,0 +1,97 @@
1
+ const PIXEL_LENGTH_KEYS = new Set([
2
+ 'width',
3
+ 'height',
4
+ 'minWidth',
5
+ 'minHeight',
6
+ 'left',
7
+ 'top',
8
+ 'right',
9
+ 'bottom',
10
+ 'fontSize',
11
+ 'paddingTop',
12
+ 'paddingRight',
13
+ 'paddingBottom',
14
+ 'paddingLeft',
15
+ 'marginTop',
16
+ 'marginRight',
17
+ 'marginBottom',
18
+ 'marginLeft',
19
+ 'borderRadius',
20
+ 'borderTopLeftRadius',
21
+ 'borderTopRightRadius',
22
+ 'borderBottomRightRadius',
23
+ 'borderBottomLeftRadius',
24
+ 'borderWidth',
25
+ ]);
26
+
27
+ const ZERO_ON_EMPTY_PIXEL_KEYS = new Set([
28
+ 'paddingTop',
29
+ 'paddingRight',
30
+ 'paddingBottom',
31
+ 'paddingLeft',
32
+ 'marginTop',
33
+ 'marginRight',
34
+ 'marginBottom',
35
+ 'marginLeft',
36
+ 'borderRadius',
37
+ 'borderTopLeftRadius',
38
+ 'borderTopRightRadius',
39
+ 'borderBottomRightRadius',
40
+ 'borderBottomLeftRadius',
41
+ 'borderWidth',
42
+ ]);
43
+
44
+ function isBareNumber(value) {
45
+ return /^-?(?:\d+|\d*\.\d+)$/.test(String(value).trim());
46
+ }
47
+
48
+ function addPxToBareNumbers(value) {
49
+ const raw = String(value ?? '');
50
+ const trimmed = raw.trim();
51
+ if (!trimmed) return raw;
52
+ return trimmed.replace(/(^|\s)(-?(?:\d+|\d*\.\d+))(?=\s|$)/g, '$1$2px');
53
+ }
54
+
55
+ export function normalizeEditPanelInputValue(key, value) {
56
+ if (!ZERO_ON_EMPTY_PIXEL_KEYS.has(key)) return value;
57
+ return String(value ?? '').trim() === '' ? '0' : value;
58
+ }
59
+
60
+ export function normalizeStyleValue(key, value) {
61
+ const input = normalizeEditPanelInputValue(key, value);
62
+ if (PIXEL_LENGTH_KEYS.has(key)) return addPxToBareNumbers(input);
63
+ if (key === 'lineHeight') {
64
+ const trimmed = String(input ?? '').trim();
65
+ if (isBareNumber(trimmed) && Math.abs(Number(trimmed)) > 4) return `${trimmed}px`;
66
+ return input;
67
+ }
68
+ return input;
69
+ }
70
+
71
+ export function getPaddingResizeDimension(side) {
72
+ if (side === 'Left' || side === 'Right') return 'width';
73
+ if (side === 'Top' || side === 'Bottom') return 'height';
74
+ return null;
75
+ }
76
+
77
+ export function getExpandedBorderBoxMetric({
78
+ boxSizing,
79
+ side,
80
+ previousPaddingPx,
81
+ nextPaddingPx,
82
+ currentMetricPx,
83
+ }) {
84
+ if (boxSizing !== 'border-box') return null;
85
+ const dimension = getPaddingResizeDimension(side);
86
+ if (!dimension) return null;
87
+
88
+ const previous = Number(previousPaddingPx);
89
+ const next = Number(nextPaddingPx);
90
+ const current = Number(currentMetricPx);
91
+ if (![previous, next, current].every(Number.isFinite)) return null;
92
+
93
+ return {
94
+ dimension,
95
+ value: Math.max(0, current + next - previous),
96
+ };
97
+ }
@@ -0,0 +1,120 @@
1
+ import { assetSrcFromPreviewUrl } from './edit-slide-url-security.js';
2
+
3
+ export const EDITOR_ZOOM_MIN = 0.25;
4
+ export const EDITOR_ZOOM_MAX = 2.4;
5
+ export const EDITOR_ZOOM_STEP = 0.1;
6
+
7
+ function finiteNumber(value, fallback = 0) {
8
+ const numeric = Number(value);
9
+ return Number.isFinite(numeric) ? numeric : fallback;
10
+ }
11
+
12
+ export function roundEditorNumber(value) {
13
+ return Math.round(finiteNumber(value) * 100) / 100;
14
+ }
15
+
16
+ export function clampEditorZoom(value, min = EDITOR_ZOOM_MIN, max = EDITOR_ZOOM_MAX) {
17
+ return roundEditorNumber(Math.max(min, Math.min(max, finiteNumber(value, 1))));
18
+ }
19
+
20
+ export function readPixelValue(value, fallback = 0) {
21
+ const raw = String(value ?? '').trim();
22
+ if (!raw || raw === 'auto') return fallback;
23
+ const numeric = Number.parseFloat(raw);
24
+ return Number.isFinite(numeric) ? numeric : fallback;
25
+ }
26
+
27
+ function stripEditorHelperClasses(element) {
28
+ if (!element?.classList) return;
29
+ element.classList.remove('_os_hover', '_os_selected');
30
+ if (element.classList.length === 0) element.removeAttribute('class');
31
+ }
32
+
33
+ export function stripEditorHelpers(root) {
34
+ stripEditorHelperClasses(root);
35
+ root?.querySelectorAll?.('._os_hover, ._os_selected').forEach(stripEditorHelperClasses);
36
+ }
37
+
38
+ function restoreElementAssetUrls(element) {
39
+ if (!element?.getAttribute) return;
40
+ const src = element.getAttribute('src');
41
+ if (src) {
42
+ const restored = assetSrcFromPreviewUrl(src);
43
+ if (restored) element.setAttribute('src', restored);
44
+ }
45
+ element.querySelectorAll?.('source[src]').forEach((source) => {
46
+ const sourceSrc = source.getAttribute('src');
47
+ const restored = assetSrcFromPreviewUrl(sourceSrc);
48
+ if (restored) source.setAttribute('src', restored);
49
+ });
50
+ const style = element.getAttribute('style');
51
+ if (style && style.includes('url(')) {
52
+ const next = style.replace(/url\(\s*(['"]?)([^)'"]+)\1\s*\)/gi, (match, quote, val) => {
53
+ const restored = assetSrcFromPreviewUrl(val);
54
+ return restored ? `url(${quote}${restored}${quote})` : match;
55
+ });
56
+ if (next !== style) element.setAttribute('style', next);
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Convert any token-scoped bridge asset preview URLs (added for in-iframe
62
+ * rendering) back to their canonical relative path before the slide HTML is
63
+ * serialized to disk. Keeps the saved file portable and free of session tokens.
64
+ */
65
+ export function restorePreviewAssetUrls(root) {
66
+ if (!root) return;
67
+ restoreElementAssetUrls(root);
68
+ root.querySelectorAll?.('img[src], video[src], source[src], [style]').forEach(restoreElementAssetUrls);
69
+ }
70
+
71
+ export function isEditorTypingTarget(target) {
72
+ const element = target?.nodeType === 1 ? target : target?.parentElement;
73
+ if (!element) return false;
74
+ const tagName = element.tagName;
75
+ return Boolean(
76
+ element.isContentEditable ||
77
+ element.closest?.('[contenteditable="true"]') ||
78
+ tagName === 'INPUT' ||
79
+ tagName === 'TEXTAREA' ||
80
+ tagName === 'SELECT',
81
+ );
82
+ }
83
+
84
+ export function isEditorDeleteKey(key) {
85
+ return key === 'Backspace' || key === 'Delete';
86
+ }
87
+
88
+ export function consumeEditorKeyEvent(event) {
89
+ event?.preventDefault?.();
90
+ event?.stopPropagation?.();
91
+ event?.stopImmediatePropagation?.();
92
+ }
93
+
94
+ export function duplicateEditableElement(element) {
95
+ if (!element?.parentElement) return null;
96
+ const clone = element.cloneNode(true);
97
+ stripEditorHelpers(clone);
98
+ element.after(clone);
99
+ return clone;
100
+ }
101
+
102
+ export function deleteEditableElement(element) {
103
+ if (!element?.parentElement) return false;
104
+ element.remove();
105
+ return true;
106
+ }
107
+
108
+ export function orderEditableElement(element, direction) {
109
+ const parent = element?.parentElement;
110
+ if (!parent) return false;
111
+ if (direction === 'front') {
112
+ parent.appendChild(element);
113
+ return true;
114
+ }
115
+ if (direction === 'back') {
116
+ parent.insertBefore(element, parent.firstElementChild);
117
+ return true;
118
+ }
119
+ return false;
120
+ }
@@ -0,0 +1,247 @@
1
+ export const DEFAULT_EDIT_SLIDE_FILE_API = '/api/edit-slide/file';
2
+ export const EDIT_SLIDE_ASSET_API_PATH = '/api/edit-slide/asset';
3
+ export const ASSET_IMAGE_EXTENSIONS = ['.png', '.jpg', '.jpeg', '.gif', '.webp', '.svg', '.avif'];
4
+ export const ASSET_VIDEO_EXTENSIONS = ['.mp4', '.webm', '.mov', '.m4v', '.ogv'];
5
+ export const ASSET_MEDIA_EXTENSIONS = [...ASSET_IMAGE_EXTENSIONS, ...ASSET_VIDEO_EXTENSIONS];
6
+
7
+ export function assetKindFromSrc(value) {
8
+ const pathPart = String(value || '').split(/[?#]/)[0].toLowerCase();
9
+ if (ASSET_VIDEO_EXTENSIONS.some((ext) => pathPart.endsWith(ext))) return 'video';
10
+ if (ASSET_IMAGE_EXTENSIONS.some((ext) => pathPart.endsWith(ext))) return 'image';
11
+ return 'file';
12
+ }
13
+
14
+ function firstParam(params, key) {
15
+ if (!params) return '';
16
+ if (params instanceof URLSearchParams) return params.get(key) || '';
17
+ const value = params[key];
18
+ if (Array.isArray(value)) return value[0] == null ? '' : String(value[0]);
19
+ return value == null ? '' : String(value);
20
+ }
21
+
22
+ export function isLoopbackBridgeUrl(url) {
23
+ const host = url.hostname.replace(/^\[|\]$/g, '');
24
+ const isLoopback =
25
+ host === 'localhost' ||
26
+ host === '::1' ||
27
+ host === '0:0:0:0:0:0:0:1' ||
28
+ /^127(?:\.\d{1,3}){3}$/.test(host);
29
+ return isLoopback && ['http:', 'https:'].includes(url.protocol);
30
+ }
31
+
32
+ export function isLoopbackRequestHost(rawHost) {
33
+ const hosts = String(rawHost || '')
34
+ .split(',')
35
+ .map((host) => host.trim())
36
+ .filter(Boolean);
37
+ if (!hosts.length) return false;
38
+ return hosts.every((host) => {
39
+ try {
40
+ return isLoopbackBridgeUrl(new URL(`http://${host}`));
41
+ } catch {
42
+ return false;
43
+ }
44
+ });
45
+ }
46
+
47
+ export function isLoopbackRequest(headersLike) {
48
+ const host = headersLike?.get?.('host') || '';
49
+ const forwardedHost = headersLike?.get?.('x-forwarded-host') || '';
50
+ if (!isLoopbackRequestHost(host)) return false;
51
+ return !forwardedHost || isLoopbackRequestHost(forwardedHost);
52
+ }
53
+
54
+ export function isLocalEditSlideLaunch(params) {
55
+ const file = firstParam(params, 'file');
56
+ const rawBridge = firstParam(params, 'bridge') || firstParam(params, 'localBridge');
57
+ if (!file || !rawBridge) return false;
58
+ try {
59
+ return isLoopbackBridgeUrl(new URL(rawBridge));
60
+ } catch {
61
+ return false;
62
+ }
63
+ }
64
+
65
+ export function bridgeTokenFromHash(hash) {
66
+ const rawHash = hash?.startsWith('#') ? hash.slice(1) : hash;
67
+ if (!rawHash) return '';
68
+ const params = new URLSearchParams(rawHash);
69
+ return params.get('bridgeToken') || params.get('token') || '';
70
+ }
71
+
72
+ export function normalizeLocalBridgeApi(rawBridge, defaultFileApi = DEFAULT_EDIT_SLIDE_FILE_API, bridgeToken = '') {
73
+ if (!rawBridge) return defaultFileApi;
74
+ try {
75
+ const url = new URL(rawBridge);
76
+ if (!isLoopbackBridgeUrl(url)) {
77
+ return defaultFileApi;
78
+ }
79
+ const token = bridgeToken || url.searchParams.get('token') || '';
80
+ url.pathname = `${url.pathname.replace(/\/+$/, '')}/api/edit-slide/file`;
81
+ url.search = '';
82
+ if (token) url.searchParams.set('token', token);
83
+ url.hash = '';
84
+ return url.toString();
85
+ } catch {
86
+ return defaultFileApi;
87
+ }
88
+ }
89
+
90
+ export function redactBridgeTokenFromEditorUrl(href) {
91
+ try {
92
+ const pageUrl = new URL(href);
93
+ let changed = false;
94
+ ['bridge', 'localBridge'].forEach((param) => {
95
+ const rawBridge = pageUrl.searchParams.get(param);
96
+ if (!rawBridge) return;
97
+ try {
98
+ const bridgeUrl = new URL(rawBridge);
99
+ if (!isLoopbackBridgeUrl(bridgeUrl) || !bridgeUrl.searchParams.has('token')) return;
100
+ bridgeUrl.searchParams.delete('token');
101
+ pageUrl.searchParams.set(param, bridgeUrl.toString());
102
+ changed = true;
103
+ } catch {
104
+ // Ignore malformed bridge URLs; normalizeLocalBridgeApi will fall back.
105
+ }
106
+ });
107
+ if (pageUrl.hash) {
108
+ const hashParams = new URLSearchParams(pageUrl.hash.slice(1));
109
+ const hadToken = hashParams.has('bridgeToken') || hashParams.has('token');
110
+ hashParams.delete('bridgeToken');
111
+ hashParams.delete('token');
112
+ if (hadToken) {
113
+ const nextHash = hashParams.toString();
114
+ pageUrl.hash = nextHash ? `#${nextHash}` : '';
115
+ changed = true;
116
+ }
117
+ }
118
+ return changed ? pageUrl.toString() : href;
119
+ } catch {
120
+ return href;
121
+ }
122
+ }
123
+
124
+ export function buildFileApiUrl(fileApi, params = {}, baseHref) {
125
+ try {
126
+ const base =
127
+ baseHref ||
128
+ (typeof window === 'undefined' ? 'http://localhost' : window.location.href);
129
+ const url = new URL(fileApi, base);
130
+ Object.entries(params).forEach(([key, value]) => {
131
+ if (value !== undefined && value !== null) url.searchParams.set(key, String(value));
132
+ });
133
+ return url.toString();
134
+ } catch {
135
+ const search = new URLSearchParams();
136
+ Object.entries(params).forEach(([key, value]) => {
137
+ if (value !== undefined && value !== null) search.set(key, String(value));
138
+ });
139
+ const glue = fileApi.includes('?') ? '&' : '?';
140
+ return search.size ? `${fileApi}${glue}${search.toString()}` : fileApi;
141
+ }
142
+ }
143
+
144
+ export function buildSlideDoc(outerHtml, css, options = {}) {
145
+ const editOverflowMargin = Number(options.editOverflowMargin) || 0;
146
+ const slideWidth = Number(options.slideWidth) || 1600;
147
+ const slideHeight = Number(options.slideHeight) || 900;
148
+ const resolve = typeof options.resolveAssetUrl === 'function' ? options.resolveAssetUrl : null;
149
+ const body = resolve ? rewriteAssetUrlsForPreview(outerHtml, resolve, { encodeAmp: true }) : (outerHtml || '');
150
+ const style = resolve ? rewriteAssetUrlsForPreview(css || '', resolve, { encodeAmp: false }) : (css || '');
151
+ if (editOverflowMargin <= 0) {
152
+ return `<!doctype html><html><head><meta charset="utf-8"><meta name="referrer" content="no-referrer"><style>
153
+ html,body{margin:0;padding:0;background:transparent;}
154
+ body{display:flex;align-items:center;justify-content:center;}
155
+ ${style}
156
+ </style></head><body>${body}</body></html>`;
157
+ }
158
+
159
+ return `<!doctype html><html><head><meta charset="utf-8"><meta name="referrer" content="no-referrer"><style>
160
+ html,body{margin:0;padding:0;background:transparent;}
161
+ ${style}
162
+ html,body{width:${slideWidth + editOverflowMargin * 2}px;min-height:${slideHeight + editOverflowMargin * 2}px;overflow:visible;}
163
+ body{display:block;padding:${editOverflowMargin}px;box-sizing:border-box;}
164
+ .slide{overflow:visible !important;}
165
+ </style></head><body>${body}</body></html>`;
166
+ }
167
+
168
+ /**
169
+ * Local media assets are stored on disk next to the slide HTML and referenced
170
+ * with relative paths (e.g. `assets/logo.png`). Those paths cannot resolve
171
+ * inside the srcDoc iframe, so for preview we rewrite them to a token-scoped
172
+ * bridge URL that streams the file. The canonical relative path is carried in
173
+ * the URL's `src` query param so it can be restored verbatim on save.
174
+ */
175
+ export function isResolvableAssetRef(value) {
176
+ const v = String(value || '').trim();
177
+ if (!v) return false;
178
+ if (/^(data:|blob:|https?:|\/\/|#|mailto:|tel:)/i.test(v)) return false;
179
+ if (v.startsWith('/')) return false; // root-relative / absolute — leave untouched
180
+ const pathPart = v.split(/[?#]/)[0].toLowerCase();
181
+ return ASSET_MEDIA_EXTENSIONS.some((ext) => pathPart.endsWith(ext));
182
+ }
183
+
184
+ export function buildAssetApiBase(fileApi) {
185
+ const raw = fileApi || DEFAULT_EDIT_SLIDE_FILE_API;
186
+ try {
187
+ const isAbsolute = /^https?:\/\//i.test(raw);
188
+ const url = new URL(raw, isAbsolute ? undefined : 'http://localhost');
189
+ url.pathname = url.pathname.replace(/\/api\/(?:edit|open)-slide\/file\/?$/, EDIT_SLIDE_ASSET_API_PATH);
190
+ if (!url.pathname.endsWith(EDIT_SLIDE_ASSET_API_PATH)) {
191
+ url.pathname = EDIT_SLIDE_ASSET_API_PATH;
192
+ }
193
+ const token = url.searchParams.get('token') || '';
194
+ url.search = '';
195
+ if (token) url.searchParams.set('token', token);
196
+ url.hash = '';
197
+ return isAbsolute ? url.toString() : `${url.pathname}${url.search}`;
198
+ } catch {
199
+ return EDIT_SLIDE_ASSET_API_PATH;
200
+ }
201
+ }
202
+
203
+ export function buildAssetPreviewUrl(assetApiBase, { file, src } = {}) {
204
+ if (!assetApiBase || !src) return null;
205
+ try {
206
+ const isAbsolute = /^https?:\/\//i.test(assetApiBase);
207
+ const url = new URL(assetApiBase, isAbsolute ? undefined : 'http://localhost');
208
+ if (file) url.searchParams.set('file', file);
209
+ url.searchParams.set('src', src);
210
+ return isAbsolute ? url.toString() : `${url.pathname}${url.search}`;
211
+ } catch {
212
+ return null;
213
+ }
214
+ }
215
+
216
+ export function assetSrcFromPreviewUrl(value) {
217
+ const v = String(value || '');
218
+ if (!v.includes(EDIT_SLIDE_ASSET_API_PATH)) return null;
219
+ try {
220
+ const url = new URL(v, 'http://localhost');
221
+ if (!url.pathname.endsWith(EDIT_SLIDE_ASSET_API_PATH)) return null;
222
+ return url.searchParams.get('src') || null;
223
+ } catch {
224
+ return null;
225
+ }
226
+ }
227
+
228
+ export function rewriteAssetUrlsForPreview(input, resolve, { encodeAmp = true } = {}) {
229
+ if (!input || typeof resolve !== 'function') return input || '';
230
+ const enc = (url) => (encodeAmp ? url.replace(/&/g, '&amp;') : url);
231
+ let out = String(input).replace(/(\ssrc=)(["'])(.*?)\2/gi, (match, lead, quote, val) => {
232
+ if (!isResolvableAssetRef(val)) return match;
233
+ const resolved = resolve(val);
234
+ return resolved ? `${lead}${quote}${enc(resolved)}${quote}` : match;
235
+ });
236
+ out = out.replace(/(<source\b[^>]*\ssrc=)(["'])(.*?)\2/gi, (match, lead, quote, val) => {
237
+ if (!isResolvableAssetRef(val)) return match;
238
+ const resolved = resolve(val);
239
+ return resolved ? `${lead}${quote}${enc(resolved)}${quote}` : match;
240
+ });
241
+ out = out.replace(/url\(\s*(['"]?)([^)'"]+)\1\s*\)/gi, (match, quote, val) => {
242
+ if (!isResolvableAssetRef(val)) return match;
243
+ const resolved = resolve(val);
244
+ return resolved ? `url(${quote}${enc(resolved)}${quote})` : match;
245
+ });
246
+ return out;
247
+ }
@@ -0,0 +1,31 @@
1
+ const localBridgeConnectSrc = 'http://localhost:* http://127.0.0.1:* https://localhost:* https://127.0.0.1:*';
2
+
3
+ const localEditorSecurityHeaders = [
4
+ { key: 'X-Content-Type-Options', value: 'nosniff' },
5
+ { key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
6
+ {
7
+ key: 'Content-Security-Policy',
8
+ value: [
9
+ "default-src 'self'",
10
+ "script-src 'self' 'unsafe-inline' 'unsafe-eval'",
11
+ "style-src 'self' 'unsafe-inline'",
12
+ "img-src 'self' data: blob: http://localhost:* http://127.0.0.1:* https://localhost:* https://127.0.0.1:*",
13
+ "media-src 'self' data: blob: http://localhost:* http://127.0.0.1:* https://localhost:* https://127.0.0.1:*",
14
+ "font-src 'self' data:",
15
+ `connect-src 'self' ${localBridgeConnectSrc}`,
16
+ "frame-src 'self' blob:",
17
+ "frame-ancestors 'none'",
18
+ "form-action 'self'",
19
+ "base-uri 'self'",
20
+ ].join('; '),
21
+ },
22
+ ];
23
+
24
+ export default {
25
+ async headers() {
26
+ return [
27
+ { source: '/edit-slide', headers: localEditorSecurityHeaders },
28
+ { source: '/:path*', headers: localEditorSecurityHeaders },
29
+ ];
30
+ },
31
+ };
@@ -0,0 +1,7 @@
1
+ {
2
+ "private": true,
3
+ "type": "module",
4
+ "scripts": {
5
+ "dev": "next dev --webpack"
6
+ }
7
+ }
@@ -186,7 +186,7 @@ async function getBaseUrl() {
186
186
  process.env.PPTX_STUDIO_BASE_URL || await readRegisteredEditorBaseUrl(process.cwd());
187
187
  if (!raw) {
188
188
  throw new Error(
189
- 'PPTX_STUDIO_BASE_URL is not set and no local editor URL is registered. Start `node scripts/dev-studio.mjs` first, or set PPTX_STUDIO_BASE_URL to the active loopback app URL.'
189
+ 'PPTX_STUDIO_BASE_URL is not set and no local editor URL is registered. Set PPTX_STUDIO_BASE_URL to the active app URL when using remote export tools from a source checkout.'
190
190
  );
191
191
  }
192
192
  const normalized = raw.endsWith('/') ? raw : `${raw}/`;
package/package.json CHANGED
@@ -1,22 +1,28 @@
1
1
  {
2
2
  "name": "html2pptx-local-mcp",
3
- "version": "1.1.20",
3
+ "version": "1.1.21",
4
4
  "type": "module",
5
5
  "description": "Local stdio MCP server for opening html2pptx slide HTML in the local edit-slide editor.",
6
6
  "bin": {
7
7
  "html2pptx-mcp": "./mcp/pptx-studio-mcp-server.mjs",
8
8
  "html2pptx-install-mcp": "./scripts/install-mcp.mjs",
9
+ "html2pptx-install-skills": "./scripts/install-skills.mjs",
9
10
  "html2pptx-comments": "./scripts/extract-html2pptx-comments.mjs"
10
11
  },
11
12
  "files": [
12
13
  "app/docs/content.js",
13
14
  "cli/dist",
14
15
  "cli/package.json",
16
+ "local-editor-app",
17
+ "lib/local-editor-server.js",
18
+ "lib/local-editor-state.js",
15
19
  "lib/local-slide-editor-launcher.js",
16
20
  "lib/pptx-studio-mcp-core.js",
17
21
  "lib/server/template-html-policy.mjs",
18
22
  "mcp/pptx-studio-mcp-server.mjs",
23
+ "public/skills",
19
24
  "scripts/install-mcp.mjs",
25
+ "scripts/install-skills.mjs",
20
26
  "scripts/extract-html2pptx-comments.mjs",
21
27
  "src/animation-injector.js",
22
28
  "src/animation-renderers.js"
@@ -24,8 +30,13 @@
24
30
  "dependencies": {
25
31
  "@clack/prompts": "^0.10.1",
26
32
  "commander": "^13.1.0",
33
+ "framer-motion": "^12.38.0",
27
34
  "jsdom": "^26.1.0",
28
- "picocolors": "^1.1.1"
35
+ "lucide-react": "^1.7.0",
36
+ "next": "^16.2.2",
37
+ "picocolors": "^1.1.1",
38
+ "react": "^19.2.4",
39
+ "react-dom": "^19.2.4"
29
40
  },
30
41
  "engines": {
31
42
  "node": ">=18"