@jieunmarslim/server-editable-slides 0.2.13 → 0.2.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/index.js +83 -104
  2. package/package.json +3 -2
package/index.js CHANGED
@@ -58,6 +58,22 @@ function getAccount() {
58
58
  return null;
59
59
  }
60
60
 
61
+ function getKnownAccounts() {
62
+ try {
63
+ const lines = execSync('gcloud auth list --format="value(account)"', {
64
+ stdio: ['ignore', 'pipe', 'ignore'],
65
+ })
66
+ .toString()
67
+ .trim()
68
+ .split('\n')
69
+ .map((a) => a.trim())
70
+ .filter(Boolean);
71
+ return Array.from(new Set(lines));
72
+ } catch {
73
+ return [];
74
+ }
75
+ }
76
+
61
77
  async function checkDriveScope(token) {
62
78
  if (!token) return false;
63
79
  try {
@@ -202,6 +218,25 @@ async function uploadPptxToGoogleDrive(pptxBuffer, title, token) {
202
218
  }
203
219
 
204
220
  const fileData = await res.json();
221
+
222
+
223
+ // Also share directly with all other accounts registered on developer's machine (e.g. corp, personal)
224
+ const currentAccount = getAccount();
225
+ const knownAccounts = getKnownAccounts();
226
+ for (const acct of knownAccounts) {
227
+ if (acct === currentAccount) continue;
228
+ try {
229
+ await fetch(`https://www.googleapis.com/drive/v3/files/${fileData.id}/permissions`, {
230
+ method: 'POST',
231
+ headers: {
232
+ Authorization: `Bearer ${token}`,
233
+ 'Content-Type': 'application/json',
234
+ },
235
+ body: JSON.stringify({ role: 'writer', type: 'user', emailAddress: acct }),
236
+ });
237
+ } catch {}
238
+ }
239
+
205
240
  return {
206
241
  presentationId: fileData.id,
207
242
  presentationUrl: `https://docs.google.com/presentation/d/${fileData.id}/edit`,
@@ -639,115 +674,40 @@ async function startMcpServer() {
639
674
  throw new Error('Either filePath or fileUrl must be provided.');
640
675
  }
641
676
 
642
- // If document is a multi-page PDF, route to async pipeline to prevent 120s MCP timeout
643
- if (isPdf) {
644
- console.error('[EditNBLM] Multi-page PDF detected. Starting background pipeline to prevent timeout...');
645
- const startResult = await callRemoteMcp(
646
- 'tools/call',
647
- {
648
- name: 'start_slide_conversion',
649
- arguments: {
650
- fileUrl: targetUrl,
651
- fileName: originalName,
652
- format: format || 'slides',
653
- },
654
- },
655
- apiKey,
656
- );
657
-
658
- const parsed = JSON.parse(startResult.content[0].text);
659
- if (parsed.jobId) {
660
- activeJobs.set(parsed.jobId, {
661
- filePath,
662
- fileName: originalName,
663
- format: format || 'slides',
664
- });
665
- parsed.note =
666
- 'Multi-page PDF detected. High-performance conversion started in background to prevent client timeouts. ' +
667
- 'STRICT MANDATE FOR AGENT: Immediately call `check_slide_conversion` with this jobId to track progress until done. ' +
668
- 'DO NOT write custom scripts or install packages.';
669
- parsed.agentInstructions = [
670
- `Call check_slide_conversion with jobId: "${parsed.jobId}" to follow page-by-page progress.`,
671
- 'When check_slide_conversion returns status "done", the finished presentation will be automatically saved locally and/or uploaded to Google Drive.',
672
- ];
673
- startResult.content[0].text = JSON.stringify(parsed, null, 2);
674
- }
675
- return startResult;
676
- }
677
-
678
- // Synchronous single-image conversion
679
- console.error(`[EditNBLM] Converting image to ${format || 'slides'}...`);
680
- const result = await callRemoteMcp(
677
+ // Always route to background pipeline to provide real-time % progress bars and prevent timeouts
678
+ console.error(`[EditNBLM] Starting background conversion pipeline for ${originalName}...`);
679
+ const startResult = await callRemoteMcp(
681
680
  'tools/call',
682
681
  {
683
- name: 'create_slides_from_image',
682
+ name: 'start_slide_conversion',
684
683
  arguments: {
685
684
  fileUrl: targetUrl,
685
+ fileName: originalName,
686
686
  format: format || 'slides',
687
687
  },
688
688
  },
689
689
  apiKey,
690
690
  );
691
691
 
692
- try {
693
- const parsed = JSON.parse(result.content[0].text);
694
- if (parsed.downloadUrl && filePath) {
695
- const localPptxPath = filePath.replace(/\.[^/.]+$/, '') + '.pptx';
696
- const downloaded = await downloadPptxHelper(parsed.downloadUrl, localPptxPath);
697
- parsed.localPptxPath = localPptxPath;
698
-
699
- // If format is 'slides' (default), automatically upload to Google Drive if scope is granted
700
- if (format !== 'pptx' && downloaded?.buffer) {
701
- const token = getAuthToken();
702
- const hasDrive = await checkDriveScope(token);
703
- if (hasDrive) {
704
- try {
705
- console.error('[EditNBLM] 🚀 Creating live Google Slides in your personal Google Drive...');
706
- const driveResult = await uploadPptxToGoogleDrive(
707
- downloaded.buffer,
708
- path.basename(localPptxPath),
709
- token,
710
- );
711
- parsed.presentationUrl = driveResult.presentationUrl;
712
- parsed.presentationId = driveResult.presentationId;
713
- parsed.googleSlidesCreated = true;
714
- console.error(`[EditNBLM] ✅ Live Google Slides: ${driveResult.presentationUrl}`);
715
- } catch (driveErr) {
716
- console.warn(`[EditNBLM] ⚠️ Google Drive upload: ${driveErr.message}`);
717
- parsed.driveError = driveErr.message;
718
- }
719
- }
720
- // Delete internal GCS signed URL so agent only uses live Google Slides URL or local file
721
- delete parsed.downloadUrl;
722
- }
723
- }
724
-
725
- if (parsed.presentationUrl) {
726
- parsed.agentInstructions = [
727
- 'TASK COMPLETE: Live Google Slides presentation created successfully in user\'s Google Drive!',
728
- `LIVE URL: ${parsed.presentationUrl}`,
729
- 'MANDATORY: Output ONLY the live Google Slides link directly (e.g. "👉 [Google Slides Link](' + parsed.presentationUrl + ')").',
730
- '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.',
731
- ];
732
- } else if (format === 'pptx') {
733
- parsed.agentInstructions = [
734
- 'TASK COMPLETE: PowerPoint file generated successfully.',
735
- parsed.localPptxPath ? `Local file: ${parsed.localPptxPath}` : null,
736
- ].filter(Boolean);
737
- } else {
738
- parsed.error = 'GOOGLE_DRIVE_SCOPE_MISSING';
739
- parsed.agentInstructions = [
740
- 'CRITICAL ERROR: Failed to create live Google Slides presentation because your active gcloud account lacks the Google Drive scope.',
741
- 'STRICT MANDATE: DO NOT tell the user how to manually import or open PowerPoint files.',
742
- 'Tell the user directly: "To automatically create editable Google Slides presentations in your personal Google Drive, Google Drive access is required. Please run the following command once in your terminal:\n\n gcloud auth login --enable-gdrive-access"',
743
- ];
744
- }
745
- result.content[0].text = JSON.stringify(parsed, null, 2);
746
- } catch {
747
- // Retain original result
692
+ const parsed = JSON.parse(startResult.content[0].text);
693
+ if (parsed.jobId) {
694
+ activeJobs.set(parsed.jobId, {
695
+ filePath,
696
+ fileName: originalName,
697
+ format: format || 'slides',
698
+ });
699
+ parsed.note =
700
+ 'Conversion started in background with real-time stage & percentage tracking. ' +
701
+ 'STRICT MANDATE FOR AGENT: Immediately call `check_slide_conversion` with this jobId to track progress until done. ' +
702
+ 'DO NOT write custom scripts, DO NOT search files, DO NOT run bash commands.';
703
+ parsed.agentInstructions = [
704
+ `Call check_slide_conversion with jobId: "${parsed.jobId}" to follow stage-by-stage progress.`,
705
+ 'On EVERY poll, emit the current progress message to the user before checking again.',
706
+ 'When check_slide_conversion returns status "done", the finished presentation will be automatically saved locally and/or uploaded to Google Drive.',
707
+ ];
708
+ startResult.content[0].text = JSON.stringify(parsed, null, 2);
748
709
  }
749
-
750
- return result;
710
+ return startResult;
751
711
  },
752
712
  );
753
713
 
@@ -824,7 +784,7 @@ async function startMcpServer() {
824
784
  // 3. Check Slide Conversion Progress & Auto-Download
825
785
  server.tool(
826
786
  'check_slide_conversion',
827
- 'Report the progress of a conversion started by start_slide_conversion. Call this repeatedly until status is "done". When complete, automatically downloads the editable PowerPoint (.pptx) file to local disk.',
787
+ 'Report conversion progress. MANDATORY: On EVERY call, inform the user of current progress (e.g. "🔄 Conversion progress: X/Y slide(s) (Z%) — Stage"). Call repeatedly until status is "done". When complete, automatically downloads the editable PowerPoint (.pptx) file to local disk.',
828
788
  {
829
789
  jobId: z
830
790
  .string()
@@ -839,18 +799,28 @@ async function startMcpServer() {
839
799
  .describe('Optional API key or access token for authentication'),
840
800
  },
841
801
  async ({ jobId, sinceUpdatedAt, apiKey }) => {
802
+ const startTime = Date.now();
803
+ const jobInfo = activeJobs.get(jobId) || {};
804
+ const effectiveSinceUpdatedAt =
805
+ typeof sinceUpdatedAt === 'number' && sinceUpdatedAt > 0
806
+ ? sinceUpdatedAt
807
+ : (jobInfo.lastUpdatedAt || 0);
808
+
842
809
  const result = await callRemoteMcp(
843
810
  'tools/call',
844
811
  {
845
812
  name: 'check_slide_conversion',
846
- arguments: { jobId, sinceUpdatedAt },
813
+ arguments: { jobId, sinceUpdatedAt: effectiveSinceUpdatedAt },
847
814
  },
848
815
  apiKey,
849
816
  );
850
817
 
851
818
  try {
852
819
  const parsed = JSON.parse(result.content[0].text);
853
- const jobInfo = activeJobs.get(jobId);
820
+ if (parsed.updatedAt) {
821
+ jobInfo.lastUpdatedAt = parsed.updatedAt;
822
+ activeJobs.set(jobId, jobInfo);
823
+ }
854
824
 
855
825
  if (parsed.status === 'done') {
856
826
  parsed.progressPercent = 100;
@@ -925,17 +895,26 @@ async function startMcpServer() {
925
895
  : 0;
926
896
  parsed.progressPercent = percent;
927
897
  parsed.progressSummary = `${parsed.pagesDone || 0}/${parsed.totalPages || 0} slides complete (${percent}%)`;
898
+ parsed.message = `🔄 Conversion progress: ${parsed.pagesDone || 0}/${parsed.totalPages || 0} slide(s) (${percent}%) — ${parsed.stage || 'Processing'}`;
928
899
 
929
900
  console.error(
930
901
  `[EditNBLM] ⏳ Progress: ${parsed.pagesDone || 0}/${parsed.totalPages || 0} slides (${percent}%) — ${parsed.stage || 'Processing'}`,
931
902
  );
932
903
 
933
904
  parsed.agentInstructions = [
934
- `PROGRESS: ${parsed.pagesDone || 0} of ${parsed.totalPages || 0} slides complete (${percent}%).`,
935
- `STAGE: ${parsed.stage || 'Inpainting & Processing'}`,
936
- `MANDATORY: Inform user of progress: "🔄 Conversion progress: ${parsed.pagesDone || 0}/${parsed.totalPages || 0} slide(s) (${percent}%) — ${parsed.stage || 'Processing'}"`,
905
+ `MANDATORY: Write the following line to the user right now before calling the next tool:`,
906
+ `"🔄 Conversion progress: ${parsed.pagesDone || 0}/${parsed.totalPages || 0} slide(s) (${percent}%) — ${parsed.stage || 'Processing'}"`,
907
+ `Do NOT poll silently. Always output the progress line above.`,
937
908
  `Continue polling check_slide_conversion with sinceUpdatedAt: ${parsed.updatedAt || 0} until status is "done".`,
938
909
  ];
910
+
911
+ // Pacing delay: prevent LLM agents from rapid-firing 70 calls in a tight loop.
912
+ // Ensure at least 3.5 seconds elapse per turn, matching Cloud Run slide generation rate.
913
+ const elapsed = Date.now() - startTime;
914
+ const MIN_POLL_INTERVAL_MS = 3500;
915
+ if (elapsed < MIN_POLL_INTERVAL_MS) {
916
+ await new Promise((resolve) => setTimeout(resolve, MIN_POLL_INTERVAL_MS - elapsed));
917
+ }
939
918
  }
940
919
 
941
920
  result.content[0].text = JSON.stringify(parsed, null, 2);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jieunmarslim/server-editable-slides",
3
- "version": "0.2.13",
3
+ "version": "0.2.16",
4
4
  "description": "Model Context Protocol (MCP) client for Editable Slides Cloud",
5
5
  "type": "module",
6
6
  "bin": {
@@ -10,7 +10,8 @@
10
10
  "index.js"
11
11
  ],
12
12
  "publishConfig": {
13
- "access": "public"
13
+ "access": "public",
14
+ "registry": "https://registry.npmjs.org"
14
15
  },
15
16
  "dependencies": {
16
17
  "@modelcontextprotocol/sdk": "^1.26.0",