@sync.so/mcp-server 0.1.4 → 0.2.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 (261) hide show
  1. package/README.md +261 -86
  2. package/deploy/generation-status/app.html.gz +0 -0
  3. package/deploy/generation-status/release.json +6 -0
  4. package/dist/abort-signals.d.ts +6 -0
  5. package/dist/abort-signals.d.ts.map +1 -0
  6. package/dist/abort-signals.js +23 -0
  7. package/dist/account-features.d.ts +7 -0
  8. package/dist/account-features.d.ts.map +1 -0
  9. package/dist/account-features.js +48 -0
  10. package/dist/app-upload-relay.d.ts +13 -0
  11. package/dist/app-upload-relay.d.ts.map +1 -0
  12. package/dist/app-upload-relay.js +136 -0
  13. package/dist/asset-thumbnail.d.ts +3 -0
  14. package/dist/asset-thumbnail.d.ts.map +1 -0
  15. package/dist/asset-thumbnail.js +2 -0
  16. package/dist/auth/async-context.d.ts +5 -0
  17. package/dist/auth/async-context.d.ts.map +1 -0
  18. package/dist/auth/async-context.js +14 -0
  19. package/dist/auth/bearer-auth.d.ts +16 -0
  20. package/dist/auth/bearer-auth.d.ts.map +1 -0
  21. package/dist/auth/bearer-auth.js +55 -0
  22. package/dist/auth/client-credentials.d.ts +13 -0
  23. package/dist/auth/client-credentials.d.ts.map +1 -0
  24. package/dist/auth/client-credentials.js +67 -0
  25. package/dist/auth/device-auth.d.ts.map +1 -1
  26. package/dist/auth/device-auth.js +3 -2
  27. package/dist/auth/fixture-cleanup.d.ts +4 -0
  28. package/dist/auth/fixture-cleanup.d.ts.map +1 -0
  29. package/dist/auth/fixture-cleanup.js +9 -0
  30. package/dist/auth/index.d.ts +2 -0
  31. package/dist/auth/index.d.ts.map +1 -1
  32. package/dist/auth/index.js +2 -0
  33. package/dist/auth/oauth-diagnostics.d.ts +11 -0
  34. package/dist/auth/oauth-diagnostics.d.ts.map +1 -0
  35. package/dist/auth/oauth-diagnostics.js +78 -0
  36. package/dist/auth/oauth-provider.d.ts +16 -0
  37. package/dist/auth/oauth-provider.d.ts.map +1 -0
  38. package/dist/auth/oauth-provider.js +107 -0
  39. package/dist/auth/resource-metadata.d.ts +2 -0
  40. package/dist/auth/resource-metadata.d.ts.map +1 -0
  41. package/dist/auth/resource-metadata.js +3 -0
  42. package/dist/auth/token-verification.d.ts +11 -0
  43. package/dist/auth/token-verification.d.ts.map +1 -0
  44. package/dist/auth/token-verification.js +99 -0
  45. package/dist/auth/verification-cache.d.ts +8 -0
  46. package/dist/auth/verification-cache.d.ts.map +1 -0
  47. package/dist/auth/verification-cache.js +111 -0
  48. package/dist/chatgpt-analytics.d.ts +22 -0
  49. package/dist/chatgpt-analytics.d.ts.map +1 -0
  50. package/dist/chatgpt-analytics.js +172 -0
  51. package/dist/chatgpt-app.d.ts +15 -0
  52. package/dist/chatgpt-app.d.ts.map +1 -0
  53. package/dist/chatgpt-app.js +133 -0
  54. package/dist/chatgpt-release.d.ts +12 -0
  55. package/dist/chatgpt-release.d.ts.map +1 -0
  56. package/dist/chatgpt-release.js +30 -0
  57. package/dist/cli.js +30 -16
  58. package/dist/client-profile.d.ts +10 -0
  59. package/dist/client-profile.d.ts.map +1 -0
  60. package/dist/client-profile.js +45 -0
  61. package/dist/config.d.ts +17 -0
  62. package/dist/config.d.ts.map +1 -1
  63. package/dist/config.js +52 -0
  64. package/dist/generation-status-html.d.ts +2 -0
  65. package/dist/generation-status-html.d.ts.map +1 -0
  66. package/dist/generation-status-html.js +26 -0
  67. package/dist/generation-status.d.ts +6 -0
  68. package/dist/generation-status.d.ts.map +1 -0
  69. package/dist/generation-status.js +37 -0
  70. package/dist/http-client.d.ts +45 -1
  71. package/dist/http-client.d.ts.map +1 -1
  72. package/dist/http-client.js +171 -34
  73. package/dist/http-server.d.ts +53 -0
  74. package/dist/http-server.d.ts.map +1 -0
  75. package/dist/http-server.js +753 -0
  76. package/dist/http-session.d.ts +18 -0
  77. package/dist/http-session.d.ts.map +1 -0
  78. package/dist/http-session.js +189 -0
  79. package/dist/index.d.ts +4 -3
  80. package/dist/index.d.ts.map +1 -1
  81. package/dist/index.js +3 -2
  82. package/dist/oauth-response.d.ts +3 -0
  83. package/dist/oauth-response.d.ts.map +1 -0
  84. package/dist/oauth-response.js +30 -0
  85. package/dist/openapi/index.d.ts +2 -2
  86. package/dist/openapi/index.d.ts.map +1 -1
  87. package/dist/openapi/index.js +1 -1
  88. package/dist/openapi/parser.d.ts +2 -0
  89. package/dist/openapi/parser.d.ts.map +1 -1
  90. package/dist/openapi/parser.js +41 -1
  91. package/dist/openapi/types.d.ts +3 -0
  92. package/dist/openapi/types.d.ts.map +1 -1
  93. package/dist/organization-selection.d.ts +61 -0
  94. package/dist/organization-selection.d.ts.map +1 -0
  95. package/dist/organization-selection.js +142 -0
  96. package/dist/rate-limits.d.ts +13 -0
  97. package/dist/rate-limits.d.ts.map +1 -0
  98. package/dist/rate-limits.js +67 -0
  99. package/dist/revision.d.ts +6 -0
  100. package/dist/revision.d.ts.map +1 -0
  101. package/dist/revision.js +17 -0
  102. package/dist/runtime-diagnostics.d.ts +32 -0
  103. package/dist/runtime-diagnostics.d.ts.map +1 -0
  104. package/dist/runtime-diagnostics.js +58 -0
  105. package/dist/server.d.ts +23 -2
  106. package/dist/server.d.ts.map +1 -1
  107. package/dist/server.js +893 -32
  108. package/dist/session-registry.d.ts +54 -0
  109. package/dist/session-registry.d.ts.map +1 -0
  110. package/dist/session-registry.js +202 -0
  111. package/dist/session-telemetry.d.ts +13 -0
  112. package/dist/session-telemetry.d.ts.map +1 -0
  113. package/dist/session-telemetry.js +87 -0
  114. package/dist/spend-confirmation.d.ts +21 -0
  115. package/dist/spend-confirmation.d.ts.map +1 -0
  116. package/dist/spend-confirmation.js +105 -0
  117. package/dist/tool-analytics-context.d.ts +11 -0
  118. package/dist/tool-analytics-context.d.ts.map +1 -0
  119. package/dist/tool-analytics-context.js +8 -0
  120. package/dist/tool-names.d.ts +35 -0
  121. package/dist/tool-names.d.ts.map +1 -0
  122. package/dist/tool-names.js +133 -0
  123. package/dist/tools/account-context.d.ts +5 -0
  124. package/dist/tools/account-context.d.ts.map +1 -0
  125. package/dist/tools/account-context.js +131 -0
  126. package/dist/tools/account-management.d.ts +4 -0
  127. package/dist/tools/account-management.d.ts.map +1 -0
  128. package/dist/tools/account-management.js +111 -0
  129. package/dist/tools/app-tools.d.ts +31 -0
  130. package/dist/tools/app-tools.d.ts.map +1 -0
  131. package/dist/tools/app-tools.js +350 -0
  132. package/dist/tools/asset-analysis.d.ts +4 -0
  133. package/dist/tools/asset-analysis.d.ts.map +1 -0
  134. package/dist/tools/asset-analysis.js +172 -0
  135. package/dist/tools/asset-creators.d.ts +4 -0
  136. package/dist/tools/asset-creators.d.ts.map +1 -0
  137. package/dist/tools/asset-creators.js +38 -0
  138. package/dist/tools/audio-trim.d.ts +6 -0
  139. package/dist/tools/audio-trim.d.ts.map +1 -0
  140. package/dist/tools/audio-trim.js +79 -0
  141. package/dist/tools/batch-results.d.ts +16 -0
  142. package/dist/tools/batch-results.d.ts.map +1 -0
  143. package/dist/tools/batch-results.js +103 -0
  144. package/dist/tools/batches.d.ts +28 -0
  145. package/dist/tools/batches.d.ts.map +1 -0
  146. package/dist/tools/batches.js +293 -0
  147. package/dist/tools/dialogue.d.ts +6 -0
  148. package/dist/tools/dialogue.d.ts.map +1 -0
  149. package/dist/tools/dialogue.js +309 -0
  150. package/dist/tools/estimate-duration.d.ts +35 -0
  151. package/dist/tools/estimate-duration.d.ts.map +1 -0
  152. package/dist/tools/estimate-duration.js +66 -0
  153. package/dist/tools/feedback.d.ts +14 -0
  154. package/dist/tools/feedback.d.ts.map +1 -0
  155. package/dist/tools/feedback.js +72 -0
  156. package/dist/tools/generated-adapters.d.ts +25 -0
  157. package/dist/tools/generated-adapters.d.ts.map +1 -0
  158. package/dist/tools/generated-adapters.js +143 -0
  159. package/dist/tools/generation-context.d.ts +14 -0
  160. package/dist/tools/generation-context.d.ts.map +1 -0
  161. package/dist/tools/generation-context.js +39 -0
  162. package/dist/tools/generation-continuation.d.ts +4 -0
  163. package/dist/tools/generation-continuation.d.ts.map +1 -0
  164. package/dist/tools/generation-continuation.js +124 -0
  165. package/dist/tools/generation-costs.d.ts +6 -0
  166. package/dist/tools/generation-costs.d.ts.map +1 -0
  167. package/dist/tools/generation-costs.js +67 -0
  168. package/dist/tools/generation-output.d.ts +3 -0
  169. package/dist/tools/generation-output.d.ts.map +1 -0
  170. package/dist/tools/generation-output.js +39 -0
  171. package/dist/tools/generator.d.ts +35 -2
  172. package/dist/tools/generator.d.ts.map +1 -1
  173. package/dist/tools/generator.js +127 -9
  174. package/dist/tools/job-status.d.ts +35 -0
  175. package/dist/tools/job-status.d.ts.map +1 -0
  176. package/dist/tools/job-status.js +92 -0
  177. package/dist/tools/job-wait.d.ts +41 -0
  178. package/dist/tools/job-wait.d.ts.map +1 -0
  179. package/dist/tools/job-wait.js +84 -0
  180. package/dist/tools/jobs.d.ts +3 -0
  181. package/dist/tools/jobs.d.ts.map +1 -0
  182. package/dist/tools/jobs.js +210 -0
  183. package/dist/tools/media-upload.d.ts +27 -0
  184. package/dist/tools/media-upload.d.ts.map +1 -0
  185. package/dist/tools/media-upload.js +247 -0
  186. package/dist/tools/model-options.d.ts +36 -0
  187. package/dist/tools/model-options.d.ts.map +1 -0
  188. package/dist/tools/model-options.js +48 -0
  189. package/dist/tools/organization-choice.d.ts +9 -0
  190. package/dist/tools/organization-choice.d.ts.map +1 -0
  191. package/dist/tools/organization-choice.js +136 -0
  192. package/dist/tools/organizations.d.ts +56 -0
  193. package/dist/tools/organizations.d.ts.map +1 -0
  194. package/dist/tools/organizations.js +235 -0
  195. package/dist/tools/output-schemas.d.ts +54 -0
  196. package/dist/tools/output-schemas.d.ts.map +1 -0
  197. package/dist/tools/output-schemas.js +58 -0
  198. package/dist/tools/overrides.d.ts +3 -0
  199. package/dist/tools/overrides.d.ts.map +1 -1
  200. package/dist/tools/overrides.js +113 -15
  201. package/dist/tools/playback-metadata.d.ts +4 -0
  202. package/dist/tools/playback-metadata.d.ts.map +1 -0
  203. package/dist/tools/playback-metadata.js +51 -0
  204. package/dist/tools/project-destination.d.ts +13 -0
  205. package/dist/tools/project-destination.d.ts.map +1 -0
  206. package/dist/tools/project-destination.js +45 -0
  207. package/dist/tools/project-history.d.ts +6 -0
  208. package/dist/tools/project-history.d.ts.map +1 -0
  209. package/dist/tools/project-history.js +45 -0
  210. package/dist/tools/project-intent.d.ts +8 -0
  211. package/dist/tools/project-intent.d.ts.map +1 -0
  212. package/dist/tools/project-intent.js +87 -0
  213. package/dist/tools/running-submissions.d.ts +7 -0
  214. package/dist/tools/running-submissions.d.ts.map +1 -0
  215. package/dist/tools/running-submissions.js +91 -0
  216. package/dist/tools/speech.d.ts +6 -0
  217. package/dist/tools/speech.d.ts.map +1 -0
  218. package/dist/tools/speech.js +116 -0
  219. package/dist/tools/studio-draft.d.ts +7 -0
  220. package/dist/tools/studio-draft.d.ts.map +1 -0
  221. package/dist/tools/studio-draft.js +195 -0
  222. package/dist/tools/studio-view-contract.d.ts +184 -0
  223. package/dist/tools/studio-view-contract.d.ts.map +1 -0
  224. package/dist/tools/studio-view-contract.js +67 -0
  225. package/dist/tools/studio-view.d.ts +6 -0
  226. package/dist/tools/studio-view.d.ts.map +1 -0
  227. package/dist/tools/studio-view.js +126 -0
  228. package/dist/tools/timeline-lipsync.d.ts +316 -0
  229. package/dist/tools/timeline-lipsync.d.ts.map +1 -0
  230. package/dist/tools/timeline-lipsync.js +304 -0
  231. package/dist/tools/translation-preview.d.ts +8 -0
  232. package/dist/tools/translation-preview.d.ts.map +1 -0
  233. package/dist/tools/translation-preview.js +146 -0
  234. package/dist/tools/translation.d.ts +6 -0
  235. package/dist/tools/translation.d.ts.map +1 -0
  236. package/dist/tools/translation.js +131 -0
  237. package/dist/tools/upload-widget.d.ts +13 -0
  238. package/dist/tools/upload-widget.d.ts.map +1 -0
  239. package/dist/tools/upload-widget.js +788 -0
  240. package/dist/toolsets.d.ts +18 -0
  241. package/dist/toolsets.d.ts.map +1 -0
  242. package/dist/toolsets.js +157 -0
  243. package/dist/update-check.d.ts +38 -0
  244. package/dist/update-check.d.ts.map +1 -0
  245. package/dist/update-check.js +150 -0
  246. package/dist/upload-runtime.d.ts +45 -0
  247. package/dist/upload-runtime.d.ts.map +1 -0
  248. package/dist/upload-runtime.js +156 -0
  249. package/dist/upload-source.d.ts +4 -0
  250. package/dist/upload-source.d.ts.map +1 -0
  251. package/dist/upload-source.js +46 -0
  252. package/dist/version.d.ts +2 -0
  253. package/dist/version.d.ts.map +1 -0
  254. package/dist/version.js +3 -0
  255. package/package.json +77 -31
  256. package/dist/openapi/parser.test.d.ts +0 -2
  257. package/dist/openapi/parser.test.d.ts.map +0 -1
  258. package/dist/openapi/parser.test.js +0 -188
  259. package/dist/tools/generator.test.d.ts +0 -2
  260. package/dist/tools/generator.test.d.ts.map +0 -1
  261. package/dist/tools/generator.test.js +0 -178
@@ -0,0 +1,309 @@
1
+ import { z } from 'zod';
2
+ import { ApiRequestError } from '../http-client.js';
3
+ import { uploadRuntime } from '../upload-runtime.js';
4
+ import { generationContextHeaders, generationContextSchema } from './generation-context.js';
5
+ import { generateTools } from './generator.js';
6
+ import { uploadMediaAsset } from './media-upload.js';
7
+ import { modelOptionsSchema } from './model-options.js';
8
+ import { generationOutputSchema } from './output-schemas.js';
9
+ const paths = [
10
+ '/v2/transcriptions',
11
+ '/v2/transcriptions/{id}',
12
+ '/v2/dialogue-edits',
13
+ '/v2/dialogue-edits/{id}',
14
+ ];
15
+ export function dialogueOperations(operations) {
16
+ return operations.filter((operation) => paths.includes(operation.path));
17
+ }
18
+ const sourceSchema = z.object({ projectId: z.uuid(), videoAssetId: z.uuid() });
19
+ const object = z.record(z.string(), z.unknown());
20
+ const previewKeySchema = z.string().regex(/^[A-Za-z0-9._~-]{1,128}$/);
21
+ // The API's edit items are a oneOf, which the OpenAPI conversion turns into untyped items,
22
+ // so agents had to guess their shape.
23
+ const dialogueEdits = z
24
+ .array(z.discriminatedUnion('kind', [
25
+ z.object({
26
+ kind: z.literal('change'),
27
+ wordId: z.string().describe('Timed word ID from the source transcript.'),
28
+ replacement: z.string().min(1).max(500).describe('What the speaker says instead.'),
29
+ pronunciation: z
30
+ .string()
31
+ .min(1)
32
+ .max(100)
33
+ .optional()
34
+ .describe('Plain-text respelling for a name or word the voice would otherwise mispronounce.'),
35
+ }),
36
+ z.object({
37
+ kind: z.literal('remove'),
38
+ wordIds: z.array(z.string()).min(1).describe('Timed word IDs to remove.'),
39
+ }),
40
+ ]))
41
+ .min(1)
42
+ .max(100)
43
+ .describe('User-approved changes/removals referencing timed word IDs in the source transcript.');
44
+ /** transcriptionJobId is the API's name; transcriptionId is the older MCP name. */
45
+ function transcriptionJobId(args) {
46
+ const { transcriptionJobId: id, transcriptionId: alias } = args;
47
+ if (id && alias && id !== alias)
48
+ throw new Error('transcriptionJobId and transcriptionId name different transcriptions; pass transcriptionJobId only.');
49
+ return id ?? alias;
50
+ }
51
+ export function createDialogueTools(operations, httpClient) {
52
+ const selected = dialogueOperations(operations);
53
+ const tools = generateTools(selected, httpClient);
54
+ const find = (path, method) => tools[selected.findIndex((op) => op.path === path && op.method === method)];
55
+ const transcribe = find(paths[0], 'post');
56
+ const readTranscript = find(paths[1], 'get');
57
+ const preview = find(paths[2], 'post');
58
+ const readPreview = find(paths[3], 'get');
59
+ if (!transcribe || !readTranscript || !preview || !readPreview || !preview.inputSchema.edits)
60
+ return [];
61
+ const keyedPreview = selected.some((operation) => operation.path === paths[2] &&
62
+ operation.method === 'post' &&
63
+ operation.parameters.some((parameter) => parameter.in === 'header' && parameter.name.toLowerCase() === 'idempotency-key'));
64
+ async function source(args, signal) {
65
+ signal?.throwIfAborted();
66
+ const input = sourceSchema.parse(args);
67
+ const project = object.parse(await httpClient.request('get', `/v2/projects/${input.projectId}`, { signal }));
68
+ if (project.id !== input.projectId)
69
+ throw new Error('Could not verify project access.');
70
+ signal?.throwIfAborted();
71
+ const asset = z
72
+ .object({ id: z.uuid(), type: z.literal('VIDEO'), url: z.url(), name: z.string().nullish() })
73
+ .parse(await httpClient.request('get', `/v2/assets/${input.videoAssetId}`, { signal }));
74
+ if (asset.id !== input.videoAssetId)
75
+ throw new Error('Could not verify the source video.');
76
+ signal?.throwIfAborted();
77
+ return { ...input, sourceVideoUrl: asset.url, sourceName: asset.name ?? 'dialogue-source.mp4' };
78
+ }
79
+ async function copyToSyncStorage(input, signal) {
80
+ const assetId = await uploadMediaAsset(httpClient, 'video', {
81
+ download_url: input.sourceVideoUrl,
82
+ file_id: input.videoAssetId,
83
+ file_name: input.sourceName,
84
+ }, uploadRuntime, signal, { projectId: input.projectId, name: input.sourceName });
85
+ const asset = z
86
+ .object({ id: z.uuid(), type: z.literal('VIDEO'), url: z.url() })
87
+ .passthrough()
88
+ .parse(await httpClient.request('get', `/v2/assets/${assetId}`, { signal }));
89
+ if (asset.id !== assetId)
90
+ throw new Error('Could not verify the uploaded video.');
91
+ return asset;
92
+ }
93
+ async function checkedJob(tool, args, signal) {
94
+ const input = await source(args, signal);
95
+ const job = object.parse(await tool.handler({ id: z.uuid().parse(args.id) }, { signal }));
96
+ // URLs from the same API source must identify the same stored source. The
97
+ // generation API performs its own storage-object comparison before billing.
98
+ const sourceUrl = z.url().parse(job.sourceVideoUrl);
99
+ const a = new URL(sourceUrl);
100
+ const b = new URL(input.sourceVideoUrl);
101
+ if (a.origin !== b.origin || a.pathname !== b.pathname)
102
+ throw new Error('The job belongs to another source video.');
103
+ return job;
104
+ }
105
+ const readHints = { readOnlyHint: true, destructiveHint: false, openWorldHint: false };
106
+ const writeHints = {
107
+ readOnlyHint: false,
108
+ destructiveHint: true,
109
+ idempotentHint: false,
110
+ openWorldHint: true,
111
+ };
112
+ const generationSchema = sourceSchema.extend({
113
+ dialogueEditId: z.uuid(),
114
+ model: z.string().min(1),
115
+ options: modelOptionsSchema.optional(),
116
+ });
117
+ async function generationBody(args, signal) {
118
+ const input = generationSchema.parse(args);
119
+ await source(args, signal);
120
+ if (typeof input.options?.active_speaker_detection === 'boolean')
121
+ input.options.active_speaker_detection = {
122
+ auto_detect: input.options.active_speaker_detection,
123
+ };
124
+ return {
125
+ projectId: input.projectId,
126
+ model: input.model,
127
+ input: [{ type: 'video', assetId: input.videoAssetId }],
128
+ dialogueEdit: { id: input.dialogueEditId },
129
+ ...(input.options ? { options: input.options } : {}),
130
+ };
131
+ }
132
+ return [
133
+ {
134
+ name: 'dialogue_get-options',
135
+ title: 'Check Edit Dialogue availability',
136
+ description: 'Check whether the connected API supports timed-word Edit Dialogue. Does not transcribe, synthesize or generate.',
137
+ inputSchema: {},
138
+ annotations: readHints,
139
+ handler: async () => ({ available: true, keyedPreview, sourceUpload: true }),
140
+ },
141
+ {
142
+ name: 'dialogue_upload-source',
143
+ title: 'Copy a video for Edit Dialogue',
144
+ description: 'Copy an accessible saved video into Sync storage when transcription rejects its external source URL. Returns a new asset; use its id as videoAssetId for transcription, preview, and final video generation. Does not transcribe or spend generation credits.',
145
+ inputSchema: sourceSchema.shape,
146
+ outputSchema: {
147
+ asset: z.object({ id: z.uuid(), type: z.literal('VIDEO'), url: z.url() }).passthrough(),
148
+ },
149
+ annotations: writeHints,
150
+ handler: async (args, context) => ({
151
+ asset: await copyToSyncStorage(await source(args, context?.signal), context?.signal),
152
+ }),
153
+ },
154
+ {
155
+ name: 'dialogue-transcriptions_create',
156
+ title: 'Transcribe a video for Edit Dialogue',
157
+ description: 'Use this to start correcting or removing spoken words in a saved single-speaker video while keeping the original speaker voice. Transcribes timed words for Edit Dialogue; not whole-script replacement, translation, or choosing a different voice. Transcription is unbilled and identical sources reuse their existing job. A video outside Sync storage is copied there first; the result then carries the videoAssetId of that copy, which the rest of the workflow must use. Does not replace dialogue or generate a video.',
158
+ inputSchema: sourceSchema.shape,
159
+ annotations: writeHints,
160
+ handler: async (args, context) => {
161
+ const input = await source(args, context?.signal);
162
+ const start = (sourceVideoUrl) => transcribe.handler({ sourceVideoUrl, projectId: input.projectId, maxSourceSeconds: 600 }, context);
163
+ try {
164
+ return await start(input.sourceVideoUrl);
165
+ }
166
+ catch (error) {
167
+ // Agents rarely made the copy themselves after this rejection, so make it here.
168
+ if (!(error instanceof ApiRequestError) ||
169
+ error.status !== 422 ||
170
+ !error.message.includes('must be hosted in sync. labs storage'))
171
+ throw error;
172
+ const copy = await copyToSyncStorage(input, context?.signal);
173
+ try {
174
+ return { ...object.parse(await start(copy.url)), videoAssetId: copy.id };
175
+ }
176
+ catch (retryError) {
177
+ // Name the saved copy so a retry transcribes it instead of copying again.
178
+ if (retryError instanceof Error)
179
+ retryError.message += ` The video is saved in Sync storage as asset ${copy.id}; retry with videoAssetId ${copy.id}.`;
180
+ throw retryError;
181
+ }
182
+ }
183
+ },
184
+ },
185
+ {
186
+ name: 'dialogue-transcriptions_get',
187
+ title: 'Check an Edit Dialogue transcription',
188
+ description: 'Read the same transcription job until complete. Requires its original saved video and accessible project.',
189
+ inputSchema: sourceSchema.extend({ id: z.uuid() }).shape,
190
+ annotations: readHints,
191
+ handler: (args, context) => checkedJob(readTranscript, args, context?.signal),
192
+ },
193
+ {
194
+ name: 'dialogue-previews_create',
195
+ title: 'Preview dialogue edits',
196
+ description: 'Start an audio synthesis job for user-approved timed word changes/removals in a saved single-speaker video, preserving the original speaker voice. Requires a project, source video and completed transcription or prior preview accessible to the connected Sync account. May clone the source speaker voice and incur synthesis charges; confirm the edits and permission to use that voice before creating. Creates a saved preview without overwriting the source; does not generate the final video. Returns a preview job ID; poll dialogue-previews_get with the same project and source until completion, and disclose any failed edits on partial completion. Persist an idempotencyKey before creating when dialogue_get-options advertises keyedPreview. Recover only through an explicit retry of identical input with that same key; never retry an ambiguous creation automatically. Not for translation, choosing another voice, or arbitrary full-transcript replacement.',
197
+ inputSchema: sourceSchema.extend({
198
+ projectId: sourceSchema.shape.projectId.describe('Accessible Sync project for the preview. Keep unchanged on retries.'),
199
+ videoAssetId: sourceSchema.shape.videoAssetId.describe('Saved VIDEO asset for the transcription or prior preview, accessible to the connected account.'),
200
+ idempotencyKey: previewKeySchema
201
+ .optional()
202
+ .describe('Persist a unique key before creation when dialogue_get-options reports keyedPreview. Reuse only with identical inputs for an explicitly requested retry.'),
203
+ transcriptionJobId: z
204
+ .uuid()
205
+ .optional()
206
+ .describe('Completed transcription ID for this source. Supply exactly one of transcriptionJobId or previousPreviewId.'),
207
+ previousPreviewId: z
208
+ .uuid()
209
+ .optional()
210
+ .describe('Completed or partially completed preview ID for this source when reopening. Supply exactly one of previousPreviewId or transcriptionJobId.'),
211
+ edits: dialogueEdits,
212
+ transcriptionId: z
213
+ .uuid()
214
+ .optional()
215
+ .describe('Deprecated alias of transcriptionJobId.')
216
+ .meta({ deprecated: true }),
217
+ }).shape,
218
+ annotations: { ...writeHints, openWorldHint: false },
219
+ handler: async (args, context) => {
220
+ const key = previewKeySchema.optional().parse(args.idempotencyKey);
221
+ if (key && !keyedPreview)
222
+ throw new Error('This API does not support keyed dialogue preview recovery.');
223
+ const transcriptionId = transcriptionJobId(args);
224
+ if (Boolean(transcriptionId) === Boolean(args.previousPreviewId)) {
225
+ throw new Error('Provide one transcriptionJobId or previousPreviewId, not both.');
226
+ }
227
+ const input = await source(args, context?.signal);
228
+ const previous = args.previousPreviewId
229
+ ? await checkedJob(readPreview, { ...args, id: args.previousPreviewId }, context?.signal)
230
+ : undefined;
231
+ const transcriptJob = previous ??
232
+ (await checkedJob(readTranscript, { ...args, id: transcriptionId }, context?.signal));
233
+ if (transcriptJob.status !== 'COMPLETED' &&
234
+ !(previous && transcriptJob.status === 'COMPLETED_PARTIAL'))
235
+ throw new Error('Wait for the source transcript to finish.');
236
+ const body = z.object(preview.inputSchema).parse({
237
+ // Job snapshots retain the immutable source identity across rotating asset signatures.
238
+ sourceVideoUrl: z.url().parse(transcriptJob.sourceVideoUrl),
239
+ projectId: input.projectId,
240
+ transcript: previous ? previous.sourceTranscript : transcriptJob.transcript,
241
+ edits: args.edits,
242
+ ...(previous?.voiceId ? { voiceId: previous.voiceId } : {}),
243
+ ...(previous ? {} : { transcriptionJobId: transcriptionId }),
244
+ ...(previous && JSON.stringify(previous.edits) === JSON.stringify(args.edits)
245
+ ? { rerunOfJobId: previous.id }
246
+ : {}),
247
+ });
248
+ return httpClient.request('post', paths[2], {
249
+ body,
250
+ ...(key ? { headers: { 'Idempotency-Key': key } } : {}),
251
+ signal: context?.signal,
252
+ });
253
+ },
254
+ },
255
+ {
256
+ name: 'dialogue-previews_get',
257
+ title: 'Check a dialogue edit preview',
258
+ description: 'Read one existing Edit Dialogue audio preview. Partial completion must be disclosed, not described as every edit succeeding.',
259
+ inputSchema: sourceSchema.extend({ id: z.uuid() }).shape,
260
+ annotations: readHints,
261
+ handler: (args, context) => checkedJob(readPreview, args, context?.signal),
262
+ },
263
+ {
264
+ name: 'dialogue-videos_estimate',
265
+ title: 'Estimate an edited dialogue video',
266
+ description: 'Estimate final video cost using the completed dialogue preview and canonical server retiming. Does not generate or reserve credits.',
267
+ inputSchema: generationSchema.shape,
268
+ annotations: readHints,
269
+ handler: async (args, context) => httpClient.request('post', '/v2/analyze/cost', {
270
+ body: await generationBody(args, context?.signal),
271
+ signal: context?.signal,
272
+ }),
273
+ },
274
+ {
275
+ name: 'dialogue-videos_create',
276
+ title: 'Create the edited dialogue video',
277
+ description: 'Create a new lip-synced video from a saved single-speaker video and its completed, user-approved Edit Dialogue audio preview. Use this to render targeted corrections or removals of spoken words while preserving the original speaker voice. Not for translation, whole-script replacement, or choosing a different voice. ' +
278
+ "This is a paid action that starts a generation job and consumes Sync credits. First use dialogue-videos_estimate with the same source, preview and model, then obtain the user's approval of the preview, configuration and estimated cost. Requires a project and source video accessible to the connected account, and a completed dialogue preview for that same source. Creates a new output without overwriting the source video. " +
279
+ 'Returns a generation ID, not a finished video. Poll generations_get with that ID until completion. Persist one idempotency key for the approved request; on an uncertain response, reuse that key with identical inputs instead of starting a second paid job.',
280
+ inputSchema: generationSchema.extend({
281
+ projectId: sourceSchema.shape.projectId.describe('Accessible Sync project for the new output. Keep unchanged on retries.'),
282
+ videoAssetId: sourceSchema.shape.videoAssetId.describe('Saved VIDEO asset used for the approved dialogue preview, accessible to the connected account.'),
283
+ dialogueEditId: generationSchema.shape.dialogueEditId.describe('Completed Edit Dialogue audio preview ID for this source video and the current user-approved edits. Not a transcription or generation ID.'),
284
+ model: generationSchema.shape.model.describe('Lip-sync model returned by models_list, matching the approved estimate.'),
285
+ continuation: generationContextSchema
286
+ .optional()
287
+ .describe('Only for an explicitly requested continuation of an existing generation in this project. The workflow must be edit_dialogue.'),
288
+ idempotencyKey: z
289
+ .string()
290
+ .regex(/^[A-Za-z0-9._~-]{1,128}$/)
291
+ .describe('Persist one unique key per approved generation. Reuse the exact key and unchanged inputs when recovering an uncertain response; never automatically generate a replacement key.'),
292
+ }).shape,
293
+ outputSchema: generationOutputSchema,
294
+ annotations: { ...writeHints, openWorldHint: false },
295
+ handler: async (args, context) => {
296
+ const key = z
297
+ .string()
298
+ .regex(/^[A-Za-z0-9._~-]{1,128}$/)
299
+ .parse(args.idempotencyKey);
300
+ const lineage = await generationContextHeaders(httpClient, args, ['edit_dialogue'], context?.signal);
301
+ return httpClient.request('post', '/v2/generate', {
302
+ body: await generationBody(args, context?.signal),
303
+ headers: { ...lineage, 'Idempotency-Key': key },
304
+ signal: context?.signal,
305
+ });
306
+ },
307
+ },
308
+ ];
309
+ }
@@ -0,0 +1,35 @@
1
+ import { z } from 'zod';
2
+ export declare const estimateDurationInputs: {
3
+ script: z.ZodOptional<z.ZodString>;
4
+ audioDuration: z.ZodOptional<z.ZodNumber>;
5
+ visualType: z.ZodOptional<z.ZodEnum<{
6
+ image: "image";
7
+ video: "video";
8
+ }>>;
9
+ videoDuration: z.ZodOptional<z.ZodNumber>;
10
+ syncMode: z.ZodOptional<z.ZodEnum<{
11
+ bounce: "bounce";
12
+ cut_off: "cut_off";
13
+ loop: "loop";
14
+ remap: "remap";
15
+ silence: "silence";
16
+ }>>;
17
+ };
18
+ export declare function resolveEstimateDuration(args: Record<string, unknown>): {
19
+ args: {
20
+ [x: string]: unknown;
21
+ };
22
+ durationEstimate: undefined;
23
+ } | {
24
+ args: {
25
+ duration: number;
26
+ };
27
+ durationEstimate: {
28
+ durationSeconds: number;
29
+ audioDurationSeconds: number;
30
+ source: string;
31
+ approximate: boolean;
32
+ excludesSpeechSynthesis: boolean;
33
+ };
34
+ };
35
+ //# sourceMappingURL=estimate-duration.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"estimate-duration.d.ts","sourceRoot":"","sources":["../../src/tools/estimate-duration.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAWxB,eAAO,MAAM,sBAAsB;IACjC,MAAM;IAGN,aAAa;IAGb,UAAU;;;;IAGV,aAAa;IAGb,QAAQ;;;;;;;CAGT,CAAC;AAkBF,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;;;;;;;;;;;;;;;;EA6CpE"}
@@ -0,0 +1,66 @@
1
+ import { z } from 'zod';
2
+ const positiveSeconds = z.number().finite().positive();
3
+ const contextSchema = z.object({
4
+ script: z.string().trim().min(1).max(5000).optional(),
5
+ audioDuration: positiveSeconds.optional(),
6
+ visualType: z.enum(['image', 'video']).optional(),
7
+ videoDuration: positiveSeconds.optional(),
8
+ syncMode: z.enum(['bounce', 'loop', 'cut_off', 'silence', 'remap']).optional(),
9
+ });
10
+ export const estimateDurationInputs = {
11
+ script: contextSchema.shape.script.describe('Replacement speech text. Estimates spoken duration without synthesizing audio. Use for script-driven lipsync instead of pricing the full source video. An approximate quote; final speech length may differ.'),
12
+ audioDuration: contextSchema.shape.audioDuration.describe('Measured replacement audio duration in seconds. Prefer this over a script estimate once speech audio exists.'),
13
+ visualType: contextSchema.shape.visualType.describe('Visual input type. Set image for image-to-video quotes; images always follow speech duration, including with cut_off or silence. Videos require videoDuration for those modes.'),
14
+ videoDuration: contextSchema.shape.videoDuration.describe('Source video duration in seconds, used with syncMode to account for cut_off or silence. Omit for an image.'),
15
+ syncMode: contextSchema.shape.syncMode.describe('The same sync_mode selected for generation. For videos, cut_off uses the shorter input and silence the longer input. Images and other modes follow replacement audio duration.'),
16
+ };
17
+ // Match Sync's @sync/common/tts estimateAudioDuration. This standalone service
18
+ // cannot import the API monorepo's workspace packages.
19
+ function scriptDuration(script) {
20
+ let words = 0;
21
+ for (const segment of new Intl.Segmenter(undefined, { granularity: 'word' }).segment(script)) {
22
+ if (!segment.isWordLike)
23
+ continue;
24
+ const cjkCharacters = segment.segment.match(/[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}]/gu)?.length ??
25
+ 0;
26
+ words += Math.max(1, cjkCharacters);
27
+ }
28
+ const characters = script.match(/[\p{L}\p{N}]/gu)?.length ?? 0;
29
+ const wordDuration = words <= 1 ? words : Math.max(1, (words / 160) * 60);
30
+ return Number(Math.max(wordDuration, characters / 16).toFixed(2));
31
+ }
32
+ export function resolveEstimateDuration(args) {
33
+ const { script, audioDuration, visualType, videoDuration, syncMode } = contextSchema.parse(args);
34
+ const { script: _script, audioDuration: _audio, visualType: _visualType, videoDuration: _video, syncMode: _mode, ...body } = args;
35
+ if (script === undefined && audioDuration === undefined) {
36
+ if (videoDuration !== undefined || syncMode !== undefined)
37
+ throw new Error('Supply script or audioDuration when using videoDuration or syncMode.');
38
+ return { args: body, durationEstimate: undefined };
39
+ }
40
+ if (args.workflow === 'translate-and-dub')
41
+ throw new Error('Translation estimates use source duration, not replacement script or audio.');
42
+ if (visualType === 'image' && videoDuration !== undefined)
43
+ throw new Error('Omit videoDuration for an image input.');
44
+ if (visualType !== 'image' &&
45
+ (syncMode === 'cut_off' || syncMode === 'silence') &&
46
+ videoDuration === undefined)
47
+ throw new Error('Supply videoDuration to estimate cut_off or silence accurately.');
48
+ const driverDuration = positiveSeconds.parse(audioDuration ?? scriptDuration(script ?? ''));
49
+ const duration = videoDuration === undefined
50
+ ? driverDuration
51
+ : syncMode === 'cut_off'
52
+ ? Math.min(driverDuration, videoDuration)
53
+ : syncMode === 'silence'
54
+ ? Math.max(driverDuration, videoDuration)
55
+ : driverDuration;
56
+ return {
57
+ args: { ...body, duration },
58
+ durationEstimate: {
59
+ durationSeconds: duration,
60
+ audioDurationSeconds: driverDuration,
61
+ source: audioDuration === undefined ? 'script' : 'audio',
62
+ approximate: audioDuration === undefined,
63
+ excludesSpeechSynthesis: true,
64
+ },
65
+ };
66
+ }
@@ -0,0 +1,14 @@
1
+ import type { HttpClient } from '../http-client.js';
2
+ import type { ParsedOperation } from '../openapi/types.js';
3
+ import type { McpToolDefinition } from './generator.js';
4
+ /**
5
+ * Agents hit friction no one sees: an error that does not say what to fix, a workflow
6
+ * that needs a workaround, a missing tool. feedback_send lets them report it to the team
7
+ * as it happens, through the API's POST /v2/feedback (captured for daily triage). The tool
8
+ * exists only when the connected API publishes that operation.
9
+ */
10
+ export declare const FEEDBACK_OPERATION_ID = "agentFeedback_submit";
11
+ export declare function feedbackOperation(operation: ParsedOperation): boolean;
12
+ export declare function createFeedbackTools(operations: ParsedOperation[], http: HttpClient, version: string): McpToolDefinition[];
13
+ export declare const FEEDBACK_INSTRUCTIONS = "When feedback_send is available and a Sync tool error does not say what to fix, a result is wrong, or you need a workaround or a missing capability, finish or stop the task as you normally would, then call feedback_send once for that problem with the tool, the exact error and what would have helped. ";
14
+ //# sourceMappingURL=feedback.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"feedback.d.ts","sourceRoot":"","sources":["../../src/tools/feedback.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAC3D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AAExD;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB,yBAAyB,CAAC;AAE5D,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,eAAe,GAAG,OAAO,CAMrE;AAsCD,wBAAgB,mBAAmB,CACjC,UAAU,EAAE,eAAe,EAAE,EAC7B,IAAI,EAAE,UAAU,EAChB,OAAO,EAAE,MAAM,GACd,iBAAiB,EAAE,CAmCrB;AAED,eAAO,MAAM,qBAAqB,kTAC+Q,CAAC"}
@@ -0,0 +1,72 @@
1
+ import { z } from 'zod';
2
+ /**
3
+ * Agents hit friction no one sees: an error that does not say what to fix, a workflow
4
+ * that needs a workaround, a missing tool. feedback_send lets them report it to the team
5
+ * as it happens, through the API's POST /v2/feedback (captured for daily triage). The tool
6
+ * exists only when the connected API publishes that operation.
7
+ */
8
+ export const FEEDBACK_OPERATION_ID = 'agentFeedback_submit';
9
+ export function feedbackOperation(operation) {
10
+ return (operation.operationId === FEEDBACK_OPERATION_ID &&
11
+ operation.method === 'post' &&
12
+ operation.path === '/v2/feedback');
13
+ }
14
+ const input = z
15
+ .object({
16
+ kind: z
17
+ .enum(['confusing_error', 'bug', 'missing_capability', 'workaround', 'docs', 'other'])
18
+ .describe('confusing_error: an error that did not say what to fix. bug: wrong result or crash. missing_capability: a tool or option you needed. workaround: you had to do extra steps. docs: a description that misled you.'),
19
+ message: z
20
+ .string()
21
+ .trim()
22
+ .min(1)
23
+ .max(4000)
24
+ .describe('What you tried, what happened, and what would have helped. Include the exact error text. No personal data or secrets.'),
25
+ tool: z.string().trim().min(1).max(128).optional().describe('Tool the feedback is about.'),
26
+ errorCode: z.string().trim().min(1).max(128).optional().describe('Error code you received.'),
27
+ generationId: z.uuid().optional().describe('Generation the feedback is about.'),
28
+ jobId: z
29
+ .string()
30
+ .trim()
31
+ .min(1)
32
+ .max(128)
33
+ .optional()
34
+ .describe('Other job the feedback is about: an import, transcription or translation id.'),
35
+ severity: z
36
+ .enum(['blocker', 'major', 'minor'])
37
+ .optional()
38
+ .describe('blocker: you could not finish. major: you finished with a workaround. minor: an annoyance.'),
39
+ })
40
+ .strict();
41
+ const receipt = z.object({ id: z.string(), received: z.literal(true) });
42
+ export function createFeedbackTools(operations, http, version) {
43
+ if (!operations.some(feedbackOperation))
44
+ return [];
45
+ return [
46
+ {
47
+ name: 'feedback_send',
48
+ title: 'Send feedback to Sync',
49
+ description: 'Tell the Sync team about friction with these tools: an error that did not say what to fix, a result that was wrong, a capability you needed, or extra steps you had to take. Send one report per problem, after you have dealt with it; the team reviews reports daily. Does not change, retry or cancel any job, and never needs the user’s approval.',
50
+ inputSchema: input.shape,
51
+ outputSchema: receipt.shape,
52
+ annotations: {
53
+ readOnlyHint: false,
54
+ destructiveHint: false,
55
+ idempotentHint: false,
56
+ openWorldHint: false,
57
+ },
58
+ handler: async (args, context) => receipt.parse(await http.request('post', '/v2/feedback', {
59
+ body: {
60
+ ...input.parse(args),
61
+ source: 'mcp',
62
+ mcpVersion: version,
63
+ client: {
64
+ name: (context?.clientName ?? context?.clientProfile?.name ?? 'unknown').slice(0, 128),
65
+ },
66
+ },
67
+ signal: context?.signal,
68
+ })),
69
+ },
70
+ ];
71
+ }
72
+ export const FEEDBACK_INSTRUCTIONS = 'When feedback_send is available and a Sync tool error does not say what to fix, a result is wrong, or you need a workaround or a missing capability, finish or stop the task as you normally would, then call feedback_send once for that problem with the tool, the exact error and what would have helped. ';
@@ -0,0 +1,25 @@
1
+ import type { ApiRequestError, HttpClient } from '../http-client.js';
2
+ import type { ParsedOperation } from '../openapi/types.js';
3
+ import type { McpToolDefinition, ToolRequestContext } from './generator.js';
4
+ /**
5
+ * Agent-facing fixes for tools generated straight from the public OpenAPI spec. Each one
6
+ * keeps accepting the generated input, so callers holding the old tool list still work.
7
+ */
8
+ export declare const projectTargetTools: WeakSet<McpToolDefinition>;
9
+ type JsonTool = Exclude<McpToolDefinition, {
10
+ resultFormat: 'mcp';
11
+ }>;
12
+ /** One import attempt: the saved asset, or the 409 that says it is still downloading. */
13
+ type ImportAttempt = {
14
+ asset: unknown;
15
+ } | {
16
+ pending: ApiRequestError;
17
+ };
18
+ export declare function attemptImport(handler: JsonTool['handler'], args: Record<string, unknown>, context: ToolRequestContext | undefined, signal?: AbortSignal | undefined): Promise<ImportAttempt>;
19
+ export declare const importWaitOptions: {
20
+ describe: (attempt: ImportAttempt) => string;
21
+ delayMs: (attempt: ImportAttempt) => number | undefined;
22
+ };
23
+ export declare function adaptGeneratedTool(tool: McpToolDefinition, operation: ParsedOperation | undefined, http: HttpClient): McpToolDefinition;
24
+ export {};
25
+ //# sourceMappingURL=generated-adapters.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"generated-adapters.d.ts","sourceRoot":"","sources":["../../src/tools/generated-adapters.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AACrE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAC3D,OAAO,KAAK,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AAI5E;;;GAGG;AAIH,eAAO,MAAM,kBAAkB,4BAAmC,CAAC;AAKnE,KAAK,QAAQ,GAAG,OAAO,CAAC,iBAAiB,EAAE;IAAE,YAAY,EAAE,KAAK,CAAA;CAAE,CAAC,CAAC;AAwFpE,yFAAyF;AACzF,KAAK,aAAa,GAAG;IAAE,KAAK,EAAE,OAAO,CAAA;CAAE,GAAG;IAAE,OAAO,EAAE,eAAe,CAAA;CAAE,CAAC;AAEvE,wBAAsB,aAAa,CACjC,OAAO,EAAE,QAAQ,CAAC,SAAS,CAAC,EAC5B,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,OAAO,EAAE,kBAAkB,GAAG,SAAS,EACvC,MAAM,0BAAkB,GACvB,OAAO,CAAC,aAAa,CAAC,CAOxB;AAED,eAAO,MAAM,iBAAiB;IAC5B,QAAQ,YAAY,aAAa;IAIjC,OAAO,YAAY,aAAa;CAEjC,CAAC;AAuBF,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,iBAAiB,EACvB,SAAS,EAAE,eAAe,GAAG,SAAS,EACtC,IAAI,EAAE,UAAU,GACf,iBAAiB,CAWnB"}