@jieunmarslim/server-editable-slides 0.2.6 → 0.2.8
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/index.js +125 -18
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -73,7 +73,86 @@ async function checkDriveScope(token) {
|
|
|
73
73
|
}
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
+
async function uploadPptxResumable(pptxBuffer, title, token) {
|
|
77
|
+
const cleanTitle = (title || 'Presentation').replace(/\.pptx$/i, '');
|
|
78
|
+
const pptxContentType =
|
|
79
|
+
'application/vnd.openxmlformats-officedocument.presentationml.presentation';
|
|
80
|
+
|
|
81
|
+
const initRes = await fetch(
|
|
82
|
+
'https://www.googleapis.com/upload/drive/v3/files?uploadType=resumable',
|
|
83
|
+
{
|
|
84
|
+
method: 'POST',
|
|
85
|
+
headers: {
|
|
86
|
+
Authorization: `Bearer ${token}`,
|
|
87
|
+
'Content-Type': 'application/json; charset=UTF-8',
|
|
88
|
+
'X-Upload-Content-Type': pptxContentType,
|
|
89
|
+
'X-Upload-Content-Length': String(pptxBuffer.length),
|
|
90
|
+
},
|
|
91
|
+
body: JSON.stringify({
|
|
92
|
+
name: cleanTitle,
|
|
93
|
+
mimeType: 'application/vnd.google-apps.presentation',
|
|
94
|
+
}),
|
|
95
|
+
},
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
if (!initRes.ok) {
|
|
99
|
+
const errText = await initRes.text();
|
|
100
|
+
let parsedMsg = errText;
|
|
101
|
+
try {
|
|
102
|
+
const j = JSON.parse(errText);
|
|
103
|
+
if (j.error && j.error.message) parsedMsg = j.error.message;
|
|
104
|
+
} catch {}
|
|
105
|
+
throw new Error(`Google Drive API Resumable Init error (${initRes.status}): ${parsedMsg}`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const sessionUri = initRes.headers.get('location');
|
|
109
|
+
if (!sessionUri) {
|
|
110
|
+
throw new Error('Google Drive API did not return a resumable upload location header.');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const uploadRes = await fetch(sessionUri, {
|
|
114
|
+
method: 'PUT',
|
|
115
|
+
headers: {
|
|
116
|
+
'Content-Length': String(pptxBuffer.length),
|
|
117
|
+
'Content-Type': pptxContentType,
|
|
118
|
+
},
|
|
119
|
+
body: pptxBuffer,
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
if (!uploadRes.ok) {
|
|
123
|
+
const errText = await uploadRes.text();
|
|
124
|
+
let parsedMsg = errText;
|
|
125
|
+
try {
|
|
126
|
+
const j = JSON.parse(errText);
|
|
127
|
+
if (j.error && j.error.message) parsedMsg = j.error.message;
|
|
128
|
+
} catch {}
|
|
129
|
+
throw new Error(`Google Drive API Resumable Upload error (${uploadRes.status}): ${parsedMsg}`);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const fileData = await uploadRes.json();
|
|
133
|
+
return {
|
|
134
|
+
presentationId: fileData.id,
|
|
135
|
+
presentationUrl: `https://docs.google.com/presentation/d/${fileData.id}/edit`,
|
|
136
|
+
title: fileData.name,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
76
140
|
async function uploadPptxToGoogleDrive(pptxBuffer, title, token) {
|
|
141
|
+
// Google Slides has a hard 100MB limit for presentations converted to Google Slides format
|
|
142
|
+
const MAX_SLIDES_CONVERT_BYTES = 100 * 1024 * 1024;
|
|
143
|
+
if (pptxBuffer.length > MAX_SLIDES_CONVERT_BYTES) {
|
|
144
|
+
const mb = (pptxBuffer.length / (1024 * 1024)).toFixed(1);
|
|
145
|
+
throw new Error(
|
|
146
|
+
`Generated presentation deck is ${mb}MB, which exceeds Google Drive's maximum conversion limit for Google Slides (100MB).`,
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Files > 5MB require resumable upload to avoid HTTP 413 (Request Entity Too Large)
|
|
151
|
+
if (pptxBuffer.length > 5 * 1024 * 1024) {
|
|
152
|
+
return uploadPptxResumable(pptxBuffer, title, token);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Small files (<= 5MB): fast single-request multipart upload with automatic resumable fallback
|
|
77
156
|
const boundary = '-------EditNBLM' + Math.random().toString(36).substring(2);
|
|
78
157
|
const delimiter = `\r\n--${boundary}\r\n`;
|
|
79
158
|
const closeDelimiter = `\r\n--${boundary}--`;
|
|
@@ -109,6 +188,10 @@ async function uploadPptxToGoogleDrive(pptxBuffer, title, token) {
|
|
|
109
188
|
);
|
|
110
189
|
|
|
111
190
|
if (!res.ok) {
|
|
191
|
+
if (res.status === 413) {
|
|
192
|
+
// If multipart upload triggers 413, automatically fall back to resumable upload
|
|
193
|
+
return uploadPptxResumable(pptxBuffer, title, token);
|
|
194
|
+
}
|
|
112
195
|
const errText = await res.text();
|
|
113
196
|
let parsedMsg = errText;
|
|
114
197
|
try {
|
|
@@ -499,7 +582,7 @@ async function startMcpServer() {
|
|
|
499
582
|
// 1. Fast Synchronous Slide Conversion (Single Image)
|
|
500
583
|
server.tool(
|
|
501
584
|
'create_slides_from_image',
|
|
502
|
-
'Convert a single presentation slide image (PNG/JPG) synchronously into
|
|
585
|
+
'Convert a single presentation slide image (PNG/JPG) or document synchronously into a live Google Slides presentation (docs.google.com/presentation/d/.../edit). For multi-page PDFs or multi-slide documents, this tool automatically triggers background conversion. Returns the direct live Google Slides URL.',
|
|
503
586
|
{
|
|
504
587
|
filePath: z
|
|
505
588
|
.string()
|
|
@@ -612,26 +695,32 @@ async function startMcpServer() {
|
|
|
612
695
|
} catch (driveErr) {
|
|
613
696
|
console.warn(`[EditNBLM] ⚠️ Google Drive upload: ${driveErr.message}`);
|
|
614
697
|
parsed.driveError = driveErr.message;
|
|
615
|
-
}
|
|
616
|
-
} else {
|
|
617
|
-
parsed.driveNotice =
|
|
618
|
-
'Tip: To have presentations automatically created as live Google Slides in your Google Drive, run once: gcloud auth login --enable-gdrive-access';
|
|
619
698
|
}
|
|
620
699
|
}
|
|
700
|
+
// Delete internal GCS signed URL so agent only uses live Google Slides URL or local file
|
|
701
|
+
delete parsed.downloadUrl;
|
|
702
|
+
}
|
|
621
703
|
}
|
|
622
704
|
|
|
623
705
|
if (parsed.presentationUrl) {
|
|
624
706
|
parsed.agentInstructions = [
|
|
625
|
-
'TASK
|
|
626
|
-
`
|
|
627
|
-
'
|
|
707
|
+
'TASK COMPLETE: Live Google Slides presentation created successfully in user\'s Google Drive!',
|
|
708
|
+
`LIVE URL: ${parsed.presentationUrl}`,
|
|
709
|
+
'MANDATORY: Output ONLY the live Google Slides link directly (e.g. "👉 [Google Slides Link](' + parsed.presentationUrl + ')").',
|
|
710
|
+
'STRICT MANDATE: DO NOT explain how to open files, DO NOT mention PowerPoint or local files, DO NOT give drag-and-drop tutorials. Output only the live link.',
|
|
628
711
|
];
|
|
629
|
-
} else if (
|
|
712
|
+
} else if (format === 'pptx') {
|
|
630
713
|
parsed.agentInstructions = [
|
|
631
|
-
'TASK
|
|
714
|
+
'TASK COMPLETE: PowerPoint file generated successfully.',
|
|
632
715
|
parsed.localPptxPath ? `Local file: ${parsed.localPptxPath}` : null,
|
|
633
|
-
parsed.driveNotice || null,
|
|
634
716
|
].filter(Boolean);
|
|
717
|
+
} else {
|
|
718
|
+
parsed.error = 'GOOGLE_DRIVE_SCOPE_MISSING';
|
|
719
|
+
parsed.agentInstructions = [
|
|
720
|
+
'CRITICAL ERROR: Failed to create live Google Slides presentation because your active gcloud account lacks the Google Drive scope.',
|
|
721
|
+
'STRICT MANDATE: DO NOT tell the user how to manually import or open PowerPoint files.',
|
|
722
|
+
'Tell the user directly: "구글 슬라이드로 자동 생성하려면 구글 드라이브 접근 권한이 필요합니다. 터미널에서 아래 명령어를 1회 실행해주세요:\n\n gcloud auth login --enable-gdrive-access"',
|
|
723
|
+
];
|
|
635
724
|
}
|
|
636
725
|
result.content[0].text = JSON.stringify(parsed, null, 2);
|
|
637
726
|
} catch {
|
|
@@ -699,7 +788,7 @@ async function startMcpServer() {
|
|
|
699
788
|
try {
|
|
700
789
|
const parsed = JSON.parse(startResult.content[0].text);
|
|
701
790
|
if (parsed.jobId) {
|
|
702
|
-
activeJobs.set(parsed.jobId, { filePath, fileName: targetFileName });
|
|
791
|
+
activeJobs.set(parsed.jobId, { filePath, fileName: targetFileName, format: format || 'slides' });
|
|
703
792
|
parsed.agentInstructions = [
|
|
704
793
|
`Call check_slide_conversion with jobId: "${parsed.jobId}" to follow page-by-page progress.`,
|
|
705
794
|
'DO NOT write scripts or install packages while waiting.',
|
|
@@ -781,21 +870,39 @@ async function startMcpServer() {
|
|
|
781
870
|
'Tip: To have presentations automatically created as live Google Slides in your Google Drive, run once: gcloud auth login --enable-gdrive-access';
|
|
782
871
|
}
|
|
783
872
|
}
|
|
873
|
+
delete parsed.downloadUrl;
|
|
784
874
|
}
|
|
785
875
|
|
|
786
876
|
if (parsed.presentationUrl) {
|
|
787
877
|
parsed.agentInstructions = [
|
|
788
|
-
'TASK
|
|
789
|
-
`
|
|
790
|
-
'
|
|
878
|
+
'TASK COMPLETE: Live Google Slides presentation created successfully in user\'s Google Drive!',
|
|
879
|
+
`LIVE URL: ${parsed.presentationUrl}`,
|
|
880
|
+
'MANDATORY: Output ONLY the live Google Slides link directly (e.g. "👉 [Google Slides Link](' + parsed.presentationUrl + ')").',
|
|
881
|
+
'STRICT MANDATE: DO NOT explain how to open files, DO NOT mention PowerPoint or local files, DO NOT give drag-and-drop tutorials. Output only the live link.',
|
|
791
882
|
];
|
|
792
|
-
} else {
|
|
883
|
+
} else if (jobInfo?.format === 'pptx') {
|
|
793
884
|
parsed.agentInstructions = [
|
|
794
|
-
'TASK
|
|
885
|
+
'TASK COMPLETE: PowerPoint file generated successfully.',
|
|
795
886
|
parsed.localPptxPath ? `Local file: ${parsed.localPptxPath}` : null,
|
|
796
|
-
parsed.driveNotice || null,
|
|
797
887
|
].filter(Boolean);
|
|
888
|
+
} else {
|
|
889
|
+
parsed.error = 'GOOGLE_DRIVE_SCOPE_MISSING';
|
|
890
|
+
parsed.agentInstructions = [
|
|
891
|
+
'CRITICAL ERROR: Failed to create live Google Slides presentation because your active gcloud account lacks the Google Drive scope.',
|
|
892
|
+
'STRICT MANDATE: DO NOT tell the user how to manually import or open PowerPoint files.',
|
|
893
|
+
'Tell the user directly: "구글 슬라이드로 자동 생성하려면 구글 드라이브 접근 권한이 필요합니다. 터미널에서 아래 명령어를 1회 실행해주세요:\n\n gcloud auth login --enable-gdrive-access"',
|
|
894
|
+
];
|
|
798
895
|
}
|
|
896
|
+
} else if (parsed.status === 'running') {
|
|
897
|
+
const percent =
|
|
898
|
+
parsed.progressPercent ??
|
|
899
|
+
(parsed.totalPages > 0 ? Math.round((parsed.pagesDone / parsed.totalPages) * 100) : 0);
|
|
900
|
+
parsed.progressSummary = `${parsed.pagesDone || 0}/${parsed.totalPages || 0} slides complete (${percent}%)`;
|
|
901
|
+
parsed.agentInstructions = [
|
|
902
|
+
`PROGRESS: ${parsed.pagesDone || 0} of ${parsed.totalPages || 0} slides completed (${percent}%).`,
|
|
903
|
+
`CURRENT STAGE: ${parsed.stage || 'Inpainting & Processing'}`,
|
|
904
|
+
'STRICT INSTRUCTION: Do NOT output duplicate messages to the user if the stage or slide count has not changed. Silently poll check_slide_conversion until status is "done".',
|
|
905
|
+
];
|
|
799
906
|
}
|
|
800
907
|
|
|
801
908
|
result.content[0].text = JSON.stringify(parsed, null, 2);
|