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.
- package/app/docs/content.js +50 -16
- package/cli/dist/commands/edit.d.ts +1 -1
- package/cli/dist/commands/edit.js +30 -13
- package/cli/dist/index.js +0 -0
- package/lib/local-editor-server.js +316 -0
- package/lib/local-editor-state.js +45 -0
- package/lib/local-slide-editor-launcher.js +19 -18
- package/lib/pptx-studio-mcp-core.js +15 -9
- package/local-editor-app/app/api/edit-slide/local-health/route.js +16 -0
- package/local-editor-app/app/edit-slide/edit-slide-client.jsx +13153 -0
- package/local-editor-app/app/edit-slide/page.jsx +13 -0
- package/local-editor-app/app/globals.css +4 -0
- package/local-editor-app/app/layout.jsx +14 -0
- package/local-editor-app/components/studio/edit-property-panel.jsx +1061 -0
- package/local-editor-app/lib/edit-panel-value-normalizer.js +97 -0
- package/local-editor-app/lib/edit-slide-editor-helpers.js +120 -0
- package/local-editor-app/lib/edit-slide-url-security.js +247 -0
- package/local-editor-app/next.config.mjs +31 -0
- package/local-editor-app/package.json +7 -0
- package/mcp/pptx-studio-mcp-server.mjs +1 -1
- package/package.json +13 -2
- package/public/skills/html2pptx/SKILL.md +635 -0
- package/public/skills/html2pptx/references/automation-contract.md +68 -0
- package/public/skills/html2pptx/references/input-contract.md +107 -0
- package/public/skills/html2pptx/references/japanese-slide-design.md +273 -0
- package/public/skills/html2pptx/references/rewrite-patterns.md +218 -0
- package/public/skills/icon-generator/SKILL.md +133 -0
- package/public/skills/open-slide/SKILL.md +160 -0
- package/public/skills/publish-template/SKILL.md +215 -0
- package/public/skills/register-template/SKILL.md +142 -0
- package/scripts/install-mcp.mjs +28 -2
- package/scripts/install-skills.mjs +82 -0
package/app/docs/content.js
CHANGED
|
@@ -179,14 +179,19 @@ npx skills add https://html2pptx.app -a cursor
|
|
|
179
179
|
# Windsurf
|
|
180
180
|
npx skills add https://html2pptx.app -a windsurf
|
|
181
181
|
|
|
182
|
+
# Grok Build
|
|
183
|
+
npx --yes --package html2pptx-local-mcp@latest html2pptx-install-skills grok
|
|
184
|
+
|
|
182
185
|
# Preview published skills without installing
|
|
183
186
|
npx skills add https://html2pptx.app --list
|
|
184
187
|
|
|
185
188
|
# More agents / multiple targets: use the interactive selector
|
|
186
189
|
npx skills add https://html2pptx.app
|
|
187
190
|
|
|
188
|
-
# Claude Code users: run
|
|
189
|
-
npx --yes --package html2pptx-local-mcp@latest html2pptx-install-mcp claude
|
|
191
|
+
# Claude Code / Codex / Grok Build users: run the matching one line for MCP.
|
|
192
|
+
npx --yes --package html2pptx-local-mcp@latest html2pptx-install-mcp claude
|
|
193
|
+
npx --yes --package html2pptx-local-mcp@latest html2pptx-install-mcp codex
|
|
194
|
+
npx --yes --package html2pptx-local-mcp@latest html2pptx-install-mcp grok`;
|
|
190
195
|
|
|
191
196
|
const SKILL_INSTALL_COMMAND_JA = `# 使うエージェントのコマンドを選択
|
|
192
197
|
# Claude Code
|
|
@@ -201,14 +206,19 @@ npx skills add https://html2pptx.app -a cursor
|
|
|
201
206
|
# Windsurf
|
|
202
207
|
npx skills add https://html2pptx.app -a windsurf
|
|
203
208
|
|
|
209
|
+
# Grok Build
|
|
210
|
+
npx --yes --package html2pptx-local-mcp@latest html2pptx-install-skills grok
|
|
211
|
+
|
|
204
212
|
# インストールせずに公開Skillを確認
|
|
205
213
|
npx skills add https://html2pptx.app --list
|
|
206
214
|
|
|
207
215
|
# その他のエージェント / 複数指定は対話形式で選択
|
|
208
216
|
npx skills add https://html2pptx.app
|
|
209
217
|
|
|
210
|
-
# Claude Code
|
|
211
|
-
npx --yes --package html2pptx-local-mcp@latest html2pptx-install-mcp claude
|
|
218
|
+
# Claude Code / Codex / Grok Build ユーザーは MCP 用の該当行を実行。
|
|
219
|
+
npx --yes --package html2pptx-local-mcp@latest html2pptx-install-mcp claude
|
|
220
|
+
npx --yes --package html2pptx-local-mcp@latest html2pptx-install-mcp codex
|
|
221
|
+
npx --yes --package html2pptx-local-mcp@latest html2pptx-install-mcp grok`;
|
|
212
222
|
|
|
213
223
|
const SKILL_INVOCATION_EXAMPLE = `# 例: Claude Code でスライド作成 → PPTX出力
|
|
214
224
|
|
|
@@ -243,8 +253,12 @@ const SKILL_AVAILABLE_LIST = [
|
|
|
243
253
|
},
|
|
244
254
|
];
|
|
245
255
|
|
|
246
|
-
const MCP_INSTALL_EXAMPLE = `# Claude Code
|
|
247
|
-
npx --yes --package html2pptx-local-mcp@latest html2pptx-install-mcp claude
|
|
256
|
+
const MCP_INSTALL_EXAMPLE = `# Claude Code / Codex / Grok Build ユーザーは該当する1行でOK
|
|
257
|
+
npx --yes --package html2pptx-local-mcp@latest html2pptx-install-mcp claude
|
|
258
|
+
npx --yes --package html2pptx-local-mcp@latest html2pptx-install-mcp codex
|
|
259
|
+
|
|
260
|
+
# Grok Build
|
|
261
|
+
npx --yes --package html2pptx-local-mcp@latest html2pptx-install-mcp grok`;
|
|
248
262
|
|
|
249
263
|
const MCP_CONFIG_EXAMPLE = `// 方法2: 設定ファイルに手動追加
|
|
250
264
|
|
|
@@ -749,7 +763,7 @@ export const DOCS_COPY = {
|
|
|
749
763
|
skillsDescription:
|
|
750
764
|
'Skills are packaged capabilities that extend AI coding agents with domain-specific knowledge and workflows. The html2pptx.app skill teaches your agent how to author slide-safe HTML, validate it against the PPTX conversion contract, optionally open the local visual editor, export through remote or local MCP workflows, and publish HTML template drafts through its built-in remote MCP publishing workflow. Install once, and your agent can convert natural language instructions into production-ready PowerPoint files or creator-owned HTML drafts.',
|
|
751
765
|
skillsHowItWorks: 'The skill bundles four core capabilities: (1) HTML authoring knowledge -- the rules for writing HTML/CSS that converts cleanly to editable PowerPoint, (2) MCP-based export automation -- connecting to the remote html2pptx.app MCP server or a local stdio MCP server to create jobs, poll status, and retrieve results, (3) local visual editing -- opening edit-slide through a localhost bridge when the user wants to inspect or tweak the HTML before export, and (4) template publishing -- requiring HTML drafts to go through the remote MCP validate/publish loop. The agent should ask before adding a local MCP server because that changes the user’s MCP configuration.',
|
|
752
|
-
skillsCompatibility: 'Works with 18+ AI agents including Claude Code, Cursor, GitHub Copilot, Windsurf, Cline, Codex, and more. Use the skills CLI command for
|
|
766
|
+
skillsCompatibility: 'Works with 18+ AI agents including Claude Code, Grok Build, Cursor, GitHub Copilot, Windsurf, Cline, Codex, and more. Use the skills CLI command for Claude Code, Codex, Cursor, and Windsurf. For Grok Build, use the Grok-specific skills installer shown below.',
|
|
753
767
|
skillsWorkflowTitle: 'Workflow',
|
|
754
768
|
skillsWorkflow: [
|
|
755
769
|
'Agent receives a user request (e.g., "Create a deck from these meeting notes")',
|
|
@@ -813,7 +827,7 @@ html2pptx edit ./html2pptx/slides.html --no-open`,
|
|
|
813
827
|
step: '1',
|
|
814
828
|
title: 'Install MCP + Skill',
|
|
815
829
|
icons: ['claude-code', 'codex', 'cursor', 'windsurf'],
|
|
816
|
-
body: 'Choose the command for the agent you actually use: Claude Code, Codex, Cursor, or Windsurf. For other supported agents or multiple targets, use the interactive selector. Avoid --yes unless you intentionally want to install into every detected agent directory.',
|
|
830
|
+
body: 'Choose the command for the agent you actually use: Claude Code, Codex, Cursor, or Windsurf. For Grok Build, use the Grok-specific installer. For other supported agents or multiple targets, use the interactive selector. Avoid --yes unless you intentionally want to install into every detected agent directory.',
|
|
817
831
|
code: SKILL_INSTALL_COMMAND_EN,
|
|
818
832
|
},
|
|
819
833
|
{
|
|
@@ -850,7 +864,7 @@ echo 'HTML2PPTX_API_KEY=sk_live_xxxx' >> .env`,
|
|
|
850
864
|
mcpDescription:
|
|
851
865
|
'MCP (Model Context Protocol) is an open protocol that exposes backend capabilities to AI agents through a standardized tool interface. html2pptx.app supports two MCP surfaces: the remote HTTP MCP endpoint at /mcp for export, usage, docs, templates, catalog, and HTML template publishing workflows, and the local stdio MCP server for export tools plus local edit-slide sessions through a localhost bridge. Use remote MCP when the agent needs to convert HTML to PPTX or create an HTML template draft. Use local stdio MCP only when the agent must open, preview, or edit a local HTML file on the user machine; local MCP does not publish templates.',
|
|
852
866
|
mcpSetupTitle: 'Installation & Setup',
|
|
853
|
-
mcpSetupLead: '
|
|
867
|
+
mcpSetupLead: 'Claude Code, Codex, and Grok Build users can run one command to register both remote export tools and local edit-slide. Remote MCP follows the hosted service; local MCP uses html2pptx-local-mcp@latest so future starts pick up the newest published package.',
|
|
854
868
|
mcpEditorTabs: [
|
|
855
869
|
{
|
|
856
870
|
id: 'claude-code',
|
|
@@ -864,16 +878,26 @@ echo 'HTML2PPTX_API_KEY=sk_live_xxxx' >> .env`,
|
|
|
864
878
|
],
|
|
865
879
|
tip: 'The installer registers the remote MCP with user scope and writes the local stdio MCP directly to Claude Code user config for better compatibility.',
|
|
866
880
|
},
|
|
881
|
+
{
|
|
882
|
+
id: 'grok-build',
|
|
883
|
+
label: 'Grok Build',
|
|
884
|
+
steps: [
|
|
885
|
+
{ title: 'Install MCP for Grok Build', body: 'Run the Grok installer. It registers remote export as `html2pptx` and local edit-slide as `html2pptx-local` through `grok mcp add`.', code: `npx --yes --package html2pptx-local-mcp@latest html2pptx-install-mcp grok` },
|
|
886
|
+
{ title: 'Install Skills for Grok Build', body: 'Install the published html2pptx skills into Grok Build’s native skills directory. This does not change MCP configuration.', code: `npx --yes --package html2pptx-local-mcp@latest html2pptx-install-skills grok` },
|
|
887
|
+
],
|
|
888
|
+
tip: 'Use `grok inspect` if you want to confirm which project skills and MCP servers Grok discovered.',
|
|
889
|
+
},
|
|
867
890
|
{
|
|
868
891
|
id: 'codex',
|
|
869
892
|
label: 'Codex',
|
|
870
893
|
icon: 'https://cdn.jsdelivr.net/gh/glincker/thesvg@main/public/icons/openai/light.svg',
|
|
871
894
|
steps: [
|
|
872
|
-
{ title: 'Run
|
|
895
|
+
{ title: 'Run one command (recommended)', body: 'This registers remote export as `html2pptx`, then adds local edit-slide as `html2pptx-local` using the published package.', code: `npx --yes --package html2pptx-local-mcp@latest html2pptx-install-mcp codex` },
|
|
873
896
|
{ title: 'Manual remote setup', body: 'If you only need hosted export tools, add the remote MCP server directly.', code: `codex mcp add html2pptx --url https://html2pptx.app/mcp` },
|
|
874
897
|
{ title: 'Optional local stdio MCP for edit-slide', body: 'Use this only when Codex needs to launch local edit-slide for a `.html` file. It runs from the published package, so no repository checkout is required.', code: `codex mcp add html2pptx-local -- npx --yes --package html2pptx-local-mcp@latest html2pptx-mcp` },
|
|
875
|
-
{ title: 'Manual setup via codex.json', body: 'Alternatively, create or edit codex.json in your project root.', code: `{\n "mcpServers": {\n "html2pptx": {\n "type": "url",\n "url": "https://html2pptx.app/mcp"\n }\n }\n}` },
|
|
898
|
+
{ title: 'Manual remote setup via codex.json', body: 'Alternatively, create or edit codex.json in your project root for the hosted remote server.', code: `{\n "mcpServers": {\n "html2pptx": {\n "type": "url",\n "url": "https://html2pptx.app/mcp"\n }\n }\n}` },
|
|
876
899
|
],
|
|
900
|
+
tip: 'The installer runs the two Codex MCP add commands in order and continues if either server is already registered.',
|
|
877
901
|
},
|
|
878
902
|
{
|
|
879
903
|
id: 'cursor',
|
|
@@ -1593,7 +1617,7 @@ echo 'HTML2PPTX_API_KEY=sk_live_xxxx' >> .env`,
|
|
|
1593
1617
|
skillsDescription:
|
|
1594
1618
|
'Skillsは、AIコーディングエージェントにドメイン固有の知識とワークフローを追加するパッケージ機能です。html2pptx.app のスキルをインストールすると、エージェントがスライド用HTMLの作成方法を理解し、PPTX変換契約に対する検証を行い、必要に応じてローカル Visual Editor を開き、remote MCP または local MCP のワークフローでエクスポートし、内蔵のremote MCP公開ワークフローでHTMLテンプレートdraftを作成できます。自然言語の指示だけで、本番品質のPowerPointファイルや作成者所有のHTML draftを生成できます。',
|
|
1595
1619
|
skillsHowItWorks: 'スキルには4つのコア機能がバンドルされています: (1) HTMLオーサリング知識 -- 高品質なPowerPointに変換されるHTML/CSSの書き方ルール、(2) MCPベースのエクスポート自動化 -- remote MCP または local stdio MCP に接続してジョブ作成・ステータスポーリング・結果取得を行う仕組み、(3) ローカル Visual Editor -- ユーザーがPPTX出力前にHTMLを目視確認・微調整したい場合に edit-slide を localhost bridge 経由で開く仕組み、(4) テンプレート公開 -- HTML draft を remote MCP の validate/publish ループに限定する仕組みです。local MCP の追加はユーザーのMCP設定を変更するため、エージェントは必ず事前に確認します。',
|
|
1596
|
-
skillsCompatibility: 'Claude Code、Cursor、GitHub Copilot、Windsurf、Cline、Codex 等 18以上のAI
|
|
1620
|
+
skillsCompatibility: 'Claude Code、Grok Build、Cursor、GitHub Copilot、Windsurf、Cline、Codex 等 18以上のAIエージェントに対応。Claude Code、Codex、Cursor、Windsurf は skills CLI コマンドを使います。Grok Build は下に表示している Grok 専用の skills installer を使います。',
|
|
1597
1621
|
skillsWorkflowTitle: 'ワークフロー',
|
|
1598
1622
|
skillsWorkflow: [
|
|
1599
1623
|
'エージェントがユーザーリクエストを受信(例: 「この会議メモからデッキを作成して」)',
|
|
@@ -1657,7 +1681,7 @@ html2pptx edit ./html2pptx/slides.html --no-open`,
|
|
|
1657
1681
|
step: '1',
|
|
1658
1682
|
title: 'MCP + スキルをインストール',
|
|
1659
1683
|
icons: ['claude-code', 'codex', 'cursor', 'windsurf'],
|
|
1660
|
-
body: 'Claude Code、Codex、Cursor、Windsurf
|
|
1684
|
+
body: 'Claude Code、Codex、Cursor、Windsurf のうち、実際に使うエージェントのコマンドを選んで実行してください。Grok Build は Grok 専用 installer を使います。その他の対応エージェントや複数指定の場合は対話形式で選択します。--yes は検出された全エージェントのディレクトリへ入るため、全対応が必要な場合以外は使わないでください。',
|
|
1661
1685
|
code: SKILL_INSTALL_COMMAND_JA,
|
|
1662
1686
|
},
|
|
1663
1687
|
{
|
|
@@ -1694,7 +1718,7 @@ echo 'HTML2PPTX_API_KEY=sk_live_xxxx' >> .env`,
|
|
|
1694
1718
|
mcpDescription:
|
|
1695
1719
|
'MCP (Model Context Protocol) は、標準化されたツールインターフェースを通じてAIエージェントにバックエンド機能を公開するオープンプロトコルです。html2pptx.app は2種類のMCPサーフェスを提供します。remote HTTP MCP (`/mcp`) はエクスポート、usage、docs、templates、catalog、HTMLテンプレート公開に使います。local stdio MCP は同じエクスポート系ツールに加えて、localhost bridge 経由でローカルHTMLを edit-slide で開くために使います。HTMLをPPTXに変換する、またはHTMLテンプレートdraftを作るなら remote MCP、ユーザーPC上の `.html` を開いてプレビュー・編集するなら local MCP を選びます。local MCP はテンプレート公開には使いません。',
|
|
1696
1720
|
mcpSetupTitle: 'インストール & セットアップ',
|
|
1697
|
-
mcpSetupLead: 'Claude Code
|
|
1721
|
+
mcpSetupLead: 'Claude Code、Codex、Grok Build は1コマンドで remote export と local edit-slide の両方を登録できます。remote MCP は hosted service 側の更新が反映され、local MCP は html2pptx-local-mcp@latest 経由で起動するため、次回起動時に新しい公開パッケージへ追従できます。',
|
|
1698
1722
|
mcpEditorTabs: [
|
|
1699
1723
|
{
|
|
1700
1724
|
id: 'claude-code',
|
|
@@ -1708,16 +1732,26 @@ echo 'HTML2PPTX_API_KEY=sk_live_xxxx' >> .env`,
|
|
|
1708
1732
|
],
|
|
1709
1733
|
tip: 'このインストーラーは remote MCP を user scope で登録し、互換性のため local stdio MCP は Claude Code の user config に直接書き込みます。',
|
|
1710
1734
|
},
|
|
1735
|
+
{
|
|
1736
|
+
id: 'grok-build',
|
|
1737
|
+
label: 'Grok Build',
|
|
1738
|
+
steps: [
|
|
1739
|
+
{ title: 'Grok Build 向けに MCP を追加', body: 'Grok installer を実行します。remote export は `html2pptx`、local edit-slide は `html2pptx-local` として `grok mcp add` で登録されます。', code: `npx --yes --package html2pptx-local-mcp@latest html2pptx-install-mcp grok` },
|
|
1740
|
+
{ title: 'Grok Build 向けに Skills を追加', body: '公開済みの html2pptx skills を Grok Build の native skills ディレクトリへ入れます。MCP設定は変更しません。', code: `npx --yes --package html2pptx-local-mcp@latest html2pptx-install-skills grok` },
|
|
1741
|
+
],
|
|
1742
|
+
tip: 'Grok が検出した project skills と MCP server を確認したい場合は `grok inspect` を使ってください。',
|
|
1743
|
+
},
|
|
1711
1744
|
{
|
|
1712
1745
|
id: 'codex',
|
|
1713
1746
|
label: 'Codex',
|
|
1714
1747
|
icon: 'https://cdn.jsdelivr.net/gh/glincker/thesvg@main/public/icons/openai/light.svg',
|
|
1715
1748
|
steps: [
|
|
1716
|
-
{ title: '
|
|
1749
|
+
{ title: '1コマンドで追加(推奨)', body: '`html2pptx` として remote export を登録し、`html2pptx-local` として local edit-slide を公開パッケージ経由で追加します。', code: `npx --yes --package html2pptx-local-mcp@latest html2pptx-install-mcp codex` },
|
|
1717
1750
|
{ title: 'remote だけ手動セットアップ', body: 'PPTX出力などのホスト側ツールだけ必要な場合はこちらを使います。', code: `codex mcp add html2pptx --url https://html2pptx.app/mcp` },
|
|
1718
1751
|
{ title: 'edit-slide 用 local stdio MCP(任意)', body: 'Codex がローカル `.html` を edit-slide で開く必要がある場合だけ追加します。公開パッケージから起動するため、リポジトリの checkout は不要です。', code: `codex mcp add html2pptx-local -- npx --yes --package html2pptx-local-mcp@latest html2pptx-mcp` },
|
|
1719
|
-
{ title: 'codex.json
|
|
1752
|
+
{ title: 'codex.json で remote を手動セットアップ', body: 'コマンドが使えない場合は、hosted remote server 用にプロジェクトルートの codex.json に以下を追加してください。', code: `{\n "mcpServers": {\n "html2pptx": {\n "type": "url",\n "url": "https://html2pptx.app/mcp"\n }\n }\n}` },
|
|
1720
1753
|
],
|
|
1754
|
+
tip: 'インストーラーは Codex の2つの MCP add コマンドを順に実行し、すでに登録済みの場合は継続します。',
|
|
1721
1755
|
},
|
|
1722
1756
|
{
|
|
1723
1757
|
id: 'cursor',
|
|
@@ -18,7 +18,7 @@ declare function normalizeBaseUrl(raw: string): URL;
|
|
|
18
18
|
declare function readRegisteredEditorBaseUrl(root: string): Promise<string | null>;
|
|
19
19
|
declare function resolveEditorBaseUrl(root: string, explicitBaseUrl: string | undefined): Promise<URL>;
|
|
20
20
|
declare function buildEditorUrl(baseUrl: URL, rel: string, bridgeUrl: string, sessionToken: string): URL;
|
|
21
|
-
declare function createBridgeServer(ctx: BridgeContext): import("http").Server<typeof IncomingMessage, typeof ServerResponse>;
|
|
21
|
+
declare function createBridgeServer(ctx: BridgeContext): import("node:http").Server<typeof IncomingMessage, typeof ServerResponse>;
|
|
22
22
|
declare function listen(server: ReturnType<typeof createBridgeServer>, requestedPort: number): Promise<number>;
|
|
23
23
|
export declare function editCommand(input: string | undefined, options?: EditOptions): Promise<void>;
|
|
24
24
|
export declare const editCommandInternalsForTest: {
|
|
@@ -8,11 +8,13 @@ import * as p from "@clack/prompts";
|
|
|
8
8
|
import pc from "picocolors";
|
|
9
9
|
const AUTO_PORT = 0;
|
|
10
10
|
const MAX_WRITE_BYTES = 5 * 1024 * 1024;
|
|
11
|
-
const MAX_ASSET_BYTES =
|
|
11
|
+
const MAX_ASSET_BYTES = 64 * 1024 * 1024;
|
|
12
12
|
const ALLOWED_EXTENSIONS = [".html", ".htm"];
|
|
13
13
|
const ALLOWED_EXT = new Set(ALLOWED_EXTENSIONS);
|
|
14
14
|
const ASSET_IMAGE_EXTENSIONS = [".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg", ".avif"];
|
|
15
|
-
const
|
|
15
|
+
const ASSET_VIDEO_EXTENSIONS = [".mp4", ".webm", ".mov", ".m4v", ".ogv"];
|
|
16
|
+
const ASSET_VIDEO_EXT = new Set(ASSET_VIDEO_EXTENSIONS);
|
|
17
|
+
const ASSET_MEDIA_EXT = new Set([...ASSET_IMAGE_EXTENSIONS, ...ASSET_VIDEO_EXTENSIONS]);
|
|
16
18
|
const ASSET_CONTENT_TYPES = {
|
|
17
19
|
".png": "image/png",
|
|
18
20
|
".jpg": "image/jpeg",
|
|
@@ -21,6 +23,11 @@ const ASSET_CONTENT_TYPES = {
|
|
|
21
23
|
".webp": "image/webp",
|
|
22
24
|
".svg": "image/svg+xml",
|
|
23
25
|
".avif": "image/avif",
|
|
26
|
+
".mp4": "video/mp4",
|
|
27
|
+
".webm": "video/webm",
|
|
28
|
+
".mov": "video/quicktime",
|
|
29
|
+
".m4v": "video/x-m4v",
|
|
30
|
+
".ogv": "video/ogg",
|
|
24
31
|
};
|
|
25
32
|
const ASSET_CONTENT_TYPE_EXT = {
|
|
26
33
|
"image/png": ".png",
|
|
@@ -29,6 +36,11 @@ const ASSET_CONTENT_TYPE_EXT = {
|
|
|
29
36
|
"image/webp": ".webp",
|
|
30
37
|
"image/svg+xml": ".svg",
|
|
31
38
|
"image/avif": ".avif",
|
|
39
|
+
"video/mp4": ".mp4",
|
|
40
|
+
"video/webm": ".webm",
|
|
41
|
+
"video/quicktime": ".mov",
|
|
42
|
+
"video/x-m4v": ".m4v",
|
|
43
|
+
"video/ogg": ".ogv",
|
|
32
44
|
};
|
|
33
45
|
const DISALLOWED_TOP_DIRECTORIES = [
|
|
34
46
|
"public",
|
|
@@ -460,8 +472,8 @@ async function safeAssetPath(ctx, rel) {
|
|
|
460
472
|
throw new Error("path escape");
|
|
461
473
|
}
|
|
462
474
|
const ext = extname(abs).toLowerCase();
|
|
463
|
-
if (!
|
|
464
|
-
throw new Error("only image files are allowed");
|
|
475
|
+
if (!ASSET_MEDIA_EXT.has(ext)) {
|
|
476
|
+
throw new Error("only image, GIF, and video files are allowed");
|
|
465
477
|
}
|
|
466
478
|
const real = await resolveReal(abs);
|
|
467
479
|
if (real !== ctx.root && !real.startsWith(ctx.root + sep)) {
|
|
@@ -477,18 +489,21 @@ async function safeAssetPath(ctx, rel) {
|
|
|
477
489
|
}
|
|
478
490
|
function assetExt(name, contentType) {
|
|
479
491
|
const fromName = extname(String(name || "")).toLowerCase();
|
|
480
|
-
if (
|
|
492
|
+
if (ASSET_MEDIA_EXT.has(fromName))
|
|
481
493
|
return fromName;
|
|
482
494
|
return ASSET_CONTENT_TYPE_EXT[String(contentType || "").toLowerCase()] || "";
|
|
483
495
|
}
|
|
484
496
|
function assetSlug(name) {
|
|
485
|
-
const base = String(name || "
|
|
497
|
+
const base = String(name || "asset").replace(/\.[^.]+$/, "");
|
|
486
498
|
const slug = base
|
|
487
499
|
.normalize("NFKD")
|
|
488
500
|
.replace(/[^\w.-]+/g, "-")
|
|
489
501
|
.replace(/^[-.]+|[-.]+$/g, "")
|
|
490
502
|
.toLowerCase();
|
|
491
|
-
return slug || "
|
|
503
|
+
return slug || "asset";
|
|
504
|
+
}
|
|
505
|
+
function assetKind(ext) {
|
|
506
|
+
return ASSET_VIDEO_EXT.has(ext) ? "video" : "image";
|
|
492
507
|
}
|
|
493
508
|
function assetDirRel(scope, htmlDirRel) {
|
|
494
509
|
if (scope === "global")
|
|
@@ -530,10 +545,11 @@ async function handleAssetGet(ctx, req, reqUrl, res) {
|
|
|
530
545
|
if (absDir === ctx.root || absDir.startsWith(ctx.root + sep)) {
|
|
531
546
|
const names = await readdir(absDir);
|
|
532
547
|
assets = names
|
|
533
|
-
.filter((name) =>
|
|
548
|
+
.filter((name) => ASSET_MEDIA_EXT.has(extname(name).toLowerCase()))
|
|
534
549
|
.map((name) => ({
|
|
535
550
|
name,
|
|
536
551
|
src: toPosixPath(relative(htmlDirAbs, join(absDir, name))),
|
|
552
|
+
kind: assetKind(extname(name).toLowerCase()),
|
|
537
553
|
}));
|
|
538
554
|
}
|
|
539
555
|
}
|
|
@@ -576,21 +592,21 @@ async function handleAssetPost(ctx, req, res) {
|
|
|
576
592
|
}
|
|
577
593
|
const { file, scope: rawScope, name, contentType, dataBase64 } = body || {};
|
|
578
594
|
if (typeof dataBase64 !== "string" || !dataBase64) {
|
|
579
|
-
sendJson(res, 400, { error: "missing
|
|
595
|
+
sendJson(res, 400, { error: "missing asset data" });
|
|
580
596
|
return;
|
|
581
597
|
}
|
|
582
598
|
const buf = Buffer.from(dataBase64, "base64");
|
|
583
599
|
if (!buf.length) {
|
|
584
|
-
sendJson(res, 400, { error: "empty
|
|
600
|
+
sendJson(res, 400, { error: "empty asset data" });
|
|
585
601
|
return;
|
|
586
602
|
}
|
|
587
603
|
if (buf.length > MAX_ASSET_BYTES) {
|
|
588
|
-
sendJson(res, 413, { error: "
|
|
604
|
+
sendJson(res, 413, { error: "asset too large (>64MB)" });
|
|
589
605
|
return;
|
|
590
606
|
}
|
|
591
607
|
const ext = assetExt(name, contentType);
|
|
592
|
-
if (!
|
|
593
|
-
sendJson(res, 400, { error: "unsupported
|
|
608
|
+
if (!ASSET_MEDIA_EXT.has(ext)) {
|
|
609
|
+
sendJson(res, 400, { error: "unsupported asset type" });
|
|
594
610
|
return;
|
|
595
611
|
}
|
|
596
612
|
const scope = rawScope === "global" ? "global" : "project";
|
|
@@ -617,6 +633,7 @@ async function handleAssetPost(ctx, req, res) {
|
|
|
617
633
|
name: fileName,
|
|
618
634
|
path: toPosixPath(relative(ctx.root, absTarget)),
|
|
619
635
|
src: toPosixPath(relative(htmlDirAbs, absTarget)),
|
|
636
|
+
kind: assetKind(ext),
|
|
620
637
|
bytes: buf.length,
|
|
621
638
|
reused: alreadyIdentical,
|
|
622
639
|
policy: buildPolicy(ctx),
|
package/cli/dist/index.js
CHANGED
|
File without changes
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import net from 'node:net';
|
|
4
|
+
import { access } from 'node:fs/promises';
|
|
5
|
+
import { dirname, join, resolve } from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import {
|
|
8
|
+
readEditorServerState,
|
|
9
|
+
writeEditorServerState,
|
|
10
|
+
} from './local-editor-state.js';
|
|
11
|
+
|
|
12
|
+
const AUTO_PORT = 0;
|
|
13
|
+
const DEFAULT_READY_TIMEOUT_MS = 30000;
|
|
14
|
+
|
|
15
|
+
export function createLocalEditorServerManager(options = {}) {
|
|
16
|
+
const sessions = new Map();
|
|
17
|
+
const launchTimeoutMs = Number.isFinite(options.launchTimeoutMs)
|
|
18
|
+
? Math.max(1000, Math.floor(options.launchTimeoutMs))
|
|
19
|
+
: DEFAULT_READY_TIMEOUT_MS;
|
|
20
|
+
|
|
21
|
+
async function ensure(root = process.cwd(), input = {}) {
|
|
22
|
+
const projectRoot = resolve(root);
|
|
23
|
+
const requestedPort = normalizePort(input.editorPort ?? process.env.HTML2PPTX_STUDIO_PORT ?? process.env.PORT, AUTO_PORT);
|
|
24
|
+
|
|
25
|
+
const registered = await readEditorServerState(projectRoot);
|
|
26
|
+
if (registered && await isEditorServer(registered.port)) {
|
|
27
|
+
return {
|
|
28
|
+
baseUrl: registered.baseUrl,
|
|
29
|
+
port: registered.port,
|
|
30
|
+
reused: true,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const existing = sessions.get(projectRoot);
|
|
35
|
+
if (existing && !existing.child.killed && existing.child.exitCode == null && await isEditorServer(existing.port)) {
|
|
36
|
+
await writeEditorServerState(projectRoot, {
|
|
37
|
+
baseUrl: existing.baseUrl,
|
|
38
|
+
port: existing.port,
|
|
39
|
+
pid: existing.child.pid,
|
|
40
|
+
});
|
|
41
|
+
return {
|
|
42
|
+
baseUrl: existing.baseUrl,
|
|
43
|
+
port: existing.port,
|
|
44
|
+
reused: true,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (requestedPort !== AUTO_PORT && await isEditorServer(requestedPort)) {
|
|
49
|
+
const baseUrl = `http://localhost:${requestedPort}`;
|
|
50
|
+
await writeEditorServerState(projectRoot, {
|
|
51
|
+
baseUrl,
|
|
52
|
+
port: requestedPort,
|
|
53
|
+
pid: null,
|
|
54
|
+
});
|
|
55
|
+
return {
|
|
56
|
+
baseUrl,
|
|
57
|
+
port: requestedPort,
|
|
58
|
+
reused: true,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const appRoot = await resolveEditorAppRoot(options);
|
|
63
|
+
const port = requestedPort === AUTO_PORT ? await getEphemeralPort() : await pickOpenPort(requestedPort);
|
|
64
|
+
const baseUrl = `http://localhost:${port}`;
|
|
65
|
+
const invocation = await resolveNextInvocation(appRoot, options);
|
|
66
|
+
|
|
67
|
+
const child = spawn(invocation.command, [
|
|
68
|
+
...invocation.baseArgs,
|
|
69
|
+
'dev',
|
|
70
|
+
'--webpack',
|
|
71
|
+
'-p',
|
|
72
|
+
String(port),
|
|
73
|
+
], {
|
|
74
|
+
cwd: appRoot,
|
|
75
|
+
env: {
|
|
76
|
+
...process.env,
|
|
77
|
+
HTML2PPTX_EDITOR_BASE_URL: baseUrl,
|
|
78
|
+
NEXT_TELEMETRY_DISABLED: '1',
|
|
79
|
+
PORT: String(port),
|
|
80
|
+
},
|
|
81
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const session = {
|
|
85
|
+
child,
|
|
86
|
+
appRoot,
|
|
87
|
+
baseUrl,
|
|
88
|
+
port,
|
|
89
|
+
stdout: '',
|
|
90
|
+
stderr: '',
|
|
91
|
+
startedAt: new Date().toISOString(),
|
|
92
|
+
};
|
|
93
|
+
sessions.set(projectRoot, session);
|
|
94
|
+
|
|
95
|
+
child.stdout.setEncoding('utf8');
|
|
96
|
+
child.stderr.setEncoding('utf8');
|
|
97
|
+
child.stdout.on('data', (chunk) => {
|
|
98
|
+
session.stdout += chunk;
|
|
99
|
+
});
|
|
100
|
+
child.stderr.on('data', (chunk) => {
|
|
101
|
+
session.stderr += chunk;
|
|
102
|
+
});
|
|
103
|
+
child.once('exit', () => {
|
|
104
|
+
if (sessions.get(projectRoot) === session) sessions.delete(projectRoot);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
await waitForEditorServer(session, launchTimeoutMs);
|
|
108
|
+
await writeEditorServerState(projectRoot, {
|
|
109
|
+
baseUrl,
|
|
110
|
+
port,
|
|
111
|
+
pid: child.pid,
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
baseUrl,
|
|
116
|
+
port,
|
|
117
|
+
pid: child.pid,
|
|
118
|
+
reused: false,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function stopAll() {
|
|
123
|
+
for (const session of sessions.values()) {
|
|
124
|
+
session.child.kill('SIGTERM');
|
|
125
|
+
}
|
|
126
|
+
sessions.clear();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
ensure,
|
|
131
|
+
stopAll,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export const localEditorServerManager = createLocalEditorServerManager();
|
|
136
|
+
|
|
137
|
+
export async function ensureLocalEditorServer(root = process.cwd(), options = {}) {
|
|
138
|
+
return localEditorServerManager.ensure(root, options);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export async function resolveEditorAppRoot(options = {}) {
|
|
142
|
+
if (options.appRoot) return resolve(options.appRoot);
|
|
143
|
+
if (process.env.HTML2PPTX_EDITOR_APP_ROOT) return resolve(process.env.HTML2PPTX_EDITOR_APP_ROOT);
|
|
144
|
+
|
|
145
|
+
const packageRoot = fileURLToPath(new URL('..', import.meta.url));
|
|
146
|
+
const packagedAppRoot = join(packageRoot, 'local-editor-app');
|
|
147
|
+
if (existsSync(join(packagedAppRoot, 'app', 'edit-slide', 'page.jsx'))) {
|
|
148
|
+
return packagedAppRoot;
|
|
149
|
+
}
|
|
150
|
+
if (existsSync(join(packageRoot, 'app', 'edit-slide', 'page.jsx'))) {
|
|
151
|
+
return packageRoot;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
throw new Error(
|
|
155
|
+
'html2pptx local editor app is not bundled. Reinstall html2pptx-local-mcp or pass HTML2PPTX_EDITOR_APP_ROOT.',
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export async function resolveNextInvocation(appRoot, options = {}) {
|
|
160
|
+
if (options.command) {
|
|
161
|
+
return {
|
|
162
|
+
command: options.command,
|
|
163
|
+
baseArgs: Array.isArray(options.baseArgs) ? options.baseArgs : [],
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
if (process.env.HTML2PPTX_NEXT_BIN) {
|
|
167
|
+
return {
|
|
168
|
+
command: process.execPath,
|
|
169
|
+
baseArgs: [process.env.HTML2PPTX_NEXT_BIN],
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const nextBin = await findUp(join('node_modules', 'next', 'dist', 'bin', 'next'), appRoot);
|
|
174
|
+
if (nextBin) {
|
|
175
|
+
return {
|
|
176
|
+
command: process.execPath,
|
|
177
|
+
baseArgs: [nextBin],
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
command: 'npx',
|
|
183
|
+
baseArgs: ['--yes', 'next'],
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export async function isEditorServer(port) {
|
|
188
|
+
const controller = new AbortController();
|
|
189
|
+
const timer = setTimeout(() => controller.abort(), 1500);
|
|
190
|
+
try {
|
|
191
|
+
const response = await fetch(`http://127.0.0.1:${port}/api/edit-slide/local-health`, {
|
|
192
|
+
headers: { accept: 'application/json' },
|
|
193
|
+
signal: controller.signal,
|
|
194
|
+
});
|
|
195
|
+
if (!response.ok) return false;
|
|
196
|
+
const payload = await response.json();
|
|
197
|
+
return payload?.app === 'html2pptx-local-editor' && payload?.ok === true;
|
|
198
|
+
} catch {
|
|
199
|
+
return false;
|
|
200
|
+
} finally {
|
|
201
|
+
clearTimeout(timer);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function waitForEditorServer(session, timeoutMs) {
|
|
206
|
+
return new Promise((resolvePromise, rejectPromise) => {
|
|
207
|
+
const startedAt = Date.now();
|
|
208
|
+
const tick = async () => {
|
|
209
|
+
if (await isEditorServer(session.port)) {
|
|
210
|
+
resolvePromise();
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
if (session.child.exitCode != null || session.child.killed) {
|
|
214
|
+
rejectPromise(new Error([
|
|
215
|
+
`html2pptx editor server exited before it was ready on ${session.baseUrl}.`,
|
|
216
|
+
session.stderr.trim() || session.stdout.trim(),
|
|
217
|
+
].filter(Boolean).join(' ')));
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
if (Date.now() - startedAt >= timeoutMs) {
|
|
221
|
+
session.child.kill('SIGTERM');
|
|
222
|
+
rejectPromise(new Error([
|
|
223
|
+
`Timed out after ${timeoutMs}ms while starting html2pptx editor server on ${session.baseUrl}.`,
|
|
224
|
+
session.stderr.trim(),
|
|
225
|
+
].filter(Boolean).join(' ')));
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
setTimeout(tick, 350);
|
|
229
|
+
};
|
|
230
|
+
tick();
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async function findUp(relativePath, startDir) {
|
|
235
|
+
let dir = resolve(startDir);
|
|
236
|
+
while (true) {
|
|
237
|
+
const candidate = join(dir, relativePath);
|
|
238
|
+
if (await exists(candidate)) return candidate;
|
|
239
|
+
const parent = dirname(dir);
|
|
240
|
+
if (parent === dir) return null;
|
|
241
|
+
dir = parent;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
async function exists(path) {
|
|
246
|
+
try {
|
|
247
|
+
await access(path);
|
|
248
|
+
return true;
|
|
249
|
+
} catch {
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
async function pickOpenPort(preferredPort) {
|
|
255
|
+
if (!(await isPortOpen(preferredPort))) return preferredPort;
|
|
256
|
+
|
|
257
|
+
for (let port = preferredPort + 1; port < preferredPort + 100; port += 1) {
|
|
258
|
+
if (!(await isPortOpen(port))) return port;
|
|
259
|
+
}
|
|
260
|
+
return getEphemeralPort();
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function getEphemeralPort() {
|
|
264
|
+
return new Promise((resolvePromise, rejectPromise) => {
|
|
265
|
+
const server = net.createServer();
|
|
266
|
+
server.once('error', rejectPromise);
|
|
267
|
+
server.listen(0, '127.0.0.1', () => {
|
|
268
|
+
const address = server.address();
|
|
269
|
+
server.close(() => {
|
|
270
|
+
if (!address || typeof address === 'string') {
|
|
271
|
+
rejectPromise(new Error('Failed to allocate an editor port.'));
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
resolvePromise(address.port);
|
|
275
|
+
});
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function isPortOpen(port) {
|
|
281
|
+
return new Promise((resolvePromise) => {
|
|
282
|
+
const socket = net.createConnection({ host: '127.0.0.1', port });
|
|
283
|
+
socket.once('connect', () => {
|
|
284
|
+
socket.end();
|
|
285
|
+
resolvePromise(true);
|
|
286
|
+
});
|
|
287
|
+
socket.once('error', () => {
|
|
288
|
+
socket.destroy();
|
|
289
|
+
resolvePromise(false);
|
|
290
|
+
});
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function normalizePort(value, fallback) {
|
|
295
|
+
if (value == null || value === '') return fallback;
|
|
296
|
+
const port = Number.parseInt(String(value), 10);
|
|
297
|
+
if (!Number.isInteger(port) || port < 0 || port > 65535) {
|
|
298
|
+
throw new Error('editorPort must be an integer from 0 to 65535.');
|
|
299
|
+
}
|
|
300
|
+
return port;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function installExitHandlers() {
|
|
304
|
+
const stop = () => localEditorServerManager.stopAll();
|
|
305
|
+
process.once('exit', stop);
|
|
306
|
+
process.once('SIGINT', () => {
|
|
307
|
+
stop();
|
|
308
|
+
process.exit(130);
|
|
309
|
+
});
|
|
310
|
+
process.once('SIGTERM', () => {
|
|
311
|
+
stop();
|
|
312
|
+
process.exit(143);
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
installExitHandlers();
|