transloadit 4.7.4 → 4.7.6

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 (142) hide show
  1. package/README.md +888 -5
  2. package/dist/Transloadit.d.ts +3 -3
  3. package/dist/Transloadit.d.ts.map +1 -1
  4. package/dist/Transloadit.js +2 -2
  5. package/dist/Transloadit.js.map +1 -1
  6. package/dist/alphalib/types/assembliesGet.d.ts +5 -0
  7. package/dist/alphalib/types/assembliesGet.d.ts.map +1 -1
  8. package/dist/alphalib/types/assemblyReplay.d.ts +5 -0
  9. package/dist/alphalib/types/assemblyReplay.d.ts.map +1 -1
  10. package/dist/alphalib/types/assemblyReplayNotification.d.ts +5 -0
  11. package/dist/alphalib/types/assemblyReplayNotification.d.ts.map +1 -1
  12. package/dist/alphalib/types/assemblyStatus.d.ts +25 -25
  13. package/dist/alphalib/types/assemblyStatus.d.ts.map +1 -1
  14. package/dist/alphalib/types/assemblyStatus.js +4 -1
  15. package/dist/alphalib/types/assemblyStatus.js.map +1 -1
  16. package/dist/alphalib/types/bill.d.ts +5 -0
  17. package/dist/alphalib/types/bill.d.ts.map +1 -1
  18. package/dist/alphalib/types/builtinTemplates.d.ts +83 -0
  19. package/dist/alphalib/types/builtinTemplates.d.ts.map +1 -0
  20. package/dist/alphalib/types/builtinTemplates.js +19 -0
  21. package/dist/alphalib/types/builtinTemplates.js.map +1 -0
  22. package/dist/alphalib/types/robots/ai-chat.d.ts.map +1 -1
  23. package/dist/alphalib/types/robots/ai-chat.js +1 -0
  24. package/dist/alphalib/types/robots/ai-chat.js.map +1 -1
  25. package/dist/alphalib/types/skillFrontmatter.d.ts +29 -0
  26. package/dist/alphalib/types/skillFrontmatter.d.ts.map +1 -0
  27. package/dist/alphalib/types/skillFrontmatter.js +19 -0
  28. package/dist/alphalib/types/skillFrontmatter.js.map +1 -0
  29. package/dist/alphalib/types/template.d.ts +36 -0
  30. package/dist/alphalib/types/template.d.ts.map +1 -1
  31. package/dist/alphalib/types/template.js +10 -0
  32. package/dist/alphalib/types/template.js.map +1 -1
  33. package/dist/alphalib/types/templateCredential.d.ts +10 -0
  34. package/dist/alphalib/types/templateCredential.d.ts.map +1 -1
  35. package/dist/cli/commands/assemblies.d.ts +8 -2
  36. package/dist/cli/commands/assemblies.d.ts.map +1 -1
  37. package/dist/cli/commands/assemblies.js +566 -411
  38. package/dist/cli/commands/assemblies.js.map +1 -1
  39. package/dist/cli/commands/index.d.ts.map +1 -1
  40. package/dist/cli/commands/index.js +5 -0
  41. package/dist/cli/commands/index.js.map +1 -1
  42. package/dist/cli/commands/templates.d.ts.map +1 -1
  43. package/dist/cli/commands/templates.js +4 -14
  44. package/dist/cli/commands/templates.js.map +1 -1
  45. package/dist/cli/fileProcessingOptions.d.ts +35 -0
  46. package/dist/cli/fileProcessingOptions.d.ts.map +1 -0
  47. package/dist/cli/fileProcessingOptions.js +182 -0
  48. package/dist/cli/fileProcessingOptions.js.map +1 -0
  49. package/dist/cli/generateIntentDocs.d.ts +2 -0
  50. package/dist/cli/generateIntentDocs.d.ts.map +1 -0
  51. package/dist/cli/generateIntentDocs.js +321 -0
  52. package/dist/cli/generateIntentDocs.js.map +1 -0
  53. package/dist/cli/intentCommandSpecs.d.ts +36 -0
  54. package/dist/cli/intentCommandSpecs.d.ts.map +1 -0
  55. package/dist/cli/intentCommandSpecs.js +181 -0
  56. package/dist/cli/intentCommandSpecs.js.map +1 -0
  57. package/dist/cli/intentCommands.d.ts +13 -0
  58. package/dist/cli/intentCommands.d.ts.map +1 -0
  59. package/dist/cli/intentCommands.js +368 -0
  60. package/dist/cli/intentCommands.js.map +1 -0
  61. package/dist/cli/intentFields.d.ts +25 -0
  62. package/dist/cli/intentFields.d.ts.map +1 -0
  63. package/dist/cli/intentFields.js +298 -0
  64. package/dist/cli/intentFields.js.map +1 -0
  65. package/dist/cli/intentInputPolicy.d.ts +10 -0
  66. package/dist/cli/intentInputPolicy.d.ts.map +1 -0
  67. package/dist/cli/intentInputPolicy.js +2 -0
  68. package/dist/cli/intentInputPolicy.js.map +1 -0
  69. package/dist/cli/intentRuntime.d.ts +114 -0
  70. package/dist/cli/intentRuntime.d.ts.map +1 -0
  71. package/dist/cli/intentRuntime.js +464 -0
  72. package/dist/cli/intentRuntime.js.map +1 -0
  73. package/dist/cli/resultFiles.d.ts +19 -0
  74. package/dist/cli/resultFiles.d.ts.map +1 -0
  75. package/dist/cli/resultFiles.js +66 -0
  76. package/dist/cli/resultFiles.js.map +1 -0
  77. package/dist/cli/resultUrls.d.ts +19 -0
  78. package/dist/cli/resultUrls.d.ts.map +1 -0
  79. package/dist/cli/resultUrls.js +36 -0
  80. package/dist/cli/resultUrls.js.map +1 -0
  81. package/dist/cli/semanticIntents/imageDescribe.d.ts +43 -0
  82. package/dist/cli/semanticIntents/imageDescribe.d.ts.map +1 -0
  83. package/dist/cli/semanticIntents/imageDescribe.js +188 -0
  84. package/dist/cli/semanticIntents/imageDescribe.js.map +1 -0
  85. package/dist/cli/semanticIntents/index.d.ts +18 -0
  86. package/dist/cli/semanticIntents/index.d.ts.map +1 -0
  87. package/dist/cli/semanticIntents/index.js +18 -0
  88. package/dist/cli/semanticIntents/index.js.map +1 -0
  89. package/dist/cli/semanticIntents/markdownPdf.d.ts +4 -0
  90. package/dist/cli/semanticIntents/markdownPdf.d.ts.map +1 -0
  91. package/dist/cli/semanticIntents/markdownPdf.js +93 -0
  92. package/dist/cli/semanticIntents/markdownPdf.js.map +1 -0
  93. package/dist/cli/semanticIntents/parsing.d.ts +11 -0
  94. package/dist/cli/semanticIntents/parsing.d.ts.map +1 -0
  95. package/dist/cli/semanticIntents/parsing.js +29 -0
  96. package/dist/cli/semanticIntents/parsing.js.map +1 -0
  97. package/dist/cli/stepsInput.d.ts +4 -0
  98. package/dist/cli/stepsInput.d.ts.map +1 -0
  99. package/dist/cli/stepsInput.js +23 -0
  100. package/dist/cli/stepsInput.js.map +1 -0
  101. package/dist/cli.d.ts +1 -1
  102. package/dist/cli.d.ts.map +1 -1
  103. package/dist/cli.js +5 -4
  104. package/dist/cli.js.map +1 -1
  105. package/dist/ensureUniqueCounter.d.ts +8 -0
  106. package/dist/ensureUniqueCounter.d.ts.map +1 -0
  107. package/dist/ensureUniqueCounter.js +48 -0
  108. package/dist/ensureUniqueCounter.js.map +1 -0
  109. package/dist/inputFiles.d.ts +9 -0
  110. package/dist/inputFiles.d.ts.map +1 -1
  111. package/dist/inputFiles.js +177 -26
  112. package/dist/inputFiles.js.map +1 -1
  113. package/dist/robots.js +1 -1
  114. package/dist/robots.js.map +1 -1
  115. package/package.json +9 -7
  116. package/src/Transloadit.ts +3 -3
  117. package/src/alphalib/types/assemblyStatus.ts +4 -1
  118. package/src/alphalib/types/builtinTemplates.ts +24 -0
  119. package/src/alphalib/types/robots/ai-chat.ts +1 -0
  120. package/src/alphalib/types/skillFrontmatter.ts +24 -0
  121. package/src/alphalib/types/template.ts +14 -0
  122. package/src/cli/commands/assemblies.ts +825 -505
  123. package/src/cli/commands/index.ts +6 -3
  124. package/src/cli/commands/templates.ts +6 -17
  125. package/src/cli/fileProcessingOptions.ts +294 -0
  126. package/src/cli/generateIntentDocs.ts +419 -0
  127. package/src/cli/intentCommandSpecs.ts +282 -0
  128. package/src/cli/intentCommands.ts +525 -0
  129. package/src/cli/intentFields.ts +403 -0
  130. package/src/cli/intentInputPolicy.ts +11 -0
  131. package/src/cli/intentRuntime.ts +734 -0
  132. package/src/cli/resultFiles.ts +105 -0
  133. package/src/cli/resultUrls.ts +72 -0
  134. package/src/cli/semanticIntents/imageDescribe.ts +254 -0
  135. package/src/cli/semanticIntents/index.ts +48 -0
  136. package/src/cli/semanticIntents/markdownPdf.ts +120 -0
  137. package/src/cli/semanticIntents/parsing.ts +56 -0
  138. package/src/cli/stepsInput.ts +32 -0
  139. package/src/cli.ts +5 -4
  140. package/src/ensureUniqueCounter.ts +75 -0
  141. package/src/inputFiles.ts +277 -26
  142. package/src/robots.ts +1 -1
@@ -0,0 +1,105 @@
1
+ export interface AssemblyResultEntryLike {
2
+ basename?: unknown
3
+ ext?: unknown
4
+ name?: unknown
5
+ ssl_url?: unknown
6
+ url?: unknown
7
+ }
8
+
9
+ export interface NormalizedAssemblyResultFile {
10
+ file: AssemblyResultEntryLike
11
+ name: string
12
+ stepName: string
13
+ url: string
14
+ }
15
+
16
+ export interface NormalizedAssemblyResults {
17
+ allFiles: NormalizedAssemblyResultFile[]
18
+ entries: Array<[string, Array<AssemblyResultEntryLike>]>
19
+ }
20
+
21
+ function isAssemblyResultEntryLike(value: unknown): value is AssemblyResultEntryLike {
22
+ return value != null && typeof value === 'object'
23
+ }
24
+
25
+ function normalizeAssemblyResultName(
26
+ stepName: string,
27
+ file: AssemblyResultEntryLike,
28
+ ): string | null {
29
+ if (typeof file.name === 'string') {
30
+ return file.name
31
+ }
32
+
33
+ if (typeof file.basename === 'string') {
34
+ if (typeof file.ext === 'string' && file.ext.length > 0) {
35
+ return `${file.basename}.${file.ext}`
36
+ }
37
+
38
+ return file.basename
39
+ }
40
+
41
+ return `${stepName}_result`
42
+ }
43
+
44
+ function normalizeAssemblyResultUrl(file: AssemblyResultEntryLike): string | null {
45
+ if (typeof file.ssl_url === 'string') {
46
+ return file.ssl_url
47
+ }
48
+
49
+ if (typeof file.url === 'string') {
50
+ return file.url
51
+ }
52
+
53
+ return null
54
+ }
55
+
56
+ function normalizeAssemblyResultFile(
57
+ stepName: string,
58
+ value: unknown,
59
+ ): NormalizedAssemblyResultFile | null {
60
+ if (!isAssemblyResultEntryLike(value)) {
61
+ return null
62
+ }
63
+
64
+ const url = normalizeAssemblyResultUrl(value)
65
+ const name = normalizeAssemblyResultName(stepName, value)
66
+ if (url == null || name == null) {
67
+ return null
68
+ }
69
+
70
+ return {
71
+ file: value,
72
+ name,
73
+ stepName,
74
+ url,
75
+ }
76
+ }
77
+
78
+ export function normalizeAssemblyResults(results: unknown): NormalizedAssemblyResults {
79
+ if (results == null || typeof results !== 'object' || Array.isArray(results)) {
80
+ return {
81
+ allFiles: [],
82
+ entries: [],
83
+ }
84
+ }
85
+
86
+ const files: NormalizedAssemblyResultFile[] = []
87
+ const entries = Object.entries(results)
88
+ for (const [stepName, stepResults] of entries) {
89
+ if (!Array.isArray(stepResults)) {
90
+ continue
91
+ }
92
+
93
+ for (const stepResult of stepResults) {
94
+ const normalized = normalizeAssemblyResultFile(stepName, stepResult)
95
+ if (normalized != null) {
96
+ files.push(normalized)
97
+ }
98
+ }
99
+ }
100
+
101
+ return {
102
+ allFiles: files,
103
+ entries,
104
+ }
105
+ }
@@ -0,0 +1,72 @@
1
+ import type { IOutputCtl } from './OutputCtl.ts'
2
+ import type { NormalizedAssemblyResults } from './resultFiles.ts'
3
+ import { normalizeAssemblyResults } from './resultFiles.ts'
4
+
5
+ export interface ResultUrlRow {
6
+ assemblyId: string
7
+ name: string
8
+ step: string
9
+ url: string
10
+ }
11
+
12
+ export function collectResultUrlRows({
13
+ assemblyId,
14
+ results,
15
+ }: {
16
+ assemblyId: string
17
+ results: unknown
18
+ }): ResultUrlRow[] {
19
+ return collectNormalizedResultUrlRows({
20
+ assemblyId,
21
+ normalizedResults: normalizeAssemblyResults(results),
22
+ })
23
+ }
24
+
25
+ export function collectNormalizedResultUrlRows({
26
+ assemblyId,
27
+ normalizedResults,
28
+ }: {
29
+ assemblyId: string
30
+ normalizedResults: NormalizedAssemblyResults
31
+ }): ResultUrlRow[] {
32
+ return normalizedResults.allFiles.map((file) => ({
33
+ assemblyId,
34
+ step: file.stepName,
35
+ name: file.name,
36
+ url: file.url,
37
+ }))
38
+ }
39
+
40
+ export function formatResultUrlRows(rows: readonly ResultUrlRow[]): string {
41
+ if (rows.length === 0) {
42
+ return ''
43
+ }
44
+
45
+ const includeAssembly = new Set(rows.map((row) => row.assemblyId)).size > 1
46
+ const headers = includeAssembly ? ['ASSEMBLY', 'STEP', 'NAME', 'URL'] : ['STEP', 'NAME', 'URL']
47
+ const tableRows = rows.map((row) =>
48
+ includeAssembly ? [row.assemblyId, row.step, row.name, row.url] : [row.step, row.name, row.url],
49
+ )
50
+
51
+ const widths = headers.map((header, index) =>
52
+ Math.max(header.length, ...tableRows.map((row) => row[index]?.length ?? 0)),
53
+ )
54
+
55
+ return [headers, ...tableRows]
56
+ .map((row) =>
57
+ row
58
+ .map((value, index) =>
59
+ index === row.length - 1 ? value : value.padEnd(widths[index] ?? value.length),
60
+ )
61
+ .join(' '),
62
+ )
63
+ .join('\n')
64
+ }
65
+
66
+ export function printResultUrls(output: IOutputCtl, rows: readonly ResultUrlRow[]): void {
67
+ if (rows.length === 0) {
68
+ return
69
+ }
70
+
71
+ output.print(formatResultUrlRows(rows), { urls: rows })
72
+ }
@@ -0,0 +1,254 @@
1
+ import { parseStringArrayValue } from '../intentFields.ts'
2
+ import type {
3
+ IntentDynamicStepExecutionDefinition,
4
+ IntentOptionDefinition,
5
+ } from '../intentRuntime.ts'
6
+ import type { SemanticIntentDescriptor, SemanticIntentPresentation } from './index.ts'
7
+ import { parseOptionalEnumValue, parseUniqueEnumArray } from './parsing.ts'
8
+
9
+ const imageDescribeFields = ['labels', 'altText', 'title', 'caption', 'description'] as const
10
+
11
+ type ImageDescribeField = (typeof imageDescribeFields)[number]
12
+
13
+ const wordpressDescribeFields = [
14
+ 'altText',
15
+ 'title',
16
+ 'caption',
17
+ 'description',
18
+ ] as const satisfies readonly ImageDescribeField[]
19
+
20
+ const defaultDescribeModel = 'anthropic/claude-4-sonnet-20250514'
21
+ const describeFieldDescriptions = {
22
+ altText: 'A concise accessibility-focused alt text that objectively describes the image',
23
+ title: 'A concise publishable title for the image',
24
+ caption: 'A short caption suitable for displaying below the image',
25
+ description: 'A richer description of the image suitable for CMS usage',
26
+ } as const satisfies Record<Exclude<ImageDescribeField, 'labels'>, string>
27
+
28
+ const imageDescribeExecutionDefinition = {
29
+ kind: 'dynamic-step',
30
+ handler: 'image-describe',
31
+ resultStepName: 'describe',
32
+ fields: [
33
+ {
34
+ name: 'fields',
35
+ kind: 'string-array',
36
+ propertyName: 'fields',
37
+ optionFlags: '--fields',
38
+ description:
39
+ 'Describe output fields to generate, for example labels or altText,title,caption,description',
40
+ required: false,
41
+ },
42
+ {
43
+ name: 'forProfile',
44
+ kind: 'string',
45
+ propertyName: 'forProfile',
46
+ optionFlags: '--for',
47
+ description: 'Use a named output profile, currently: wordpress',
48
+ required: false,
49
+ },
50
+ {
51
+ name: 'model',
52
+ kind: 'string',
53
+ propertyName: 'model',
54
+ optionFlags: '--model',
55
+ description:
56
+ 'Model to use for generated text fields (default: anthropic/claude-4-sonnet-20250514)',
57
+ required: false,
58
+ },
59
+ ] as const satisfies readonly IntentOptionDefinition[],
60
+ } satisfies IntentDynamicStepExecutionDefinition
61
+
62
+ const imageDescribeCommandPresentation = {
63
+ description: 'Describe images as labels or publishable text fields',
64
+ details:
65
+ 'Generates image labels through `/image/describe`, or structured altText/title/caption/description through `/ai/chat`, then writes the JSON result to `--out`.',
66
+ examples: [
67
+ [
68
+ 'Describe an image as labels',
69
+ 'transloadit image describe --input hero.jpg --out labels.json',
70
+ ],
71
+ [
72
+ 'Generate WordPress-ready fields',
73
+ 'transloadit image describe --input hero.jpg --for wordpress --out fields.json',
74
+ ],
75
+ [
76
+ 'Request a custom field set',
77
+ 'transloadit image describe --input hero.jpg --fields altText,title,caption --out fields.json',
78
+ ],
79
+ ] as Array<[string, string]>,
80
+ } as const satisfies SemanticIntentPresentation
81
+
82
+ function parseDescribeFields(value: string[] | undefined): ImageDescribeField[] {
83
+ const rawFields = parseStringArrayValue(value ?? [])
84
+ return parseUniqueEnumArray({
85
+ flagName: '--fields',
86
+ supportedValues: imageDescribeFields,
87
+ values: rawFields,
88
+ })
89
+ }
90
+
91
+ function resolveDescribeProfile(profile: string | undefined): 'wordpress' | null {
92
+ return parseOptionalEnumValue({
93
+ flagName: '--for',
94
+ supportedValues: ['wordpress'] as const,
95
+ value: profile,
96
+ })
97
+ }
98
+
99
+ function resolveRequestedDescribeFields({
100
+ explicitFields,
101
+ profile,
102
+ }: {
103
+ explicitFields: ImageDescribeField[]
104
+ profile: 'wordpress' | null
105
+ }): ImageDescribeField[] {
106
+ if (explicitFields.length > 0) {
107
+ return explicitFields
108
+ }
109
+
110
+ if (profile === 'wordpress') {
111
+ return [...wordpressDescribeFields]
112
+ }
113
+
114
+ return ['labels']
115
+ }
116
+
117
+ function validateDescribeFields({
118
+ fields,
119
+ model,
120
+ profile,
121
+ }: {
122
+ fields: ImageDescribeField[]
123
+ model: string
124
+ profile: 'wordpress' | null
125
+ }): void {
126
+ const includesLabels = fields.includes('labels')
127
+
128
+ if (includesLabels && fields.length > 1) {
129
+ throw new Error(
130
+ 'The labels field cannot be combined with altText, title, caption, or description',
131
+ )
132
+ }
133
+
134
+ if (includesLabels && profile != null) {
135
+ throw new Error('--for cannot be combined with --fields labels')
136
+ }
137
+
138
+ if (includesLabels && model !== defaultDescribeModel) {
139
+ throw new Error(
140
+ '--model is only supported when generating altText, title, caption, or description',
141
+ )
142
+ }
143
+ }
144
+
145
+ function resolveImageDescribeRequest(rawValues: Record<string, unknown>): {
146
+ fields: ImageDescribeField[]
147
+ profile: 'wordpress' | null
148
+ } {
149
+ const explicitFields = parseDescribeFields(rawValues.fields as string[] | undefined)
150
+ const profile = resolveDescribeProfile(rawValues.forProfile as string | undefined)
151
+ const fields = resolveRequestedDescribeFields({ explicitFields, profile })
152
+ validateDescribeFields({
153
+ fields,
154
+ model: String(rawValues.model ?? defaultDescribeModel),
155
+ profile,
156
+ })
157
+
158
+ return { fields, profile }
159
+ }
160
+
161
+ function buildDescribeAiChatSchema(fields: readonly ImageDescribeField[]): Record<string, unknown> {
162
+ const properties = Object.fromEntries(
163
+ fields.map((field) => {
164
+ return [
165
+ field,
166
+ {
167
+ type: 'string',
168
+ description: describeFieldDescriptions[field as Exclude<ImageDescribeField, 'labels'>],
169
+ },
170
+ ]
171
+ }),
172
+ )
173
+
174
+ return {
175
+ type: 'object',
176
+ additionalProperties: false,
177
+ required: [...fields],
178
+ properties,
179
+ }
180
+ }
181
+
182
+ function buildDescribeAiChatMessages({
183
+ fields,
184
+ profile,
185
+ }: {
186
+ fields: readonly ImageDescribeField[]
187
+ profile: 'wordpress' | null
188
+ }): {
189
+ messages: string
190
+ systemMessage: string
191
+ } {
192
+ const requestedFields = fields.join(', ')
193
+ const profileHint =
194
+ profile === 'wordpress'
195
+ ? 'The output is for the WordPress media library.'
196
+ : 'The output is for a publishing workflow.'
197
+
198
+ return {
199
+ systemMessage: [
200
+ 'You generate accurate image copy for publishing workflows.',
201
+ profileHint,
202
+ 'Return only the schema fields requested.',
203
+ 'Be concrete, concise, and faithful to what is visibly present in the image.',
204
+ 'Do not invent facts, brands, locations, or identities that are not clearly visible.',
205
+ 'Avoid keyword stuffing, hype, and mentions of SEO or accessibility in the output itself.',
206
+ 'For altText, write one objective sentence focused on what matters to someone who cannot see the image.',
207
+ 'For title, keep it short and natural.',
208
+ 'For caption, write one short sentence suitable for publication.',
209
+ 'For description, write one or two sentences with slightly more context than the caption.',
210
+ ].join(' '),
211
+ messages: `Analyze the attached image and fill these fields: ${requestedFields}.`,
212
+ }
213
+ }
214
+
215
+ function createImageDescribeStep(rawValues: Record<string, unknown>): Record<string, unknown> {
216
+ const { fields, profile } = resolveImageDescribeRequest(rawValues)
217
+ if (fields.length === 1 && fields[0] === 'labels') {
218
+ return {
219
+ robot: '/image/describe',
220
+ use: ':original',
221
+ result: true,
222
+ provider: 'aws',
223
+ format: 'json',
224
+ granularity: 'list',
225
+ explicit_descriptions: false,
226
+ }
227
+ }
228
+
229
+ const { messages, systemMessage } = buildDescribeAiChatMessages({ fields, profile })
230
+
231
+ return {
232
+ robot: '/ai/chat',
233
+ use: ':original',
234
+ result: true,
235
+ model: String(rawValues.model ?? defaultDescribeModel),
236
+ format: 'json',
237
+ return_messages: 'last',
238
+ test_credentials: true,
239
+ schema: JSON.stringify(buildDescribeAiChatSchema(fields)),
240
+ messages,
241
+ system_message: systemMessage,
242
+ // @TODO Move these inline /ai/chat instructions into a builtin template in api2 and
243
+ // switch this command to call that builtin instead of shipping prompt logic in the CLI.
244
+ }
245
+ }
246
+
247
+ export const imageDescribeSemanticIntentDescriptor = {
248
+ createStep: createImageDescribeStep,
249
+ execution: imageDescribeExecutionDefinition,
250
+ inputPolicy: { kind: 'required' },
251
+ outputDescription: 'Write the JSON result to this path or directory',
252
+ presentation: imageDescribeCommandPresentation,
253
+ runnerKind: 'watchable',
254
+ } as const satisfies SemanticIntentDescriptor
@@ -0,0 +1,48 @@
1
+ import type { IntentInputPolicy } from '../intentInputPolicy.ts'
2
+ import type {
3
+ IntentDynamicStepExecutionDefinition,
4
+ IntentRunnerKind,
5
+ PreparedIntentInputs,
6
+ } from '../intentRuntime.ts'
7
+ import { imageDescribeSemanticIntentDescriptor } from './imageDescribe.ts'
8
+ import {
9
+ markdownDocxSemanticIntentDescriptor,
10
+ markdownPdfSemanticIntentDescriptor,
11
+ } from './markdownPdf.ts'
12
+
13
+ export interface SemanticIntentPresentation {
14
+ description: string
15
+ details: string
16
+ examples: Array<[string, string]>
17
+ }
18
+
19
+ export interface SemanticIntentDescriptor {
20
+ createStep: (rawValues: Record<string, unknown>) => Record<string, unknown>
21
+ execution: IntentDynamicStepExecutionDefinition
22
+ inputPolicy: IntentInputPolicy
23
+ outputDescription: string
24
+ prepareInputs?: (
25
+ preparedInputs: PreparedIntentInputs,
26
+ rawValues: Record<string, unknown>,
27
+ ) => Promise<PreparedIntentInputs>
28
+ presentation: SemanticIntentPresentation
29
+ runnerKind: IntentRunnerKind
30
+ }
31
+
32
+ const semanticIntentDescriptors: Record<string, SemanticIntentDescriptor> = {
33
+ 'image-describe': imageDescribeSemanticIntentDescriptor,
34
+ 'markdown-pdf': {
35
+ ...markdownPdfSemanticIntentDescriptor,
36
+ },
37
+ 'markdown-docx': {
38
+ ...markdownDocxSemanticIntentDescriptor,
39
+ },
40
+ }
41
+
42
+ export function getSemanticIntentDescriptor(name: string): SemanticIntentDescriptor {
43
+ if (!(name in semanticIntentDescriptors)) {
44
+ throw new Error(`Semantic intent descriptor does not exist for "${name}"`)
45
+ }
46
+
47
+ return semanticIntentDescriptors[name]
48
+ }
@@ -0,0 +1,120 @@
1
+ import type { IntentOptionDefinition } from '../intentRuntime.ts'
2
+ import type { SemanticIntentDescriptor, SemanticIntentPresentation } from './index.ts'
3
+ import { parseOptionalEnumValue } from './parsing.ts'
4
+
5
+ const defaultMarkdownFormat = 'gfm'
6
+ const defaultMarkdownTheme = 'github'
7
+ const markdownFormats = ['commonmark', 'gfm'] as const
8
+ const markdownThemes = ['bare', 'github'] as const
9
+
10
+ function resolveMarkdownFormat(value: unknown): 'commonmark' | 'gfm' {
11
+ return (
12
+ parseOptionalEnumValue({
13
+ flagName: '--markdown-format',
14
+ supportedValues: markdownFormats,
15
+ value,
16
+ }) ?? defaultMarkdownFormat
17
+ )
18
+ }
19
+
20
+ function resolveMarkdownTheme(value: unknown): 'bare' | 'github' {
21
+ return (
22
+ parseOptionalEnumValue({
23
+ flagName: '--markdown-theme',
24
+ supportedValues: markdownThemes,
25
+ value,
26
+ }) ?? defaultMarkdownTheme
27
+ )
28
+ }
29
+
30
+ const markdownOptionDefinitions = [
31
+ {
32
+ name: 'markdownFormat',
33
+ kind: 'string',
34
+ propertyName: 'markdownFormat',
35
+ optionFlags: '--markdown-format',
36
+ description: 'Markdown variant to parse, either commonmark or gfm',
37
+ required: false,
38
+ },
39
+ {
40
+ name: 'markdownTheme',
41
+ kind: 'string',
42
+ propertyName: 'markdownTheme',
43
+ optionFlags: '--markdown-theme',
44
+ description: 'Markdown theme to render, either github or bare',
45
+ required: false,
46
+ },
47
+ ] as const satisfies readonly IntentOptionDefinition[]
48
+
49
+ function createMarkdownConvertSemanticIntent({
50
+ description,
51
+ details,
52
+ exampleOutput,
53
+ format,
54
+ handler,
55
+ }: {
56
+ description: string
57
+ details: string
58
+ exampleOutput: string
59
+ format: 'docx' | 'pdf'
60
+ handler: 'markdown-docx' | 'markdown-pdf'
61
+ }): SemanticIntentDescriptor {
62
+ const formatLabel = format.toUpperCase()
63
+ const presentation = {
64
+ description,
65
+ details,
66
+ examples: [
67
+ [
68
+ `Render a Markdown file as a ${formatLabel} file`,
69
+ `transloadit markdown ${format} --input README.md --out ${exampleOutput}`,
70
+ ],
71
+ [
72
+ 'Print a temporary result URL without downloading locally',
73
+ `transloadit markdown ${format} --input README.md --print-urls`,
74
+ ],
75
+ ],
76
+ } satisfies SemanticIntentPresentation
77
+
78
+ return {
79
+ createStep(rawValues) {
80
+ return {
81
+ robot: '/document/convert',
82
+ use: ':original',
83
+ result: true,
84
+ format,
85
+ markdown_format: resolveMarkdownFormat(rawValues.markdownFormat),
86
+ markdown_theme: resolveMarkdownTheme(rawValues.markdownTheme),
87
+ // @TODO Replace this semantic CLI alias with a builtin/api2-owned command surface if we later
88
+ // want richer Markdown conversion semantics beyond `/document/convert`.
89
+ }
90
+ },
91
+ execution: {
92
+ kind: 'dynamic-step',
93
+ handler,
94
+ resultStepName: 'convert',
95
+ fields: markdownOptionDefinitions,
96
+ },
97
+ inputPolicy: { kind: 'required' },
98
+ outputDescription: `Write the rendered ${formatLabel} to this path or directory`,
99
+ presentation,
100
+ runnerKind: 'watchable',
101
+ }
102
+ }
103
+
104
+ export const markdownPdfSemanticIntentDescriptor = createMarkdownConvertSemanticIntent({
105
+ description: 'Render Markdown files as PDFs',
106
+ details:
107
+ 'Runs `/document/convert` with `format: pdf`, letting the backend render Markdown and preserve features such as internal heading links in the generated PDF.',
108
+ exampleOutput: 'README.pdf',
109
+ format: 'pdf',
110
+ handler: 'markdown-pdf',
111
+ })
112
+
113
+ export const markdownDocxSemanticIntentDescriptor = createMarkdownConvertSemanticIntent({
114
+ description: 'Render Markdown files as DOCX documents',
115
+ details:
116
+ 'Runs `/document/convert` with `format: docx`, letting the backend render Markdown and convert it into a Word document.',
117
+ exampleOutput: 'README.docx',
118
+ format: 'docx',
119
+ handler: 'markdown-docx',
120
+ })
@@ -0,0 +1,56 @@
1
+ export function parseOptionalEnumValue<TValue extends string>({
2
+ flagName,
3
+ supportedValues,
4
+ value,
5
+ }: {
6
+ flagName: string
7
+ supportedValues: readonly TValue[]
8
+ value: unknown
9
+ }): TValue | null {
10
+ if (value == null || value === '') {
11
+ return null
12
+ }
13
+
14
+ if (typeof value === 'string' && supportedValues.includes(value as TValue)) {
15
+ return value as TValue
16
+ }
17
+
18
+ throw new Error(
19
+ `Unsupported ${flagName} value "${String(value)}". Supported values: ${supportedValues.join(', ')}`,
20
+ )
21
+ }
22
+
23
+ export function parseUniqueEnumArray<TValue extends string>({
24
+ flagName,
25
+ supportedValues,
26
+ values,
27
+ }: {
28
+ flagName: string
29
+ supportedValues: readonly TValue[]
30
+ values: readonly string[]
31
+ }): TValue[] {
32
+ if (values.length === 0) {
33
+ return []
34
+ }
35
+
36
+ const parsedValues: TValue[] = []
37
+ const seen = new Set<TValue>()
38
+
39
+ for (const value of values) {
40
+ if (!supportedValues.includes(value as TValue)) {
41
+ throw new Error(
42
+ `Unsupported ${flagName} value "${value}". Supported values: ${supportedValues.join(', ')}`,
43
+ )
44
+ }
45
+
46
+ const parsedValue = value as TValue
47
+ if (seen.has(parsedValue)) {
48
+ continue
49
+ }
50
+
51
+ seen.add(parsedValue)
52
+ parsedValues.push(parsedValue)
53
+ }
54
+
55
+ return parsedValues
56
+ }
@@ -0,0 +1,32 @@
1
+ import fsp from 'node:fs/promises'
2
+
3
+ import type { StepsInput } from '../alphalib/types/template.ts'
4
+ import { stepsSchema } from '../alphalib/types/template.ts'
5
+
6
+ export function parseStepsInputJson(content: string): StepsInput {
7
+ const parsed: unknown = JSON.parse(content)
8
+ const validated = stepsSchema.safeParse(parsed)
9
+ if (!validated.success) {
10
+ throw new Error(`Invalid steps format: ${validated.error.message}`)
11
+ }
12
+
13
+ const parsedSteps = parsed as Record<string, Record<string, unknown>>
14
+ const validatedSteps = validated.data as Record<string, Record<string, unknown>>
15
+
16
+ return Object.fromEntries(
17
+ Object.entries(parsedSteps).map(([stepName, stepInput]) => {
18
+ const normalizedStep = validatedSteps[stepName] ?? {}
19
+ return [
20
+ stepName,
21
+ Object.fromEntries(
22
+ Object.keys(stepInput).map((key) => [key, normalizedStep[key] ?? stepInput[key]]),
23
+ ),
24
+ ]
25
+ }),
26
+ ) as StepsInput
27
+ }
28
+
29
+ export async function readStepsInputFile(filePath: string): Promise<StepsInput> {
30
+ const content = await fsp.readFile(filePath, 'utf8')
31
+ return parseStepsInputJson(content)
32
+ }