html2pptx-local-mcp 1.1.29 → 1.1.32
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/cli/dist/commands/edit.d.ts +1 -2
- package/cli/dist/dist/commands/config-show.d.ts +1 -0
- package/cli/dist/dist/commands/config-show.js +16 -0
- package/cli/dist/dist/commands/convert.d.ts +10 -0
- package/cli/dist/dist/commands/convert.js +311 -0
- package/cli/dist/dist/commands/edit.d.ts +34 -0
- package/cli/dist/dist/commands/edit.js +801 -0
- package/cli/dist/dist/commands/init.d.ts +1 -0
- package/cli/dist/dist/commands/init.js +35 -0
- package/cli/dist/dist/commands/logout.d.ts +1 -0
- package/cli/dist/dist/commands/logout.js +19 -0
- package/cli/dist/dist/commands/publish.d.ts +10 -0
- package/cli/dist/dist/commands/publish.js +17 -0
- package/cli/dist/dist/commands/status.d.ts +5 -0
- package/cli/dist/dist/commands/status.js +71 -0
- package/cli/dist/dist/commands/templates.d.ts +13 -0
- package/cli/dist/dist/commands/templates.js +85 -0
- package/cli/dist/dist/commands/whoami.d.ts +5 -0
- package/cli/dist/dist/commands/whoami.js +51 -0
- package/cli/dist/dist/config.d.ts +7 -0
- package/cli/dist/dist/config.js +24 -0
- package/cli/dist/dist/index.d.ts +2 -0
- package/cli/dist/dist/index.js +93 -0
- package/cli/dist/dist/update-check.d.ts +1 -0
- package/cli/dist/dist/update-check.js +30 -0
- package/lib/pptx-studio-mcp-core.js +11 -13
- package/lib/self-heal-skills.js +124 -0
- package/mcp/pptx-studio-mcp-server.mjs +5 -0
- package/package.json +2 -1
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { type IncomingMessage, type ServerResponse } from "node:http";
|
|
2
1
|
export interface EditOptions {
|
|
3
2
|
port?: string;
|
|
4
3
|
baseUrl?: string;
|
|
@@ -18,7 +17,7 @@ declare function normalizeBaseUrl(raw: string): URL;
|
|
|
18
17
|
declare function readRegisteredEditorBaseUrl(root: string): Promise<string | null>;
|
|
19
18
|
declare function resolveEditorBaseUrl(root: string, explicitBaseUrl: string | undefined): Promise<URL>;
|
|
20
19
|
declare function buildEditorUrl(baseUrl: URL, rel: string, bridgeUrl: string, sessionToken: string): URL;
|
|
21
|
-
declare function createBridgeServer(ctx: BridgeContext):
|
|
20
|
+
declare function createBridgeServer(ctx: BridgeContext): any;
|
|
22
21
|
declare function listen(server: ReturnType<typeof createBridgeServer>, requestedPort: number): Promise<number>;
|
|
23
22
|
export declare function editCommand(input: string | undefined, options?: EditOptions): Promise<void>;
|
|
24
23
|
export declare const editCommandInternalsForTest: {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function configShowCommand(): Promise<void>;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import * as p from "@clack/prompts";
|
|
2
|
+
import pc from "picocolors";
|
|
3
|
+
import { loadConfig, getConfigPath } from "../config.js";
|
|
4
|
+
export async function configShowCommand() {
|
|
5
|
+
const config = await loadConfig();
|
|
6
|
+
const configPath = getConfigPath();
|
|
7
|
+
p.log.info(pc.bold("Config file: ") + pc.dim(configPath));
|
|
8
|
+
if (!config.apiKey) {
|
|
9
|
+
p.log.warn("No configuration found. Run " + pc.cyan("html2pptx login") + " to set up.");
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
p.log.info(pc.bold("API Key: ") +
|
|
13
|
+
pc.dim(config.apiKey.slice(0, 12) + "..." + config.apiKey.slice(-4)));
|
|
14
|
+
p.log.info(pc.bold("API Base URL: ") +
|
|
15
|
+
(config.baseUrl ?? "https://html2pptx.app") + pc.dim(" (default)"));
|
|
16
|
+
}
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { exec } from "node:child_process";
|
|
3
|
+
import { platform } from "node:os";
|
|
4
|
+
import { basename, resolve } from "node:path";
|
|
5
|
+
import * as p from "@clack/prompts";
|
|
6
|
+
import pc from "picocolors";
|
|
7
|
+
import { loadConfig } from "../config.js";
|
|
8
|
+
function parseSize(size) {
|
|
9
|
+
if (size === "16:9")
|
|
10
|
+
return { layout: "LAYOUT_16x9" };
|
|
11
|
+
if (size === "4:3")
|
|
12
|
+
return { layout: "LAYOUT_4x3" };
|
|
13
|
+
const match = size.match(/^(\d+)x(\d+)$/);
|
|
14
|
+
if (match)
|
|
15
|
+
return { width: parseInt(match[1]), height: parseInt(match[2]) };
|
|
16
|
+
return { layout: "LAYOUT_16x9" };
|
|
17
|
+
}
|
|
18
|
+
function formatDuration(ms) {
|
|
19
|
+
if (ms < 1000)
|
|
20
|
+
return `${ms}ms`;
|
|
21
|
+
return `${(ms / 1000).toFixed(1)}s`;
|
|
22
|
+
}
|
|
23
|
+
function openFile(filePath) {
|
|
24
|
+
const os = platform();
|
|
25
|
+
const cmd = os === "darwin" ? "open" : os === "win32" ? "start" : "xdg-open";
|
|
26
|
+
exec(`${cmd} "${filePath}"`);
|
|
27
|
+
}
|
|
28
|
+
async function interactivePrompt() {
|
|
29
|
+
p.intro(pc.bgCyan(pc.black(" html2pptx convert ")));
|
|
30
|
+
const result = await p.group({
|
|
31
|
+
input: () => p.text({
|
|
32
|
+
message: "HTML file to convert",
|
|
33
|
+
placeholder: "./slides.html",
|
|
34
|
+
validate(value) {
|
|
35
|
+
if (!value)
|
|
36
|
+
return "HTML file path is required";
|
|
37
|
+
if (!value.endsWith(".html") && !value.endsWith(".htm"))
|
|
38
|
+
return "File must be .html or .htm";
|
|
39
|
+
},
|
|
40
|
+
}),
|
|
41
|
+
size: () => p.select({
|
|
42
|
+
message: "Slide size",
|
|
43
|
+
options: [
|
|
44
|
+
{ value: "16:9", label: "16:9 (1600x900)", hint: "default" },
|
|
45
|
+
{ value: "4:3", label: "4:3 (1024x768)" },
|
|
46
|
+
{ value: "custom", label: "Custom size" },
|
|
47
|
+
],
|
|
48
|
+
}),
|
|
49
|
+
customSize: ({ results }) => {
|
|
50
|
+
if (results.size !== "custom")
|
|
51
|
+
return;
|
|
52
|
+
return p.text({
|
|
53
|
+
message: "Enter custom size (WxH)",
|
|
54
|
+
placeholder: "1920x1080",
|
|
55
|
+
validate(value) {
|
|
56
|
+
if (!value?.match(/^\d+x\d+$/))
|
|
57
|
+
return 'Format: WIDTHxHEIGHT (e.g. 1920x1080)';
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
},
|
|
61
|
+
css: () => p.text({
|
|
62
|
+
message: "External CSS file (optional)",
|
|
63
|
+
placeholder: "Press Enter to skip",
|
|
64
|
+
}),
|
|
65
|
+
output: ({ results }) => {
|
|
66
|
+
const defaultName = basename(results.input).replace(/\.html?$/, "") + ".pptx";
|
|
67
|
+
return p.text({
|
|
68
|
+
message: "Output filename",
|
|
69
|
+
placeholder: defaultName,
|
|
70
|
+
initialValue: defaultName,
|
|
71
|
+
});
|
|
72
|
+
},
|
|
73
|
+
}, {
|
|
74
|
+
onCancel() {
|
|
75
|
+
p.cancel("Conversion cancelled.");
|
|
76
|
+
process.exit(0);
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
return {
|
|
80
|
+
input: result.input,
|
|
81
|
+
output: result.output,
|
|
82
|
+
size: result.customSize ?? result.size,
|
|
83
|
+
css: result.css || undefined,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
async function runExport(html, css, size, fileName, baseUrl, apiKey) {
|
|
87
|
+
const sizeParams = parseSize(size);
|
|
88
|
+
const body = {
|
|
89
|
+
html,
|
|
90
|
+
fileName,
|
|
91
|
+
responseFormat: "url",
|
|
92
|
+
...sizeParams,
|
|
93
|
+
};
|
|
94
|
+
if (css)
|
|
95
|
+
body.css = css;
|
|
96
|
+
const res = await fetch(`${baseUrl}/api/v1/export/jobs`, {
|
|
97
|
+
method: "POST",
|
|
98
|
+
headers: {
|
|
99
|
+
"Content-Type": "application/json",
|
|
100
|
+
Authorization: `Bearer ${apiKey}`,
|
|
101
|
+
},
|
|
102
|
+
body: JSON.stringify(body),
|
|
103
|
+
});
|
|
104
|
+
if (res.status === 401) {
|
|
105
|
+
throw new Error("Invalid API key. Run `html2pptx login` to update your key.");
|
|
106
|
+
}
|
|
107
|
+
if (res.status === 403) {
|
|
108
|
+
throw new Error("Access denied. Your API key may be expired or your plan may have changed. Visit https://html2pptx.app/dashboard to check.");
|
|
109
|
+
}
|
|
110
|
+
if (res.status === 413) {
|
|
111
|
+
throw new Error("HTML payload too large for your plan. Try reducing the HTML size or upgrading your plan.");
|
|
112
|
+
}
|
|
113
|
+
if (res.status === 429) {
|
|
114
|
+
throw new Error("Rate limit exceeded. Please wait a moment and try again, or upgrade your plan for higher limits.");
|
|
115
|
+
}
|
|
116
|
+
if (!res.ok) {
|
|
117
|
+
const text = await res.text();
|
|
118
|
+
throw new Error(`API error ${res.status}: ${text}`);
|
|
119
|
+
}
|
|
120
|
+
return (await res.json());
|
|
121
|
+
}
|
|
122
|
+
async function pollJob(statusUrl, apiKey, baseUrl) {
|
|
123
|
+
if (!statusUrl.startsWith(baseUrl)) {
|
|
124
|
+
throw new Error("Unexpected status URL from server");
|
|
125
|
+
}
|
|
126
|
+
const maxAttempts = 60;
|
|
127
|
+
for (let i = 0; i < maxAttempts; i++) {
|
|
128
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
129
|
+
const res = await fetch(statusUrl, {
|
|
130
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
131
|
+
});
|
|
132
|
+
if (!res.ok) {
|
|
133
|
+
const text = await res.text();
|
|
134
|
+
throw new Error(`Poll error ${res.status}: ${text}`);
|
|
135
|
+
}
|
|
136
|
+
const data = (await res.json());
|
|
137
|
+
if (data.status === "completed")
|
|
138
|
+
return data;
|
|
139
|
+
if (data.status === "failed")
|
|
140
|
+
throw new Error(data.error ?? "Export failed");
|
|
141
|
+
}
|
|
142
|
+
throw new Error("Export timed out after 2 minutes");
|
|
143
|
+
}
|
|
144
|
+
async function downloadFile(url, outputPath) {
|
|
145
|
+
const res = await fetch(url);
|
|
146
|
+
if (!res.ok)
|
|
147
|
+
throw new Error(`Download failed: ${res.status}`);
|
|
148
|
+
const buffer = Buffer.from(await res.arrayBuffer());
|
|
149
|
+
await writeFile(outputPath, buffer);
|
|
150
|
+
return buffer.byteLength;
|
|
151
|
+
}
|
|
152
|
+
function formatBytes(bytes) {
|
|
153
|
+
if (bytes < 1024)
|
|
154
|
+
return `${bytes} B`;
|
|
155
|
+
if (bytes < 1024 * 1024)
|
|
156
|
+
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
157
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
158
|
+
}
|
|
159
|
+
export async function convertCommand(input, options = {}) {
|
|
160
|
+
const config = await loadConfig();
|
|
161
|
+
const apiKey = config.apiKey;
|
|
162
|
+
const baseUrl = options.baseUrl ?? config.baseUrl ?? "https://html2pptx.app";
|
|
163
|
+
const jsonMode = options.json ?? false;
|
|
164
|
+
if (!apiKey) {
|
|
165
|
+
if (jsonMode) {
|
|
166
|
+
console.log(JSON.stringify({ success: false, error: "No API key found. Run html2pptx login first." }));
|
|
167
|
+
process.exit(1);
|
|
168
|
+
}
|
|
169
|
+
p.log.error("No API key found. Run " + pc.cyan("html2pptx login") + " first.");
|
|
170
|
+
process.exit(1);
|
|
171
|
+
}
|
|
172
|
+
let htmlPath;
|
|
173
|
+
let outputFile;
|
|
174
|
+
let size;
|
|
175
|
+
let cssPath;
|
|
176
|
+
// If args provided -> direct mode, else -> interactive mode
|
|
177
|
+
if (input) {
|
|
178
|
+
htmlPath = resolve(input);
|
|
179
|
+
outputFile =
|
|
180
|
+
options.output ?? basename(htmlPath).replace(/\.html?$/, "") + ".pptx";
|
|
181
|
+
size = options.size ?? "16:9";
|
|
182
|
+
cssPath = options.css;
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
const answers = await interactivePrompt();
|
|
186
|
+
htmlPath = resolve(answers.input);
|
|
187
|
+
outputFile = answers.output;
|
|
188
|
+
size = answers.size;
|
|
189
|
+
cssPath = answers.css;
|
|
190
|
+
}
|
|
191
|
+
const startTime = Date.now();
|
|
192
|
+
const spinner = jsonMode ? null : p.spinner();
|
|
193
|
+
spinner?.start("Reading HTML file...");
|
|
194
|
+
let html;
|
|
195
|
+
try {
|
|
196
|
+
html = await readFile(htmlPath, "utf-8");
|
|
197
|
+
}
|
|
198
|
+
catch {
|
|
199
|
+
if (jsonMode) {
|
|
200
|
+
console.log(JSON.stringify({ success: false, error: `Could not read file: ${htmlPath}` }));
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
spinner?.stop("Failed");
|
|
204
|
+
p.log.error(`Could not read file: ${pc.dim(htmlPath)}`);
|
|
205
|
+
}
|
|
206
|
+
process.exit(1);
|
|
207
|
+
}
|
|
208
|
+
let css;
|
|
209
|
+
if (cssPath) {
|
|
210
|
+
try {
|
|
211
|
+
css = await readFile(resolve(cssPath), "utf-8");
|
|
212
|
+
}
|
|
213
|
+
catch {
|
|
214
|
+
if (jsonMode) {
|
|
215
|
+
console.log(JSON.stringify({ success: false, error: `Could not read CSS file: ${cssPath}` }));
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
spinner?.stop("Failed");
|
|
219
|
+
p.log.error(`Could not read CSS file: ${pc.dim(cssPath)}`);
|
|
220
|
+
}
|
|
221
|
+
process.exit(1);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
spinner?.message("Sending to html2pptx API...");
|
|
225
|
+
let job;
|
|
226
|
+
try {
|
|
227
|
+
job = await runExport(html, css, size, outputFile, baseUrl, apiKey);
|
|
228
|
+
}
|
|
229
|
+
catch (e) {
|
|
230
|
+
const errMsg = e.message;
|
|
231
|
+
if (jsonMode) {
|
|
232
|
+
console.log(JSON.stringify({ success: false, error: errMsg }));
|
|
233
|
+
}
|
|
234
|
+
else {
|
|
235
|
+
spinner?.stop("Failed");
|
|
236
|
+
p.log.error(errMsg);
|
|
237
|
+
}
|
|
238
|
+
process.exit(1);
|
|
239
|
+
}
|
|
240
|
+
const finishConversion = async (downloadUrl) => {
|
|
241
|
+
spinner?.message("Downloading PPTX...");
|
|
242
|
+
const fileSize = await downloadFile(downloadUrl, resolve(outputFile));
|
|
243
|
+
const duration = Date.now() - startTime;
|
|
244
|
+
if (jsonMode) {
|
|
245
|
+
const result = {
|
|
246
|
+
success: true,
|
|
247
|
+
file: outputFile,
|
|
248
|
+
size: formatBytes(fileSize),
|
|
249
|
+
duration: formatDuration(duration),
|
|
250
|
+
};
|
|
251
|
+
console.log(JSON.stringify(result));
|
|
252
|
+
}
|
|
253
|
+
else {
|
|
254
|
+
spinner?.stop(pc.green("Done!"));
|
|
255
|
+
p.log.success(`Saved to ${pc.cyan(outputFile)} ${pc.dim(`(${formatBytes(fileSize)}, ${formatDuration(duration)})`)}`);
|
|
256
|
+
// Next steps guidance
|
|
257
|
+
p.log.info(pc.dim("Next: ") +
|
|
258
|
+
`Open the file or run ${pc.cyan("html2pptx status")} to check your usage.`);
|
|
259
|
+
}
|
|
260
|
+
if (options.open) {
|
|
261
|
+
openFile(resolve(outputFile));
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
// If job completed immediately
|
|
265
|
+
if (job.status === "completed" && job.downloadUrl) {
|
|
266
|
+
try {
|
|
267
|
+
await finishConversion(job.downloadUrl);
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
catch (e) {
|
|
271
|
+
const errMsg = e.message;
|
|
272
|
+
if (jsonMode) {
|
|
273
|
+
console.log(JSON.stringify({ success: false, error: errMsg }));
|
|
274
|
+
}
|
|
275
|
+
else {
|
|
276
|
+
spinner?.stop("Failed");
|
|
277
|
+
p.log.error(errMsg);
|
|
278
|
+
}
|
|
279
|
+
process.exit(1);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
// Poll for completion
|
|
283
|
+
spinner?.message("Converting... this may take a moment");
|
|
284
|
+
try {
|
|
285
|
+
const result = await pollJob(job.statusUrl, apiKey, baseUrl);
|
|
286
|
+
if (result.downloadUrl) {
|
|
287
|
+
await finishConversion(result.downloadUrl);
|
|
288
|
+
}
|
|
289
|
+
else {
|
|
290
|
+
if (jsonMode) {
|
|
291
|
+
console.log(JSON.stringify({ success: false, error: "No download URL returned" }));
|
|
292
|
+
}
|
|
293
|
+
else {
|
|
294
|
+
spinner?.stop("Failed");
|
|
295
|
+
p.log.error("No download URL returned");
|
|
296
|
+
}
|
|
297
|
+
process.exit(1);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
catch (e) {
|
|
301
|
+
const errMsg = e.message;
|
|
302
|
+
if (jsonMode) {
|
|
303
|
+
console.log(JSON.stringify({ success: false, error: errMsg }));
|
|
304
|
+
}
|
|
305
|
+
else {
|
|
306
|
+
spinner?.stop("Failed");
|
|
307
|
+
p.log.error(errMsg);
|
|
308
|
+
}
|
|
309
|
+
process.exit(1);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { type IncomingMessage, type ServerResponse } from "node:http";
|
|
2
|
+
export interface EditOptions {
|
|
3
|
+
port?: string;
|
|
4
|
+
baseUrl?: string;
|
|
5
|
+
noOpen?: boolean;
|
|
6
|
+
open?: boolean;
|
|
7
|
+
json?: boolean;
|
|
8
|
+
}
|
|
9
|
+
interface BridgeContext {
|
|
10
|
+
root: string;
|
|
11
|
+
editorOrigin: string;
|
|
12
|
+
localStateDir: string;
|
|
13
|
+
sessionToken: string;
|
|
14
|
+
}
|
|
15
|
+
declare function generateSessionToken(): string;
|
|
16
|
+
declare function parsePort(value: string | undefined): number;
|
|
17
|
+
declare function normalizeBaseUrl(raw: string): URL;
|
|
18
|
+
declare function readRegisteredEditorBaseUrl(root: string): Promise<string | null>;
|
|
19
|
+
declare function resolveEditorBaseUrl(root: string, explicitBaseUrl: string | undefined): Promise<URL>;
|
|
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>;
|
|
22
|
+
declare function listen(server: ReturnType<typeof createBridgeServer>, requestedPort: number): Promise<number>;
|
|
23
|
+
export declare function editCommand(input: string | undefined, options?: EditOptions): Promise<void>;
|
|
24
|
+
export declare const editCommandInternalsForTest: {
|
|
25
|
+
buildEditorUrl: typeof buildEditorUrl;
|
|
26
|
+
createBridgeServer: typeof createBridgeServer;
|
|
27
|
+
generateSessionToken: typeof generateSessionToken;
|
|
28
|
+
listen: typeof listen;
|
|
29
|
+
normalizeBaseUrl: typeof normalizeBaseUrl;
|
|
30
|
+
parsePort: typeof parsePort;
|
|
31
|
+
readRegisteredEditorBaseUrl: typeof readRegisteredEditorBaseUrl;
|
|
32
|
+
resolveEditorBaseUrl: typeof resolveEditorBaseUrl;
|
|
33
|
+
};
|
|
34
|
+
export {};
|