@transloadit/node 4.5.1 → 4.6.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 (46) hide show
  1. package/dist/Transloadit.d.ts +1 -1
  2. package/dist/Transloadit.d.ts.map +1 -1
  3. package/dist/Transloadit.js +1 -1
  4. package/dist/Transloadit.js.map +1 -1
  5. package/dist/alphalib/mcache.d.ts.map +1 -1
  6. package/dist/alphalib/mcache.js +4 -2
  7. package/dist/alphalib/mcache.js.map +1 -1
  8. package/dist/alphalib/types/assemblyReplay.d.ts +2 -0
  9. package/dist/alphalib/types/assemblyReplay.d.ts.map +1 -1
  10. package/dist/alphalib/types/assemblyReplayNotification.d.ts +2 -0
  11. package/dist/alphalib/types/assemblyReplayNotification.d.ts.map +1 -1
  12. package/dist/alphalib/types/assemblyStatus.d.ts +20 -20
  13. package/dist/alphalib/types/assemblyStatus.d.ts.map +1 -1
  14. package/dist/alphalib/types/assemblyStatus.js +13 -1
  15. package/dist/alphalib/types/assemblyStatus.js.map +1 -1
  16. package/dist/alphalib/types/robots/_index.d.ts +20 -0
  17. package/dist/alphalib/types/robots/_index.d.ts.map +1 -1
  18. package/dist/alphalib/types/robots/ai-chat.d.ts +20 -0
  19. package/dist/alphalib/types/robots/ai-chat.d.ts.map +1 -1
  20. package/dist/alphalib/types/robots/ai-chat.js +5 -3
  21. package/dist/alphalib/types/robots/ai-chat.js.map +1 -1
  22. package/dist/alphalib/types/template.d.ts +36 -0
  23. package/dist/alphalib/types/template.d.ts.map +1 -1
  24. package/dist/cli/commands/assemblies.d.ts.map +1 -1
  25. package/dist/cli/commands/assemblies.js +109 -25
  26. package/dist/cli/commands/assemblies.js.map +1 -1
  27. package/dist/cli/commands/docs.d.ts +15 -0
  28. package/dist/cli/commands/docs.d.ts.map +1 -0
  29. package/dist/cli/commands/docs.js +58 -0
  30. package/dist/cli/commands/docs.js.map +1 -0
  31. package/dist/cli/commands/index.d.ts.map +1 -1
  32. package/dist/cli/commands/index.js +4 -0
  33. package/dist/cli/commands/index.js.map +1 -1
  34. package/dist/robots.d.ts +2 -1
  35. package/dist/robots.d.ts.map +1 -1
  36. package/dist/robots.js +9 -3
  37. package/dist/robots.js.map +1 -1
  38. package/package.json +2 -2
  39. package/src/Transloadit.ts +1 -1
  40. package/src/alphalib/mcache.ts +5 -3
  41. package/src/alphalib/types/assemblyStatus.ts +13 -1
  42. package/src/alphalib/types/robots/ai-chat.ts +11 -3
  43. package/src/cli/commands/assemblies.ts +131 -25
  44. package/src/cli/commands/docs.ts +68 -0
  45. package/src/cli/commands/index.ts +5 -2
  46. package/src/robots.ts +12 -4
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@transloadit/node",
3
- "version": "4.5.1",
3
+ "version": "4.6.0",
4
4
  "description": "Node.js SDK for Transloadit",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -76,7 +76,7 @@
76
76
  "fix": "npm-run-all --serial 'fix:js'",
77
77
  "lint:deps": "knip --dependencies --no-progress",
78
78
  "fix:deps": "knip --dependencies --no-progress --fix",
79
- "prepack": "rm -f tsconfig.tsbuildinfo tsconfig.build.tsbuildinfo && yarn --cwd ../.. tsc:node",
79
+ "prepack": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\" && rm -f tsconfig.tsbuildinfo tsconfig.build.tsbuildinfo && yarn --cwd ../.. tsc:node",
80
80
  "test:unit": "yarn --cwd ../.. tsc:utils && ../../node_modules/.bin/vitest run --coverage ./test/unit",
81
81
  "test:e2e": "yarn --cwd ../.. tsc:utils && ../../node_modules/.bin/vitest run ./test/e2e",
82
82
  "test": "yarn --cwd ../.. tsc:utils && ../../node_modules/.bin/vitest run --coverage"
@@ -90,7 +90,7 @@ export type {
90
90
  RobotListResult,
91
91
  RobotParamHelp,
92
92
  } from './robots.ts'
93
- export { getRobotHelp, listRobots } from './robots.ts'
93
+ export { getRobotHelp, isKnownRobot, listRobots } from './robots.ts'
94
94
 
95
95
  const log = debug('transloadit')
96
96
  const logWarn = debug('transloadit:warn')
@@ -98,9 +98,11 @@ export class Mcache<T> {
98
98
  })
99
99
 
100
100
  this.#pending.set(key, promise)
101
- void promise.finally(() => {
102
- this.#pending.delete(key)
103
- })
101
+ void promise
102
+ .finally(() => {
103
+ this.#pending.delete(key)
104
+ })
105
+ .catch(() => undefined)
104
106
  return promise
105
107
  }
106
108
 
@@ -10,7 +10,6 @@ export const assemblyStatusOkCodeSchema = z.enum([
10
10
  'ASSEMBLY_CANCELED',
11
11
  'ASSEMBLY_COMPLETED',
12
12
  'ASSEMBLY_EXECUTING',
13
- 'ASSEMBLY_EXPIRED',
14
13
  'ASSEMBLY_REPLAYING',
15
14
  'ASSEMBLY_UPLOADING',
16
15
  'REQUEST_ABORTED',
@@ -27,6 +26,7 @@ export const assemblyStatusErrCodeSchema = z.enum([
27
26
  'ASSEMBLY_CRASHED',
28
27
  'ASSEMBLY_DISALLOWED_ROBOTS_USED',
29
28
  'ASSEMBLY_EMPTY_STEPS',
29
+ 'ASSEMBLY_EXECUTION_PROGRESS_NOT_ENABLED',
30
30
  'ASSEMBLY_EXPIRED',
31
31
  'ASSEMBLY_FILE_NOT_RESERVED',
32
32
  'ASSEMBLY_INFINITE',
@@ -43,9 +43,17 @@ export const assemblyStatusErrCodeSchema = z.enum([
43
43
  'ASSEMBLY_NOT_FINISHED',
44
44
  'ASSEMBLY_NOT_FOUND',
45
45
  'ASSEMBLY_NOT_REPLAYED',
46
+ 'ASSEMBLY_NOTIFICATION_LIST_ERROR',
46
47
  'ASSEMBLY_NOTIFICATION_NOT_PERSISTED',
48
+ 'ASSEMBLY_NOTIFICATION_NOT_REPLAYED',
49
+ 'ASSEMBLY_NOTIFICATIONS_LIST_ERROR',
50
+ 'ASSEMBLY_NO_NOTIFY_URL',
47
51
  'ASSEMBLY_ROBOT_MISSING',
48
52
  'ASSEMBLY_SATURATED',
53
+ 'ASSEMBLY_STATS_ERROR',
54
+ 'ASSEMBLY_STATS_INVALID_TIME',
55
+ 'ASSEMBLY_STATS_MISSING_REGION',
56
+ 'ASSEMBLY_STATUS_FETCHING_RATE_LIMIT_REACHED',
49
57
  'ASSEMBLY_STATUS_NOT_FOUND',
50
58
  'ASSEMBLY_STATUS_PARSE_ERROR',
51
59
  'ASSEMBLY_STEP_INVALID_ROBOT',
@@ -61,6 +69,9 @@ export const assemblyStatusErrCodeSchema = z.enum([
61
69
  'AUTH_KEYS_NOT_FOUND',
62
70
  'AUTH_SECRET_NOT_RETRIEVED',
63
71
  'AZURE_STORE_ACCESS_DENIED',
72
+ 'BEARER_TOKEN_AUTH_KEY_MISMATCH',
73
+ 'BEARER_TOKEN_EXPIRED',
74
+ 'BEARER_TOKEN_INVALID',
64
75
  'BACKBLAZE_IMPORT_ACCESS_DENIED',
65
76
  'BACKBLAZE_IMPORT_NOT_FOUND',
66
77
  'BACKBLAZE_STORE_ACCESS_DENIED',
@@ -88,6 +99,7 @@ export const assemblyStatusErrCodeSchema = z.enum([
88
99
  'FILE_META_DATA_ERROR',
89
100
  'FILE_PREVIEW_VALIDATION',
90
101
  'FILE_READ_VALIDATION_ERROR',
102
+ 'FILE_SERVE_NO_RESULT',
91
103
  'FILE_VERIFY_INVALID_FILE',
92
104
  'FILE_VIRUSSCAN_DECLINED_FILE',
93
105
  'GET_ACCOUNT_DB_ERROR',
@@ -150,6 +150,7 @@ export const MODEL_CAPABILITIES: Record<string, { pdf: boolean; image: boolean }
150
150
  'anthropic/claude-4-opus-20250514': { pdf: true, image: true },
151
151
  'anthropic/claude-sonnet-4-5': { pdf: true, image: true },
152
152
  'anthropic/claude-opus-4-5': { pdf: true, image: true },
153
+ 'anthropic/claude-opus-4-6': { pdf: true, image: true },
153
154
  'openai/gpt-4.1-2025-04-14': { pdf: false, image: true },
154
155
  'openai/chatgpt-4o-latest': { pdf: false, image: true },
155
156
  'openai/o3-2025-04-16': { pdf: false, image: true },
@@ -197,21 +198,28 @@ export const robotAiChatInstructionsSchema = robotBase
197
198
  credentials: z
198
199
  .union([z.string(), z.array(z.string())])
199
200
  .optional()
200
- .describe('Names of template credentials to make available to the robot.'),
201
+ .describe(
202
+ 'Names of template credentials to make available to the robot. When using your own AI provider keys, Transloadit charges a 10% markup (minimum $0.0005 per request).',
203
+ ),
201
204
  test_credentials: z
202
205
  .boolean()
203
206
  .optional()
204
- .describe('Use Transloadit-provided credentials for testing.'),
207
+ .describe(
208
+ 'Use Transloadit-provided credentials for testing. Usage is billed at provider cost plus a 10% markup (minimum $0.0005 per request).',
209
+ ),
205
210
  mcp_servers: z
206
211
  .array(
207
212
  z.object({
208
213
  type: z.enum(['sse', 'http']),
209
214
  url: z.string(),
210
215
  headers: z.record(z.string()).optional(),
216
+ auth: z.enum(['transloadit']).optional(),
211
217
  }),
212
218
  )
213
219
  .optional()
214
- .describe('The MCP servers to use. This is used to call tools from the LLM.'),
220
+ .describe(
221
+ 'The MCP servers to use. This is used to call tools from the LLM. Use `headers` to pass `Authorization: Bearer <token>` when needed. You can use any MCP server reachable from your environment. For Transloadit\'s own MCP server, you can set `auth: "transloadit"` to let API2 auto-auth and inject an Authorization header for you (only for Transloadit-hosted MCP servers).',
222
+ ),
215
223
  })
216
224
  .strict()
217
225
 
@@ -356,7 +356,12 @@ async function myStat(
356
356
 
357
357
  function dirProvider(output: string): OutstreamProvider {
358
358
  return async (inpath, indir = process.cwd()) => {
359
- if (inpath == null || inpath === '-') {
359
+ // Inputless assemblies can still write into a directory, but output paths are derived from
360
+ // assembly results rather than an input file path (handled later).
361
+ if (inpath == null) {
362
+ return null
363
+ }
364
+ if (inpath === '-') {
360
365
  throw new Error('You must provide an input to output to a directory')
361
366
  }
362
367
 
@@ -937,13 +942,12 @@ export async function create(
937
942
  // Create fresh streams for this job
938
943
  const inStream = inPath ? fs.createReadStream(inPath) : null
939
944
  inStream?.on('error', () => {})
940
- const outStream = outPath ? (fs.createWriteStream(outPath) as OutStream) : null
941
- outStream?.on('error', () => {})
942
- if (outStream) outStream.mtime = outMtime
943
945
 
944
946
  let superceded = false
945
- if (outStream != null) {
946
- outStream.on('finish', () => {
947
+ // When writing to a file path (non-directory output), we treat finish as a supersede signal.
948
+ // Directory-output multi-download mode does not use a single shared outstream.
949
+ const markSupersededOnFinish = (stream: OutStream) => {
950
+ stream.on('finish', () => {
947
951
  superceded = true
948
952
  })
949
953
  }
@@ -982,23 +986,124 @@ export async function create(
982
986
  }
983
987
 
984
988
  if (!assembly.results) throw new Error('No results in assembly')
985
- const resultsKeys = Object.keys(assembly.results)
986
- const firstKey = resultsKeys[0]
987
- if (!firstKey) throw new Error('No results in assembly')
988
- const firstResult = assembly.results[firstKey]
989
- if (!firstResult || !firstResult[0]) throw new Error('No results in assembly')
990
- const resulturl =
991
- (firstResult[0] as { ssl_url?: string; url?: string }).ssl_url ?? firstResult[0].url
992
-
993
- if (outStream != null && resulturl && !superceded) {
994
- outputctl.debug('DOWNLOADING')
995
- const [dlErr] = await tryCatch(
996
- pipeline(got.stream(resulturl, { signal: abortController.signal }), outStream),
997
- )
998
- if (dlErr) {
999
- if (dlErr.name !== 'AbortError') {
1000
- outputctl.error(dlErr.message)
1001
- throw dlErr
989
+
990
+ const outIsDirectory = Boolean(resolvedOutput != null && outstat?.isDirectory())
991
+ const entries = Object.entries(assembly.results)
992
+ const allFiles: Array<{
993
+ stepName: string
994
+ file: { name?: string; basename?: string; ext?: string; ssl_url?: string; url?: string }
995
+ }> = []
996
+ for (const [stepName, stepResults] of entries) {
997
+ for (const file of stepResults as Array<{
998
+ name?: string
999
+ basename?: string
1000
+ ext?: string
1001
+ ssl_url?: string
1002
+ url?: string
1003
+ }>) {
1004
+ allFiles.push({ stepName, file })
1005
+ }
1006
+ }
1007
+
1008
+ const getFileUrl = (file: { ssl_url?: string; url?: string }): string | null =>
1009
+ file.ssl_url ?? file.url ?? null
1010
+
1011
+ const sanitizeName = (value: string): string => {
1012
+ const base = path.basename(value)
1013
+ return base.replaceAll('\\', '_').replaceAll('/', '_').replaceAll('\u0000', '')
1014
+ }
1015
+
1016
+ const ensureUniquePath = async (targetPath: string): Promise<string> => {
1017
+ const parsed = path.parse(targetPath)
1018
+ let candidate = targetPath
1019
+ let counter = 1
1020
+ while (true) {
1021
+ const [statErr] = await tryCatch(fsp.stat(candidate))
1022
+ if (statErr) return candidate
1023
+ candidate = path.join(parsed.dir, `${parsed.name}__${counter}${parsed.ext}`)
1024
+ counter += 1
1025
+ }
1026
+ }
1027
+
1028
+ if (resolvedOutput != null && !superceded) {
1029
+ // Directory output:
1030
+ // - For single-result, input-backed jobs, preserve existing behavior (write to mapped file path).
1031
+ // - Otherwise (multi-result or inputless), download all results into a directory structure.
1032
+ if (outIsDirectory && (inPath == null || allFiles.length !== 1 || outPath == null)) {
1033
+ let baseDir = resolvedOutput
1034
+ if (inPath != null) {
1035
+ let relpath = path.relative(process.cwd(), inPath)
1036
+ relpath = relpath.replace(/^(\.\.\/)+/, '')
1037
+ baseDir = path.join(resolvedOutput, path.dirname(relpath), path.parse(relpath).name)
1038
+ }
1039
+ await fsp.mkdir(baseDir, { recursive: true })
1040
+
1041
+ for (const { stepName, file } of allFiles) {
1042
+ const resultUrl = getFileUrl(file)
1043
+ if (!resultUrl) continue
1044
+
1045
+ const stepDir = path.join(baseDir, stepName)
1046
+ await fsp.mkdir(stepDir, { recursive: true })
1047
+
1048
+ const rawName =
1049
+ file.name ??
1050
+ (file.basename && file.ext ? `${file.basename}.${file.ext}` : undefined) ??
1051
+ `${stepName}_result`
1052
+ const safeName = sanitizeName(rawName)
1053
+ const targetPath = await ensureUniquePath(path.join(stepDir, safeName))
1054
+
1055
+ outputctl.debug('DOWNLOADING')
1056
+ const outStream = fs.createWriteStream(targetPath) as OutStream
1057
+ outStream.on('error', () => {})
1058
+ const [dlErr] = await tryCatch(
1059
+ pipeline(got.stream(resultUrl, { signal: abortController.signal }), outStream),
1060
+ )
1061
+ if (dlErr) {
1062
+ if (dlErr.name === 'AbortError') continue
1063
+ outputctl.error(dlErr.message)
1064
+ throw dlErr
1065
+ }
1066
+ }
1067
+ } else if (!outIsDirectory && outPath != null) {
1068
+ const first = allFiles[0]
1069
+ const resultUrl = first ? getFileUrl(first.file) : null
1070
+ if (resultUrl) {
1071
+ outputctl.debug('DOWNLOADING')
1072
+ const outStream = fs.createWriteStream(outPath) as OutStream
1073
+ outStream.on('error', () => {})
1074
+ outStream.mtime = outMtime
1075
+ markSupersededOnFinish(outStream)
1076
+
1077
+ const [dlErr] = await tryCatch(
1078
+ pipeline(got.stream(resultUrl, { signal: abortController.signal }), outStream),
1079
+ )
1080
+ if (dlErr) {
1081
+ if (dlErr.name !== 'AbortError') {
1082
+ outputctl.error(dlErr.message)
1083
+ throw dlErr
1084
+ }
1085
+ }
1086
+ }
1087
+ } else if (outIsDirectory && outPath != null) {
1088
+ // Single-result, input-backed job: preserve existing file mapping in outdir.
1089
+ const first = allFiles[0]
1090
+ const resultUrl = first ? getFileUrl(first.file) : null
1091
+ if (resultUrl) {
1092
+ outputctl.debug('DOWNLOADING')
1093
+ const outStream = fs.createWriteStream(outPath) as OutStream
1094
+ outStream.on('error', () => {})
1095
+ outStream.mtime = outMtime
1096
+ markSupersededOnFinish(outStream)
1097
+
1098
+ const [dlErr] = await tryCatch(
1099
+ pipeline(got.stream(resultUrl, { signal: abortController.signal }), outStream),
1100
+ )
1101
+ if (dlErr) {
1102
+ if (dlErr.name !== 'AbortError') {
1103
+ outputctl.error(dlErr.message)
1104
+ throw dlErr
1105
+ }
1106
+ }
1002
1107
  }
1003
1108
  }
1004
1109
  }
@@ -1272,8 +1377,9 @@ export class AssembliesCreateCommand extends AuthenticatedCommand {
1272
1377
  return 1
1273
1378
  }
1274
1379
 
1275
- // Default to stdin if no inputs and not a TTY
1276
- if (inputList.length === 0 && !process.stdin.isTTY) {
1380
+ // Default to stdin only for `--steps` mode (common "pipe a file into a one-off assembly" use case).
1381
+ // For `--template` mode, templates may be inputless or use /http/import, so stdin should be explicit (`--input -`).
1382
+ if (this.steps && inputList.length === 0 && !process.stdin.isTTY) {
1277
1383
  inputList.push('-')
1278
1384
  }
1279
1385
 
@@ -0,0 +1,68 @@
1
+ import { Option } from 'clipanion'
2
+ import { getRobotHelp, isKnownRobot, listRobots } from '../../robots.ts'
3
+ import { UnauthenticatedCommand } from './BaseCommand.ts'
4
+
5
+ const splitRobotArgs = (values: string[]): string[] => {
6
+ const out: string[] = []
7
+ for (const value of values) {
8
+ for (const part of value.split(',')) {
9
+ const trimmed = part.trim()
10
+ if (trimmed) out.push(trimmed)
11
+ }
12
+ }
13
+ return out
14
+ }
15
+
16
+ export class DocsRobotsListCommand extends UnauthenticatedCommand {
17
+ static override paths = [['docs', 'robots', 'list']]
18
+
19
+ search = Option.String('--search', { description: 'Filter by substring match.' })
20
+ category = Option.String('--category', { description: 'Filter by category (service slug).' })
21
+ limit = Option.String('--limit', { description: 'Max results (default: 20).' })
22
+ cursor = Option.String('--cursor', { description: 'Pagination cursor.' })
23
+
24
+ protected override run(): Promise<number | undefined> {
25
+ const limitNum = this.limit ? Number.parseInt(this.limit, 10) : undefined
26
+ const result = listRobots({
27
+ search: this.search,
28
+ category: this.category,
29
+ limit: Number.isFinite(limitNum as number) ? (limitNum as number) : undefined,
30
+ cursor: this.cursor,
31
+ })
32
+
33
+ this.output.print(result.robots.map((r) => `${r.name} ${r.summary}`).join('\n'), {
34
+ robots: result.robots,
35
+ nextCursor: result.nextCursor,
36
+ })
37
+
38
+ return Promise.resolve(0)
39
+ }
40
+ }
41
+
42
+ export class DocsRobotsGetCommand extends UnauthenticatedCommand {
43
+ static override paths = [['docs', 'robots', 'get']]
44
+
45
+ robots = Option.Rest({ required: 1 })
46
+
47
+ protected override run(): Promise<number | undefined> {
48
+ const requested = splitRobotArgs(this.robots)
49
+ const robots = []
50
+ const notFound: string[] = []
51
+
52
+ for (const robotName of requested) {
53
+ if (!isKnownRobot(robotName)) {
54
+ notFound.push(robotName)
55
+ continue
56
+ }
57
+ const help = getRobotHelp({ robotName, detailLevel: 'full' })
58
+ robots.push(help)
59
+ }
60
+
61
+ this.output.print(robots.length === 1 ? robots[0] : robots, {
62
+ robots,
63
+ notFound,
64
+ })
65
+
66
+ return Promise.resolve(notFound.length > 0 ? 1 : 0)
67
+ }
68
+ }
@@ -14,9 +14,8 @@ import {
14
14
  import { SignatureCommand, SmartCdnSignatureCommand } from './auth.ts'
15
15
 
16
16
  import { BillsGetCommand } from './bills.ts'
17
-
17
+ import { DocsRobotsGetCommand, DocsRobotsListCommand } from './docs.ts'
18
18
  import { NotificationsReplayCommand } from './notifications.ts'
19
-
20
19
  import {
21
20
  TemplatesCreateCommand,
22
21
  TemplatesDeleteCommand,
@@ -67,5 +66,9 @@ export function createCli(): Cli {
67
66
  // Uploads commands
68
67
  cli.register(UploadCommand)
69
68
 
69
+ // Documentation commands (offline metadata)
70
+ cli.register(DocsRobotsListCommand)
71
+ cli.register(DocsRobotsGetCommand)
72
+
70
73
  return cli
71
74
  }
package/src/robots.ts CHANGED
@@ -36,7 +36,7 @@ export type RobotHelp = {
36
36
 
37
37
  export type RobotHelpOptions = {
38
38
  robotName: string
39
- detailLevel?: 'summary' | 'params' | 'examples'
39
+ detailLevel?: 'summary' | 'params' | 'examples' | 'full'
40
40
  }
41
41
 
42
42
  type RobotsMetaMap = typeof robotsMeta
@@ -299,11 +299,11 @@ export const getRobotHelp = (options: RobotHelpOptions): RobotHelp => {
299
299
  const help: RobotHelp = {
300
300
  name: path,
301
301
  summary,
302
- requiredParams: detailLevel === 'params' ? params.required : [],
303
- optionalParams: detailLevel === 'params' ? params.optional : [],
302
+ requiredParams: detailLevel === 'params' || detailLevel === 'full' ? params.required : [],
303
+ optionalParams: detailLevel === 'params' || detailLevel === 'full' ? params.optional : [],
304
304
  }
305
305
 
306
- if (detailLevel === 'examples' && meta?.example_code) {
306
+ if ((detailLevel === 'examples' || detailLevel === 'full') && meta?.example_code) {
307
307
  const snippet = isRecord(meta.example_code) ? meta.example_code : {}
308
308
  help.examples = [
309
309
  {
@@ -315,3 +315,11 @@ export const getRobotHelp = (options: RobotHelpOptions): RobotHelp => {
315
315
 
316
316
  return help
317
317
  }
318
+
319
+ export const isKnownRobot = (robotName: string): boolean => {
320
+ const { byPath, byName } = getMetaIndex()
321
+ const schemaIndex = getSchemaIndex()
322
+
323
+ const path = resolveRobotPath(robotName)
324
+ return byPath.has(path) || byName.has(robotName) || schemaIndex.has(path)
325
+ }