@upload-post/mcp 0.3.0

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 (112) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +225 -0
  3. package/dist/client.d.ts +32 -0
  4. package/dist/client.d.ts.map +1 -0
  5. package/dist/client.js +61 -0
  6. package/dist/client.js.map +1 -0
  7. package/dist/index.d.ts +3 -0
  8. package/dist/index.d.ts.map +1 -0
  9. package/dist/index.js +66 -0
  10. package/dist/index.js.map +1 -0
  11. package/dist/oauth/auth_resolver.d.ts +33 -0
  12. package/dist/oauth/auth_resolver.d.ts.map +1 -0
  13. package/dist/oauth/auth_resolver.js +30 -0
  14. package/dist/oauth/auth_resolver.js.map +1 -0
  15. package/dist/oauth/authorize.d.ts +18 -0
  16. package/dist/oauth/authorize.d.ts.map +1 -0
  17. package/dist/oauth/authorize.js +26 -0
  18. package/dist/oauth/authorize.js.map +1 -0
  19. package/dist/oauth/config.d.ts +19 -0
  20. package/dist/oauth/config.d.ts.map +1 -0
  21. package/dist/oauth/config.js +28 -0
  22. package/dist/oauth/config.js.map +1 -0
  23. package/dist/oauth/http_utils.d.ts +8 -0
  24. package/dist/oauth/http_utils.d.ts.map +1 -0
  25. package/dist/oauth/http_utils.js +48 -0
  26. package/dist/oauth/http_utils.js.map +1 -0
  27. package/dist/oauth/introspect_cache.d.ts +19 -0
  28. package/dist/oauth/introspect_cache.d.ts.map +1 -0
  29. package/dist/oauth/introspect_cache.js +40 -0
  30. package/dist/oauth/introspect_cache.js.map +1 -0
  31. package/dist/oauth/metadata.d.ts +35 -0
  32. package/dist/oauth/metadata.d.ts.map +1 -0
  33. package/dist/oauth/metadata.js +43 -0
  34. package/dist/oauth/metadata.js.map +1 -0
  35. package/dist/oauth/registration.d.ts +14 -0
  36. package/dist/oauth/registration.d.ts.map +1 -0
  37. package/dist/oauth/registration.js +40 -0
  38. package/dist/oauth/registration.js.map +1 -0
  39. package/dist/oauth/tokens.d.ts +15 -0
  40. package/dist/oauth/tokens.d.ts.map +1 -0
  41. package/dist/oauth/tokens.js +43 -0
  42. package/dist/oauth/tokens.js.map +1 -0
  43. package/dist/oauth/upstream_client.d.ts +32 -0
  44. package/dist/oauth/upstream_client.d.ts.map +1 -0
  45. package/dist/oauth/upstream_client.js +62 -0
  46. package/dist/oauth/upstream_client.js.map +1 -0
  47. package/dist/schemas.d.ts +38 -0
  48. package/dist/schemas.d.ts.map +1 -0
  49. package/dist/schemas.js +121 -0
  50. package/dist/schemas.js.map +1 -0
  51. package/dist/server.d.ts +4 -0
  52. package/dist/server.d.ts.map +1 -0
  53. package/dist/server.js +36 -0
  54. package/dist/server.js.map +1 -0
  55. package/dist/tools/analytics.d.ts +4 -0
  56. package/dist/tools/analytics.d.ts.map +1 -0
  57. package/dist/tools/analytics.js +74 -0
  58. package/dist/tools/analytics.js.map +1 -0
  59. package/dist/tools/comments.d.ts +4 -0
  60. package/dist/tools/comments.d.ts.map +1 -0
  61. package/dist/tools/comments.js +108 -0
  62. package/dist/tools/comments.js.map +1 -0
  63. package/dist/tools/dms.d.ts +4 -0
  64. package/dist/tools/dms.d.ts.map +1 -0
  65. package/dist/tools/dms.js +100 -0
  66. package/dist/tools/dms.js.map +1 -0
  67. package/dist/tools/ffmpeg.d.ts +4 -0
  68. package/dist/tools/ffmpeg.d.ts.map +1 -0
  69. package/dist/tools/ffmpeg.js +96 -0
  70. package/dist/tools/ffmpeg.js.map +1 -0
  71. package/dist/tools/media_uploads.d.ts +4 -0
  72. package/dist/tools/media_uploads.d.ts.map +1 -0
  73. package/dist/tools/media_uploads.js +106 -0
  74. package/dist/tools/media_uploads.js.map +1 -0
  75. package/dist/tools/pages.d.ts +4 -0
  76. package/dist/tools/pages.d.ts.map +1 -0
  77. package/dist/tools/pages.js +101 -0
  78. package/dist/tools/pages.js.map +1 -0
  79. package/dist/tools/queue.d.ts +4 -0
  80. package/dist/tools/queue.d.ts.map +1 -0
  81. package/dist/tools/queue.js +79 -0
  82. package/dist/tools/queue.js.map +1 -0
  83. package/dist/tools/schedule.d.ts +4 -0
  84. package/dist/tools/schedule.d.ts.map +1 -0
  85. package/dist/tools/schedule.js +48 -0
  86. package/dist/tools/schedule.js.map +1 -0
  87. package/dist/tools/status.d.ts +4 -0
  88. package/dist/tools/status.d.ts.map +1 -0
  89. package/dist/tools/status.js +67 -0
  90. package/dist/tools/status.js.map +1 -0
  91. package/dist/tools/upload.d.ts +10 -0
  92. package/dist/tools/upload.d.ts.map +1 -0
  93. package/dist/tools/upload.js +397 -0
  94. package/dist/tools/upload.js.map +1 -0
  95. package/dist/tools/upload_studio.d.ts +3 -0
  96. package/dist/tools/upload_studio.d.ts.map +1 -0
  97. package/dist/tools/upload_studio.js +808 -0
  98. package/dist/tools/upload_studio.js.map +1 -0
  99. package/dist/tools/users.d.ts +4 -0
  100. package/dist/tools/users.d.ts.map +1 -0
  101. package/dist/tools/users.js +95 -0
  102. package/dist/tools/users.js.map +1 -0
  103. package/dist/transport/http.d.ts +33 -0
  104. package/dist/transport/http.d.ts.map +1 -0
  105. package/dist/transport/http.js +263 -0
  106. package/dist/transport/http.js.map +1 -0
  107. package/dist/transport/stdio.d.ts +3 -0
  108. package/dist/transport/stdio.d.ts.map +1 -0
  109. package/dist/transport/stdio.js +7 -0
  110. package/dist/transport/stdio.js.map +1 -0
  111. package/examples/mcp-config.json +11 -0
  112. package/package.json +68 -0
@@ -0,0 +1,808 @@
1
+ import { z } from "zod";
2
+ import { VideoPlatform } from "../schemas.js";
3
+ const UPLOAD_STUDIO_URI = "ui://upload-post/video-upload-studio.html";
4
+ const WIDGET_DOMAIN = (process.env.UPLOAD_POST_WIDGET_DOMAIN ??
5
+ process.env.OAUTH_ISSUER ??
6
+ "https://mcp.upload-post.com").replace(/\/$/, "");
7
+ const DEFAULT_R2_CONNECT_DOMAINS = [
8
+ "https://0de16d5f5e344fe4757ecd62640a9ea3.r2.cloudflarestorage.com",
9
+ "https://upload-post-schedulers-eu3.0de16d5f5e344fe4757ecd62640a9ea3.r2.cloudflarestorage.com",
10
+ "https://346a933178393d200f7a17065933e0bd.r2.cloudflarestorage.com",
11
+ "https://upload-post-schedulers-eu3.346a933178393d200f7a17065933e0bd.r2.cloudflarestorage.com",
12
+ ];
13
+ const R2_CONNECT_DOMAINS = (process.env.UPLOAD_POST_R2_CONNECT_DOMAINS ??
14
+ process.env.UPLOAD_POST_R2_CONNECT_DOMAIN ??
15
+ DEFAULT_R2_CONNECT_DOMAINS.join(","))
16
+ .split(",")
17
+ .map((domain) => domain.trim().replace(/\/$/, ""))
18
+ .filter(Boolean);
19
+ const InstagramVideoMediaType = z.enum(["REELS", "STORIES"]);
20
+ const resourceMeta = {
21
+ ui: {
22
+ prefersBorder: true,
23
+ domain: WIDGET_DOMAIN,
24
+ csp: {
25
+ connectDomains: R2_CONNECT_DOMAINS,
26
+ resourceDomains: [],
27
+ },
28
+ },
29
+ "openai/widgetDescription": "A compact Upload-Post video uploader that stages a local video in Upload-Post/R2, then publishes through upload_video.",
30
+ "openai/widgetPrefersBorder": true,
31
+ "openai/widgetDomain": WIDGET_DOMAIN,
32
+ "openai/widgetCSP": {
33
+ connect_domains: R2_CONNECT_DOMAINS,
34
+ resource_domains: [],
35
+ },
36
+ };
37
+ export function registerUploadStudio(server) {
38
+ server.registerResource("upload-post-video-upload-studio", UPLOAD_STUDIO_URI, {
39
+ title: "Upload-Post Video Studio",
40
+ description: "ChatGPT UI for uploading and publishing a video with Upload-Post.",
41
+ mimeType: "text/html+skybridge",
42
+ _meta: resourceMeta,
43
+ }, async (uri) => ({
44
+ contents: [
45
+ {
46
+ uri: uri.href,
47
+ mimeType: "text/html+skybridge",
48
+ text: uploadStudioHtml,
49
+ _meta: resourceMeta,
50
+ },
51
+ ],
52
+ }));
53
+ server.registerTool("open_upload_studio", {
54
+ title: "Open upload studio",
55
+ description: "Open the ChatGPT UI for local/attached video uploads. Use this FIRST when the user attaches a video in ChatGPT or claude.ai and does not provide a public HTTPS URL. Do not try upload_video with /mnt/data, sandbox, or mounted local paths first: hosted MCP servers cannot read those files. The Studio lets the user select the file in the browser, stages it in short-lived Upload-Post/R2 storage, and publishes it through Upload-Post.",
56
+ inputSchema: {
57
+ user: z.string().optional().describe("Optional Upload-Post profile name to prefill."),
58
+ platforms: z.array(VideoPlatform).optional().describe("Optional platforms to preselect."),
59
+ title: z.string().optional().describe("Optional caption/title to prefill."),
60
+ description: z.string().optional().describe("Optional description to prefill."),
61
+ firstComment: z.string().optional().describe("Optional first comment to prefill."),
62
+ instagramMediaType: InstagramVideoMediaType.optional().describe("Instagram video format to preselect."),
63
+ },
64
+ outputSchema: {
65
+ user: z.string(),
66
+ platforms: z.array(VideoPlatform),
67
+ title: z.string(),
68
+ description: z.string(),
69
+ firstComment: z.string(),
70
+ instagramMediaType: InstagramVideoMediaType,
71
+ },
72
+ annotations: {
73
+ readOnlyHint: true,
74
+ openWorldHint: false,
75
+ destructiveHint: false,
76
+ },
77
+ _meta: {
78
+ ui: {
79
+ resourceUri: UPLOAD_STUDIO_URI,
80
+ visibility: ["model", "app"],
81
+ },
82
+ "openai/outputTemplate": UPLOAD_STUDIO_URI,
83
+ "openai/widgetAccessible": true,
84
+ "openai/toolInvocation/invoking": "Opening upload studio…",
85
+ "openai/toolInvocation/invoked": "Upload studio opened",
86
+ },
87
+ }, async (args) => {
88
+ const input = args;
89
+ const title = input.title ?? input.description ?? "";
90
+ return {
91
+ structuredContent: {
92
+ user: input.user ?? "",
93
+ platforms: input.platforms?.length ? input.platforms : ["instagram"],
94
+ title,
95
+ description: input.description ?? title,
96
+ firstComment: input.firstComment ?? "",
97
+ instagramMediaType: input.instagramMediaType ?? "REELS",
98
+ },
99
+ content: [
100
+ {
101
+ type: "text",
102
+ text: "Upload-Post video studio is ready.",
103
+ },
104
+ ],
105
+ };
106
+ });
107
+ }
108
+ const uploadStudioHtml = `<!doctype html>
109
+ <html lang="en">
110
+ <head>
111
+ <meta charset="utf-8" />
112
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
113
+ <style>
114
+ :root {
115
+ color-scheme: light dark;
116
+ --bg: #f7f4ee;
117
+ --panel: #fffaf2;
118
+ --ink: #211b14;
119
+ --muted: #756b5d;
120
+ --line: #dfd4c2;
121
+ --accent: #ff6a3d;
122
+ --accent-ink: #fff7f0;
123
+ --soft: #efe6d8;
124
+ --ok: #147b52;
125
+ --warn: #a14b10;
126
+ --bad: #b42318;
127
+ font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
128
+ }
129
+ @media (prefers-color-scheme: dark) {
130
+ :root {
131
+ --bg: #151311;
132
+ --panel: #201c18;
133
+ --ink: #f6efe5;
134
+ --muted: #b7aa98;
135
+ --line: #3a3027;
136
+ --soft: #2b251f;
137
+ --accent: #ff7a4f;
138
+ --accent-ink: #1e120d;
139
+ --ok: #54c897;
140
+ --warn: #f5a15d;
141
+ --bad: #ff7a70;
142
+ }
143
+ }
144
+ * { box-sizing: border-box; }
145
+ body {
146
+ margin: 0;
147
+ background: var(--bg);
148
+ color: var(--ink);
149
+ }
150
+ .shell {
151
+ max-width: 980px;
152
+ margin: 0 auto;
153
+ padding: 18px;
154
+ }
155
+ .top {
156
+ display: flex;
157
+ align-items: flex-start;
158
+ justify-content: space-between;
159
+ gap: 16px;
160
+ margin-bottom: 16px;
161
+ }
162
+ .eyebrow {
163
+ margin: 0 0 4px;
164
+ color: var(--muted);
165
+ font-size: 12px;
166
+ letter-spacing: .14em;
167
+ text-transform: uppercase;
168
+ }
169
+ h1 {
170
+ margin: 0;
171
+ font-size: 24px;
172
+ line-height: 1.1;
173
+ }
174
+ .chip {
175
+ border: 1px solid var(--line);
176
+ border-radius: 999px;
177
+ padding: 6px 10px;
178
+ color: var(--muted);
179
+ background: color-mix(in srgb, var(--panel) 78%, transparent);
180
+ font-size: 12px;
181
+ white-space: nowrap;
182
+ }
183
+ .grid {
184
+ display: grid;
185
+ grid-template-columns: minmax(260px, .85fr) minmax(320px, 1.15fr);
186
+ gap: 14px;
187
+ }
188
+ .panel {
189
+ border: 1px solid var(--line);
190
+ border-radius: 22px;
191
+ background: var(--panel);
192
+ box-shadow: 0 18px 40px rgba(49, 35, 16, .08);
193
+ }
194
+ .drop {
195
+ min-height: 360px;
196
+ padding: 16px;
197
+ display: flex;
198
+ flex-direction: column;
199
+ gap: 14px;
200
+ }
201
+ .dropTarget {
202
+ border: 1.5px dashed var(--line);
203
+ border-radius: 18px;
204
+ background: var(--soft);
205
+ min-height: 170px;
206
+ display: grid;
207
+ place-items: center;
208
+ text-align: center;
209
+ padding: 16px;
210
+ transition: border-color .16s ease, transform .16s ease;
211
+ }
212
+ .dropTarget.is-over {
213
+ border-color: var(--accent);
214
+ transform: translateY(-1px);
215
+ }
216
+ .dropTitle {
217
+ margin: 0 0 6px;
218
+ font-size: 17px;
219
+ font-weight: 700;
220
+ }
221
+ .dropHint {
222
+ margin: 0;
223
+ color: var(--muted);
224
+ font-size: 13px;
225
+ }
226
+ .actions {
227
+ display: flex;
228
+ flex-wrap: wrap;
229
+ gap: 8px;
230
+ }
231
+ button {
232
+ appearance: none;
233
+ border: 1px solid var(--line);
234
+ border-radius: 12px;
235
+ background: var(--panel);
236
+ color: var(--ink);
237
+ padding: 10px 12px;
238
+ font: inherit;
239
+ font-weight: 700;
240
+ cursor: pointer;
241
+ }
242
+ button.primary {
243
+ border-color: color-mix(in srgb, var(--accent) 75%, var(--line));
244
+ background: var(--accent);
245
+ color: var(--accent-ink);
246
+ }
247
+ button:disabled {
248
+ cursor: not-allowed;
249
+ opacity: .58;
250
+ }
251
+ .fileMeta {
252
+ border: 1px solid var(--line);
253
+ border-radius: 14px;
254
+ padding: 10px;
255
+ color: var(--muted);
256
+ font-size: 13px;
257
+ word-break: break-word;
258
+ }
259
+ video {
260
+ width: 100%;
261
+ max-height: 260px;
262
+ border-radius: 16px;
263
+ background: #000;
264
+ }
265
+ .form {
266
+ padding: 16px;
267
+ }
268
+ .row {
269
+ display: grid;
270
+ grid-template-columns: 1fr 170px;
271
+ gap: 10px;
272
+ }
273
+ label {
274
+ display: block;
275
+ margin: 0 0 6px;
276
+ color: var(--muted);
277
+ font-size: 12px;
278
+ font-weight: 700;
279
+ text-transform: uppercase;
280
+ letter-spacing: .08em;
281
+ }
282
+ input[type="text"], textarea, select {
283
+ width: 100%;
284
+ border: 1px solid var(--line);
285
+ border-radius: 13px;
286
+ background: color-mix(in srgb, var(--panel) 90%, white 10%);
287
+ color: var(--ink);
288
+ padding: 11px 12px;
289
+ font: inherit;
290
+ outline: none;
291
+ }
292
+ textarea {
293
+ min-height: 126px;
294
+ resize: vertical;
295
+ }
296
+ input[type="text"]:focus, textarea:focus, select:focus {
297
+ border-color: var(--accent);
298
+ box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 18%, transparent);
299
+ }
300
+ .field {
301
+ margin-bottom: 13px;
302
+ }
303
+ .platforms {
304
+ display: grid;
305
+ grid-template-columns: repeat(2, minmax(0, 1fr));
306
+ gap: 8px;
307
+ }
308
+ .platform {
309
+ display: flex;
310
+ align-items: center;
311
+ gap: 8px;
312
+ border: 1px solid var(--line);
313
+ border-radius: 12px;
314
+ padding: 9px 10px;
315
+ background: color-mix(in srgb, var(--panel) 86%, var(--soft));
316
+ color: var(--ink);
317
+ font-size: 13px;
318
+ text-transform: none;
319
+ letter-spacing: 0;
320
+ }
321
+ .platform input {
322
+ accent-color: var(--accent);
323
+ }
324
+ .footer {
325
+ display: flex;
326
+ align-items: center;
327
+ justify-content: space-between;
328
+ gap: 12px;
329
+ border-top: 1px solid var(--line);
330
+ margin-top: 14px;
331
+ padding-top: 14px;
332
+ }
333
+ .status {
334
+ color: var(--muted);
335
+ font-size: 13px;
336
+ }
337
+ .status[data-tone="ok"] { color: var(--ok); }
338
+ .status[data-tone="warn"] { color: var(--warn); }
339
+ .status[data-tone="bad"] { color: var(--bad); }
340
+ pre {
341
+ display: none;
342
+ white-space: pre-wrap;
343
+ overflow-wrap: anywhere;
344
+ margin: 14px 0 0;
345
+ border: 1px solid var(--line);
346
+ border-radius: 14px;
347
+ padding: 12px;
348
+ background: var(--soft);
349
+ color: var(--ink);
350
+ font-size: 12px;
351
+ }
352
+ @media (max-width: 760px) {
353
+ .shell { padding: 14px; }
354
+ .grid, .row { grid-template-columns: 1fr; }
355
+ .platforms { grid-template-columns: 1fr; }
356
+ .top, .footer { flex-direction: column; align-items: stretch; }
357
+ }
358
+ </style>
359
+ </head>
360
+ <body>
361
+ <div class="shell">
362
+ <header class="top">
363
+ <div>
364
+ <p class="eyebrow">Upload-Post</p>
365
+ <h1>Video upload studio</h1>
366
+ </div>
367
+ <div id="statusChip" class="chip">Ready</div>
368
+ </header>
369
+
370
+ <main class="grid">
371
+ <section class="panel drop">
372
+ <div id="dropTarget" class="dropTarget">
373
+ <div>
374
+ <p class="dropTitle">Select a video</p>
375
+ <p class="dropHint">MP4/MOV recommended. The file uploads to Upload-Post staging only when you publish.</p>
376
+ </div>
377
+ </div>
378
+ <input id="fileInput" type="file" accept="video/*" hidden />
379
+ <div class="actions">
380
+ <button id="pickButton" type="button">Choose file</button>
381
+ <button id="libraryButton" type="button">ChatGPT files</button>
382
+ </div>
383
+ <div id="fileMeta" class="fileMeta">No video selected.</div>
384
+ <video id="preview" controls hidden></video>
385
+ </section>
386
+
387
+ <section class="panel form">
388
+ <div class="row">
389
+ <div class="field">
390
+ <label for="profile">Profile</label>
391
+ <input id="profile" type="text" autocomplete="off" placeholder="automated-tests" />
392
+ </div>
393
+ <div class="field">
394
+ <label for="instagramFormat">Instagram</label>
395
+ <select id="instagramFormat">
396
+ <option value="REELS">Reels</option>
397
+ <option value="STORIES">Stories</option>
398
+ </select>
399
+ </div>
400
+ </div>
401
+
402
+ <div class="field">
403
+ <label>Platforms</label>
404
+ <div id="platforms" class="platforms"></div>
405
+ </div>
406
+
407
+ <div class="field">
408
+ <label for="caption">Caption</label>
409
+ <textarea id="caption" placeholder="Write the post caption..."></textarea>
410
+ </div>
411
+
412
+ <div class="field">
413
+ <label for="firstComment">First comment</label>
414
+ <input id="firstComment" type="text" placeholder="Optional" />
415
+ </div>
416
+
417
+ <div class="footer">
418
+ <div id="statusText" class="status">Waiting for a video.</div>
419
+ <button id="publishButton" class="primary" type="button">Publish video</button>
420
+ </div>
421
+ <pre id="result"></pre>
422
+ </section>
423
+ </main>
424
+ </div>
425
+
426
+ <script>
427
+ (function () {
428
+ var openai = window.openai || {};
429
+ var platformDefs = [
430
+ ["instagram", "Instagram"],
431
+ ["tiktok", "TikTok"],
432
+ ["youtube", "YouTube"],
433
+ ["linkedin", "LinkedIn"],
434
+ ["facebook", "Facebook"],
435
+ ["pinterest", "Pinterest"],
436
+ ["threads", "Threads"],
437
+ ["bluesky", "Bluesky"],
438
+ ["x", "X"],
439
+ ["google_business", "Google Business"],
440
+ ["discord", "Discord"],
441
+ ["telegram", "Telegram"]
442
+ ];
443
+ var state = {
444
+ file: null,
445
+ fileId: "",
446
+ fileName: "",
447
+ mimeType: "",
448
+ previewUrl: ""
449
+ };
450
+
451
+ var els = {
452
+ statusChip: document.getElementById("statusChip"),
453
+ statusText: document.getElementById("statusText"),
454
+ fileInput: document.getElementById("fileInput"),
455
+ pickButton: document.getElementById("pickButton"),
456
+ libraryButton: document.getElementById("libraryButton"),
457
+ dropTarget: document.getElementById("dropTarget"),
458
+ fileMeta: document.getElementById("fileMeta"),
459
+ preview: document.getElementById("preview"),
460
+ profile: document.getElementById("profile"),
461
+ instagramFormat: document.getElementById("instagramFormat"),
462
+ platforms: document.getElementById("platforms"),
463
+ caption: document.getElementById("caption"),
464
+ firstComment: document.getElementById("firstComment"),
465
+ publishButton: document.getElementById("publishButton"),
466
+ result: document.getElementById("result")
467
+ };
468
+
469
+ function initialData() {
470
+ var persisted = (openai.widgetState && openai.widgetState.uploadPostStudio) || {};
471
+ return Object.assign(
472
+ {},
473
+ persisted.form || {},
474
+ openai.toolOutput || {},
475
+ openai.toolInput || {}
476
+ );
477
+ }
478
+
479
+ function setStatus(text, tone) {
480
+ els.statusText.textContent = text;
481
+ els.statusText.dataset.tone = tone || "";
482
+ els.statusChip.textContent = tone === "ok" ? "Ready" : tone === "bad" ? "Needs attention" : "Working";
483
+ notifyHeight();
484
+ }
485
+
486
+ function notifyHeight() {
487
+ if (!openai.notifyIntrinsicHeight) return;
488
+ window.requestAnimationFrame(function () {
489
+ openai.notifyIntrinsicHeight(document.body.scrollHeight);
490
+ });
491
+ }
492
+
493
+ function renderPlatforms(selected) {
494
+ var selectedSet = new Set((selected && selected.length ? selected : ["instagram"]).map(String));
495
+ els.platforms.innerHTML = "";
496
+ platformDefs.forEach(function (item) {
497
+ var value = item[0];
498
+ var label = item[1];
499
+ var wrap = document.createElement("label");
500
+ wrap.className = "platform";
501
+ var input = document.createElement("input");
502
+ input.type = "checkbox";
503
+ input.value = value;
504
+ input.checked = selectedSet.has(value);
505
+ input.addEventListener("change", saveState);
506
+ var span = document.createElement("span");
507
+ span.textContent = label;
508
+ wrap.appendChild(input);
509
+ wrap.appendChild(span);
510
+ els.platforms.appendChild(wrap);
511
+ });
512
+ }
513
+
514
+ function selectedPlatforms() {
515
+ return Array.from(els.platforms.querySelectorAll("input:checked")).map(function (input) {
516
+ return input.value;
517
+ });
518
+ }
519
+
520
+ function currentForm() {
521
+ return {
522
+ user: els.profile.value.trim(),
523
+ platforms: selectedPlatforms(),
524
+ title: els.caption.value.trim(),
525
+ description: els.caption.value.trim(),
526
+ firstComment: els.firstComment.value.trim(),
527
+ instagramMediaType: els.instagramFormat.value
528
+ };
529
+ }
530
+
531
+ function saveState() {
532
+ if (!openai.setWidgetState) return;
533
+ openai.setWidgetState({
534
+ uploadPostStudio: {
535
+ form: currentForm(),
536
+ file: {
537
+ fileId: state.fileId,
538
+ fileName: state.fileName,
539
+ mimeType: state.mimeType
540
+ }
541
+ }
542
+ });
543
+ }
544
+
545
+ function applyInitial() {
546
+ var data = initialData();
547
+ els.profile.value = data.user || "";
548
+ els.caption.value = data.title || data.description || "";
549
+ els.firstComment.value = data.firstComment || "";
550
+ els.instagramFormat.value = data.instagramMediaType || "REELS";
551
+ renderPlatforms(data.platforms);
552
+ els.libraryButton.hidden = !openai.selectFiles;
553
+ [els.profile, els.caption, els.firstComment, els.instagramFormat].forEach(function (el) {
554
+ el.addEventListener("input", saveState);
555
+ el.addEventListener("change", saveState);
556
+ });
557
+ if (!openai.callTool) {
558
+ setStatus("This widget must run inside ChatGPT Apps.", "bad");
559
+ els.publishButton.disabled = true;
560
+ }
561
+ notifyHeight();
562
+ }
563
+
564
+ function formatBytes(bytes) {
565
+ if (!bytes && bytes !== 0) return "";
566
+ var units = ["B", "KB", "MB", "GB"];
567
+ var size = bytes;
568
+ var unit = 0;
569
+ while (size >= 1024 && unit < units.length - 1) {
570
+ size = size / 1024;
571
+ unit += 1;
572
+ }
573
+ return size.toFixed(unit === 0 ? 0 : 1) + " " + units[unit];
574
+ }
575
+
576
+ function setLocalFile(file) {
577
+ if (!file) return;
578
+ var name = file.name || "";
579
+ var looksLikeVideo =
580
+ (file.type && file.type.indexOf("video/") === 0) ||
581
+ /\\.(mp4|mov|m4v|webm|mpeg|mpg|avi|mkv)$/i.test(name);
582
+ if (!looksLikeVideo) {
583
+ setStatus("Select a video file.", "bad");
584
+ return;
585
+ }
586
+ if (state.previewUrl) URL.revokeObjectURL(state.previewUrl);
587
+ state.file = file;
588
+ state.fileId = "";
589
+ state.fileName = file.name || "chatgpt-video-upload.mp4";
590
+ state.mimeType = file.type || "video/mp4";
591
+ state.previewUrl = URL.createObjectURL(file);
592
+ els.preview.src = state.previewUrl;
593
+ els.preview.hidden = false;
594
+ els.fileMeta.textContent = state.fileName + " · " + formatBytes(file.size) + " · Upload-Post staging";
595
+ setStatus("Video selected. Ready to publish.", "ok");
596
+ saveState();
597
+ }
598
+
599
+ async function setLibraryFile() {
600
+ if (!openai.selectFiles) return;
601
+ var files = await openai.selectFiles();
602
+ var file = files && files[0];
603
+ if (!file) return;
604
+ state.file = null;
605
+ state.fileId = file.fileId || file.file_id || "";
606
+ state.fileName = file.fileName || file.file_name || "chatgpt-library-video.mp4";
607
+ state.mimeType = file.mimeType || file.mime_type || "";
608
+ if (state.previewUrl) URL.revokeObjectURL(state.previewUrl);
609
+ state.previewUrl = "";
610
+ els.preview.hidden = true;
611
+ els.preview.removeAttribute("src");
612
+ els.fileMeta.textContent = state.fileName + " · ChatGPT file library";
613
+ setStatus("Library video selected. Ready to publish.", "ok");
614
+ saveState();
615
+ }
616
+
617
+ function parseToolJson(result) {
618
+ if (!result) throw new Error("Tool returned no response.");
619
+ if (result.structuredContent) {
620
+ // Tools wrap payloads via ok() as { result: <payload> }. Unwrap so
621
+ // callers read fields (success, upload_url, …) at the top level,
622
+ // matching the unwrapped JSON we'd get from the text branch below.
623
+ var sc = result.structuredContent;
624
+ return sc && typeof sc === "object" && "result" in sc ? sc.result : sc;
625
+ }
626
+ if (Array.isArray(result.content)) {
627
+ var text = result.content.map(function (item) {
628
+ return item && item.text ? item.text : "";
629
+ }).join("\\n").trim();
630
+ if (text) {
631
+ try {
632
+ return JSON.parse(text);
633
+ } catch (error) {
634
+ throw new Error(text);
635
+ }
636
+ }
637
+ }
638
+ return result;
639
+ }
640
+
641
+ function originFromUrl(value) {
642
+ try {
643
+ return new URL(value).origin;
644
+ } catch (error) {
645
+ return "unknown upload origin";
646
+ }
647
+ }
648
+
649
+ function explainBrowserUploadFailure(error, uploadUrl) {
650
+ var detail = error && error.message ? error.message : String(error || "Failed to fetch");
651
+ var browserOrigin = window.location && window.location.origin ? window.location.origin : "unknown browser origin";
652
+ var uploadOrigin = originFromUrl(uploadUrl);
653
+ return new Error(
654
+ "Browser could not upload the file to Upload-Post/R2 staging (" + detail + "). " +
655
+ "This is usually R2 CORS or widget CSP. Allow the R2 bucket origin " + uploadOrigin +
656
+ " to receive PUT requests from " + browserOrigin +
657
+ " (or use AllowedOrigins '*'), with methods PUT, GET, HEAD and headers Content-Type or '*'."
658
+ );
659
+ }
660
+
661
+ async function uploadLocalFileToStaging(file) {
662
+ var contentType = state.mimeType || file.type || "video/mp4";
663
+ setStatus("Creating Upload-Post upload URL…", "warn");
664
+ var created = parseToolJson(await openai.callTool("create_media_upload", {
665
+ filename: state.fileName || file.name || "chatgpt-video-upload.mp4",
666
+ contentType: contentType,
667
+ contentLength: file.size,
668
+ mediaType: "video",
669
+ source: "mcp_chatgpt"
670
+ }));
671
+ if (!created.success || !created.upload_url || !created.upload_id) {
672
+ throw new Error(created.message || "Could not create media upload.");
673
+ }
674
+
675
+ setStatus("Uploading video to Upload-Post staging…", "warn");
676
+ var putHeaders = Object.assign({}, created.headers || {});
677
+ if (!putHeaders["Content-Type"]) putHeaders["Content-Type"] = contentType;
678
+ var put;
679
+ try {
680
+ put = await fetch(created.upload_url, {
681
+ method: created.method || "PUT",
682
+ mode: "cors",
683
+ headers: putHeaders,
684
+ body: file
685
+ });
686
+ } catch (error) {
687
+ throw explainBrowserUploadFailure(error, created.upload_url);
688
+ }
689
+ if (!put.ok) {
690
+ throw new Error("R2 upload failed with HTTP " + put.status + ".");
691
+ }
692
+
693
+ setStatus("Validating staged media…", "warn");
694
+ var completed = parseToolJson(await openai.callTool("complete_media_upload", {
695
+ uploadId: created.upload_id
696
+ }));
697
+ if (!completed.success || !completed.media_url) {
698
+ throw new Error(completed.message || "Could not complete media upload.");
699
+ }
700
+ return completed.media_url;
701
+ }
702
+
703
+ async function ensureDownloadUrl() {
704
+ if (state.file) {
705
+ return uploadLocalFileToStaging(state.file);
706
+ }
707
+ if (!state.fileId) {
708
+ throw new Error("Select a video first.");
709
+ }
710
+ if (!openai.getFileDownloadUrl) {
711
+ throw new Error("ChatGPT file download URLs are not available in this session.");
712
+ }
713
+ setStatus("Preparing temporary download URL…", "warn");
714
+ var ref = await openai.getFileDownloadUrl({ fileId: state.fileId });
715
+ var downloadUrl = typeof ref === "string" ? ref : ref.downloadUrl || ref.download_url;
716
+ if (!downloadUrl) {
717
+ throw new Error("ChatGPT did not return a file download URL.");
718
+ }
719
+ return downloadUrl;
720
+ }
721
+
722
+ function resultText(result) {
723
+ if (!result) return "No response.";
724
+ if (Array.isArray(result.content)) {
725
+ return result.content.map(function (item) {
726
+ return item && item.text ? item.text : JSON.stringify(item);
727
+ }).join("\\n");
728
+ }
729
+ if (result.structuredContent) return JSON.stringify(result.structuredContent, null, 2);
730
+ return JSON.stringify(result, null, 2);
731
+ }
732
+
733
+ async function publish() {
734
+ var form = currentForm();
735
+ if (!form.user) throw new Error("Profile is required.");
736
+ if (!form.platforms.length) throw new Error("Select at least one platform.");
737
+ if (!state.file && !state.fileId) throw new Error("Select a video first.");
738
+ var downloadUrl = await ensureDownloadUrl();
739
+ var platformOptions = {};
740
+ if (form.platforms.indexOf("instagram") >= 0) {
741
+ platformOptions.instagramMediaType = form.instagramMediaType;
742
+ }
743
+ var payload = {
744
+ videoPathOrUrl: downloadUrl,
745
+ videoFilename: state.fileName || "chatgpt-video-upload.mp4",
746
+ user: form.user,
747
+ platforms: form.platforms,
748
+ title: form.title,
749
+ description: form.description,
750
+ firstComment: form.firstComment || undefined,
751
+ asyncUpload: true,
752
+ platformOptions: platformOptions
753
+ };
754
+ setStatus("Calling Upload-Post…", "warn");
755
+ var result = await openai.callTool("upload_video", payload);
756
+ els.result.style.display = "block";
757
+ els.result.textContent = resultText(result);
758
+ setStatus("Upload queued. You can close this window; request_id is only for optional status checks.", "ok");
759
+ }
760
+
761
+ els.pickButton.addEventListener("click", function () {
762
+ els.fileInput.click();
763
+ });
764
+ els.fileInput.addEventListener("change", function (event) {
765
+ setLocalFile(event.target.files && event.target.files[0]);
766
+ });
767
+ els.libraryButton.addEventListener("click", function () {
768
+ setLibraryFile().catch(function (error) {
769
+ setStatus(error.message || String(error), "bad");
770
+ });
771
+ });
772
+ ["dragenter", "dragover"].forEach(function (name) {
773
+ els.dropTarget.addEventListener(name, function (event) {
774
+ event.preventDefault();
775
+ els.dropTarget.classList.add("is-over");
776
+ });
777
+ });
778
+ ["dragleave", "drop"].forEach(function (name) {
779
+ els.dropTarget.addEventListener(name, function (event) {
780
+ event.preventDefault();
781
+ els.dropTarget.classList.remove("is-over");
782
+ });
783
+ });
784
+ els.dropTarget.addEventListener("drop", function (event) {
785
+ setLocalFile(event.dataTransfer.files && event.dataTransfer.files[0]);
786
+ });
787
+ els.publishButton.addEventListener("click", function () {
788
+ els.publishButton.disabled = true;
789
+ els.result.style.display = "none";
790
+ publish()
791
+ .catch(function (error) {
792
+ els.result.style.display = "block";
793
+ els.result.textContent = error && error.message ? error.message : String(error);
794
+ setStatus(error && error.message ? error.message : String(error), "bad");
795
+ })
796
+ .finally(function () {
797
+ els.publishButton.disabled = false;
798
+ saveState();
799
+ notifyHeight();
800
+ });
801
+ });
802
+
803
+ applyInitial();
804
+ })();
805
+ </script>
806
+ </body>
807
+ </html>`;
808
+ //# sourceMappingURL=upload_studio.js.map