html2pptx-local-mcp 1.1.20 → 1.1.22
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 +52 -16
- 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/cli/dist/commands/config-show.d.ts +0 -1
- package/cli/dist/commands/config-show.js +0 -16
- package/cli/dist/commands/convert.d.ts +0 -10
- package/cli/dist/commands/convert.js +0 -311
- package/cli/dist/commands/edit.d.ts +0 -34
- package/cli/dist/commands/edit.js +0 -801
- package/cli/dist/commands/init.d.ts +0 -1
- package/cli/dist/commands/init.js +0 -35
- package/cli/dist/commands/logout.d.ts +0 -1
- package/cli/dist/commands/logout.js +0 -19
- package/cli/dist/commands/publish.d.ts +0 -10
- package/cli/dist/commands/publish.js +0 -17
- package/cli/dist/commands/status.d.ts +0 -5
- package/cli/dist/commands/status.js +0 -71
- package/cli/dist/commands/templates.d.ts +0 -13
- package/cli/dist/commands/templates.js +0 -85
- package/cli/dist/commands/whoami.d.ts +0 -5
- package/cli/dist/commands/whoami.js +0 -51
- package/cli/dist/config.d.ts +0 -7
- package/cli/dist/config.js +0 -24
- package/cli/dist/index.d.ts +0 -2
- package/cli/dist/index.js +0 -93
- package/cli/dist/update-check.d.ts +0 -1
- package/cli/dist/update-check.js +0 -30
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,27 @@ 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
|
+
icon: 'https://cdn.jsdelivr.net/gh/glincker/thesvg@main/public/icons/grok/light.svg',
|
|
885
|
+
steps: [
|
|
886
|
+
{ 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` },
|
|
887
|
+
{ 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` },
|
|
888
|
+
],
|
|
889
|
+
tip: 'Use `grok inspect` if you want to confirm which project skills and MCP servers Grok discovered.',
|
|
890
|
+
},
|
|
867
891
|
{
|
|
868
892
|
id: 'codex',
|
|
869
893
|
label: 'Codex',
|
|
870
894
|
icon: 'https://cdn.jsdelivr.net/gh/glincker/thesvg@main/public/icons/openai/light.svg',
|
|
871
895
|
steps: [
|
|
872
|
-
{ title: 'Run
|
|
896
|
+
{ 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
897
|
{ 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
898
|
{ 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}` },
|
|
899
|
+
{ 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
900
|
],
|
|
901
|
+
tip: 'The installer runs the two Codex MCP add commands in order and continues if either server is already registered.',
|
|
877
902
|
},
|
|
878
903
|
{
|
|
879
904
|
id: 'cursor',
|
|
@@ -1593,7 +1618,7 @@ echo 'HTML2PPTX_API_KEY=sk_live_xxxx' >> .env`,
|
|
|
1593
1618
|
skillsDescription:
|
|
1594
1619
|
'Skillsは、AIコーディングエージェントにドメイン固有の知識とワークフローを追加するパッケージ機能です。html2pptx.app のスキルをインストールすると、エージェントがスライド用HTMLの作成方法を理解し、PPTX変換契約に対する検証を行い、必要に応じてローカル Visual Editor を開き、remote MCP または local MCP のワークフローでエクスポートし、内蔵のremote MCP公開ワークフローでHTMLテンプレートdraftを作成できます。自然言語の指示だけで、本番品質のPowerPointファイルや作成者所有のHTML draftを生成できます。',
|
|
1595
1620
|
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
|
|
1621
|
+
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
1622
|
skillsWorkflowTitle: 'ワークフロー',
|
|
1598
1623
|
skillsWorkflow: [
|
|
1599
1624
|
'エージェントがユーザーリクエストを受信(例: 「この会議メモからデッキを作成して」)',
|
|
@@ -1657,7 +1682,7 @@ html2pptx edit ./html2pptx/slides.html --no-open`,
|
|
|
1657
1682
|
step: '1',
|
|
1658
1683
|
title: 'MCP + スキルをインストール',
|
|
1659
1684
|
icons: ['claude-code', 'codex', 'cursor', 'windsurf'],
|
|
1660
|
-
body: 'Claude Code、Codex、Cursor、Windsurf
|
|
1685
|
+
body: 'Claude Code、Codex、Cursor、Windsurf のうち、実際に使うエージェントのコマンドを選んで実行してください。Grok Build は Grok 専用 installer を使います。その他の対応エージェントや複数指定の場合は対話形式で選択します。--yes は検出された全エージェントのディレクトリへ入るため、全対応が必要な場合以外は使わないでください。',
|
|
1661
1686
|
code: SKILL_INSTALL_COMMAND_JA,
|
|
1662
1687
|
},
|
|
1663
1688
|
{
|
|
@@ -1694,7 +1719,7 @@ echo 'HTML2PPTX_API_KEY=sk_live_xxxx' >> .env`,
|
|
|
1694
1719
|
mcpDescription:
|
|
1695
1720
|
'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
1721
|
mcpSetupTitle: 'インストール & セットアップ',
|
|
1697
|
-
mcpSetupLead: 'Claude Code
|
|
1722
|
+
mcpSetupLead: 'Claude Code、Codex、Grok Build は1コマンドで remote export と local edit-slide の両方を登録できます。remote MCP は hosted service 側の更新が反映され、local MCP は html2pptx-local-mcp@latest 経由で起動するため、次回起動時に新しい公開パッケージへ追従できます。',
|
|
1698
1723
|
mcpEditorTabs: [
|
|
1699
1724
|
{
|
|
1700
1725
|
id: 'claude-code',
|
|
@@ -1708,16 +1733,27 @@ echo 'HTML2PPTX_API_KEY=sk_live_xxxx' >> .env`,
|
|
|
1708
1733
|
],
|
|
1709
1734
|
tip: 'このインストーラーは remote MCP を user scope で登録し、互換性のため local stdio MCP は Claude Code の user config に直接書き込みます。',
|
|
1710
1735
|
},
|
|
1736
|
+
{
|
|
1737
|
+
id: 'grok-build',
|
|
1738
|
+
label: 'Grok Build',
|
|
1739
|
+
icon: 'https://cdn.jsdelivr.net/gh/glincker/thesvg@main/public/icons/grok/light.svg',
|
|
1740
|
+
steps: [
|
|
1741
|
+
{ 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` },
|
|
1742
|
+
{ title: 'Grok Build 向けに Skills を追加', body: '公開済みの html2pptx skills を Grok Build の native skills ディレクトリへ入れます。MCP設定は変更しません。', code: `npx --yes --package html2pptx-local-mcp@latest html2pptx-install-skills grok` },
|
|
1743
|
+
],
|
|
1744
|
+
tip: 'Grok が検出した project skills と MCP server を確認したい場合は `grok inspect` を使ってください。',
|
|
1745
|
+
},
|
|
1711
1746
|
{
|
|
1712
1747
|
id: 'codex',
|
|
1713
1748
|
label: 'Codex',
|
|
1714
1749
|
icon: 'https://cdn.jsdelivr.net/gh/glincker/thesvg@main/public/icons/openai/light.svg',
|
|
1715
1750
|
steps: [
|
|
1716
|
-
{ title: '
|
|
1751
|
+
{ title: '1コマンドで追加(推奨)', body: '`html2pptx` として remote export を登録し、`html2pptx-local` として local edit-slide を公開パッケージ経由で追加します。', code: `npx --yes --package html2pptx-local-mcp@latest html2pptx-install-mcp codex` },
|
|
1717
1752
|
{ title: 'remote だけ手動セットアップ', body: 'PPTX出力などのホスト側ツールだけ必要な場合はこちらを使います。', code: `codex mcp add html2pptx --url https://html2pptx.app/mcp` },
|
|
1718
1753
|
{ 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
|
|
1754
|
+
{ 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
1755
|
],
|
|
1756
|
+
tip: 'インストーラーは Codex の2つの MCP add コマンドを順に実行し、すでに登録済みの場合は継続します。',
|
|
1721
1757
|
},
|
|
1722
1758
|
{
|
|
1723
1759
|
id: 'cursor',
|
|
@@ -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();
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
|
|
4
|
+
export const EDITOR_SERVER_STATE_FILE = '.html2pptx/edit-slide/editor-server.json';
|
|
5
|
+
export const LEGACY_EDITOR_SERVER_STATE_FILE = '.open-slide/editor-server.json';
|
|
6
|
+
|
|
7
|
+
export async function readEditorServerState(root = process.cwd()) {
|
|
8
|
+
for (const stateFile of [EDITOR_SERVER_STATE_FILE, LEGACY_EDITOR_SERVER_STATE_FILE]) {
|
|
9
|
+
try {
|
|
10
|
+
const raw = await readFile(join(root, stateFile), 'utf8');
|
|
11
|
+
const state = JSON.parse(raw);
|
|
12
|
+
const port = Number.parseInt(state?.port, 10);
|
|
13
|
+
const baseUrl = typeof state?.baseUrl === 'string' ? state.baseUrl : '';
|
|
14
|
+
if (!Number.isInteger(port) || port <= 0 || !baseUrl.startsWith('http://')) continue;
|
|
15
|
+
return {
|
|
16
|
+
...state,
|
|
17
|
+
port,
|
|
18
|
+
baseUrl,
|
|
19
|
+
};
|
|
20
|
+
} catch {
|
|
21
|
+
// Try the next known state location.
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function readRegisteredEditorBaseUrl(root = process.cwd()) {
|
|
28
|
+
const state = await readEditorServerState(root);
|
|
29
|
+
return state?.baseUrl || null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function writeEditorServerState(root, { baseUrl, port, pid, managedBy = 'html2pptx-local-mcp' }) {
|
|
33
|
+
await mkdir(join(root, '.html2pptx', 'edit-slide'), { recursive: true });
|
|
34
|
+
await writeFile(
|
|
35
|
+
join(root, EDITOR_SERVER_STATE_FILE),
|
|
36
|
+
`${JSON.stringify({
|
|
37
|
+
baseUrl,
|
|
38
|
+
port,
|
|
39
|
+
pid,
|
|
40
|
+
managedBy,
|
|
41
|
+
updatedAt: new Date().toISOString(),
|
|
42
|
+
}, null, 2)}\n`,
|
|
43
|
+
'utf8',
|
|
44
|
+
);
|
|
45
|
+
}
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
3
|
import { existsSync } from 'node:fs';
|
|
4
|
-
import { access,
|
|
5
|
-
import { dirname, extname,
|
|
4
|
+
import { access, realpath, stat } from 'node:fs/promises';
|
|
5
|
+
import { dirname, extname, relative, resolve, sep } from 'node:path';
|
|
6
6
|
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { ensureLocalEditorServer } from './local-editor-server.js';
|
|
7
8
|
|
|
8
9
|
const ALLOWED_EXTENSIONS = new Set(['.html', '.htm']);
|
|
9
|
-
const EDITOR_SERVER_STATE_FILE = '.html2pptx/edit-slide/editor-server.json';
|
|
10
|
-
const LEGACY_EDITOR_SERVER_STATE_FILE = '.open-slide/editor-server.json';
|
|
11
10
|
const DEFAULT_LAUNCH_TIMEOUT_MS = 10000;
|
|
12
11
|
|
|
13
12
|
export function createLocalSlideEditorManager(options = {}) {
|
|
@@ -33,7 +32,12 @@ export function createLocalSlideEditorManager(options = {}) {
|
|
|
33
32
|
}
|
|
34
33
|
|
|
35
34
|
const baseUrl = normalizeEditorBaseUrl(
|
|
36
|
-
input.baseUrl ||
|
|
35
|
+
input.baseUrl ||
|
|
36
|
+
process.env.HTML2PPTX_EDITOR_BASE_URL ||
|
|
37
|
+
await resolveEditorBaseUrl(file.root, {
|
|
38
|
+
...input,
|
|
39
|
+
ensureEditorServer: options.ensureEditorServer,
|
|
40
|
+
}),
|
|
37
41
|
);
|
|
38
42
|
const port = normalizePort(input.port, 0);
|
|
39
43
|
const openBrowser = input.openBrowser === true;
|
|
@@ -148,6 +152,14 @@ export function createLocalSlideEditorManager(options = {}) {
|
|
|
148
152
|
|
|
149
153
|
export const localSlideEditorManager = createLocalSlideEditorManager();
|
|
150
154
|
|
|
155
|
+
async function resolveEditorBaseUrl(root, input = {}) {
|
|
156
|
+
const ensureEditorServer = typeof input.ensureEditorServer === 'function'
|
|
157
|
+
? input.ensureEditorServer
|
|
158
|
+
: ensureLocalEditorServer;
|
|
159
|
+
const ensured = await ensureEditorServer(root, input);
|
|
160
|
+
return ensured.baseUrl;
|
|
161
|
+
}
|
|
162
|
+
|
|
151
163
|
export async function resolveEditableFile(filePath, cwd = process.cwd()) {
|
|
152
164
|
if (typeof filePath !== 'string' || filePath.trim() === '') {
|
|
153
165
|
throw new Error('filePath must be a non-empty .html or .htm path.');
|
|
@@ -305,7 +317,7 @@ export function normalizeEditorBaseUrl(raw) {
|
|
|
305
317
|
try {
|
|
306
318
|
if (!raw) {
|
|
307
319
|
throw new Error(
|
|
308
|
-
'Local editor UI is not
|
|
320
|
+
'Local editor UI is not available. Reinstall html2pptx-local-mcp or pass baseUrl as a loopback editor URL.',
|
|
309
321
|
);
|
|
310
322
|
}
|
|
311
323
|
const url = new URL(raw);
|
|
@@ -330,18 +342,7 @@ function isAllowedEditorBaseUrl(url) {
|
|
|
330
342
|
return isLoopbackHostname(url.hostname);
|
|
331
343
|
}
|
|
332
344
|
|
|
333
|
-
export
|
|
334
|
-
for (const stateFile of [EDITOR_SERVER_STATE_FILE, LEGACY_EDITOR_SERVER_STATE_FILE]) {
|
|
335
|
-
try {
|
|
336
|
-
const raw = await readFile(join(root, stateFile), 'utf8');
|
|
337
|
-
const state = JSON.parse(raw);
|
|
338
|
-
if (typeof state?.baseUrl === 'string') return state.baseUrl;
|
|
339
|
-
} catch {
|
|
340
|
-
// Try the next known state location.
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
return null;
|
|
344
|
-
}
|
|
345
|
+
export { readRegisteredEditorBaseUrl } from './local-editor-state.js';
|
|
345
346
|
|
|
346
347
|
function isLoopbackHostname(hostname) {
|
|
347
348
|
const host = String(hostname || '').replace(/^\[|\]$/g, '').toLowerCase();
|