@retell-ai/retell-cli 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +55 -0
- package/NOTICE +4 -0
- package/README.md +68 -0
- package/THIRD_PARTY_NOTICES.md +31 -0
- package/dist/src/generated/operations.js +14011 -0
- package/dist/src/index.js +59 -0
- package/dist/src/runtime/args.js +92 -0
- package/dist/src/runtime/auth.js +116 -0
- package/dist/src/runtime/credentials.js +102 -0
- package/dist/src/runtime/help.js +210 -0
- package/dist/src/runtime/http.js +32 -0
- package/dist/src/runtime/multipart.js +106 -0
- package/dist/src/runtime/naming.js +3 -0
- package/dist/src/runtime/output.js +43 -0
- package/dist/src/runtime/prompt.js +91 -0
- package/dist/src/runtime/request.js +179 -0
- package/dist/src/runtime/security.js +13 -0
- package/dist/src/runtime/validate.js +172 -0
- package/dist/src/runtime/version.js +9 -0
- package/dist/src/types.js +1 -0
- package/package.json +40 -0
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { openAsBlob } from "node:fs";
|
|
2
|
+
import { basename } from "node:path";
|
|
3
|
+
import { schemaDefinitions } from "../generated/operations.js";
|
|
4
|
+
import { fieldNameToFlag } from "./naming.js";
|
|
5
|
+
import { assertValid } from "./validate.js";
|
|
6
|
+
function isRecord(value) {
|
|
7
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
8
|
+
}
|
|
9
|
+
function parseText(raw, schema, name) {
|
|
10
|
+
if (schema.type === "object") {
|
|
11
|
+
const value = JSON.parse(raw);
|
|
12
|
+
if (!isRecord(value))
|
|
13
|
+
throw new Error(`Multipart field ${name} expects a JSON object`);
|
|
14
|
+
return value;
|
|
15
|
+
}
|
|
16
|
+
if (schema.type === "array") {
|
|
17
|
+
if (!schema.items)
|
|
18
|
+
throw new Error(`Multipart field ${name} has no item schema`);
|
|
19
|
+
if (raw.trim().startsWith("[")) {
|
|
20
|
+
const value = JSON.parse(raw);
|
|
21
|
+
if (!Array.isArray(value))
|
|
22
|
+
throw new Error(`Multipart field ${name} expects a JSON array or repeated values`);
|
|
23
|
+
return value;
|
|
24
|
+
}
|
|
25
|
+
return [parseText(raw, schema.items, name)];
|
|
26
|
+
}
|
|
27
|
+
return raw;
|
|
28
|
+
}
|
|
29
|
+
function appendText(form, definition, value) {
|
|
30
|
+
if (definition.serialization === "json") {
|
|
31
|
+
if (definition.schema.type !== "array" && definition.schema.type !== "object") {
|
|
32
|
+
throw new Error(`Multipart field ${definition.name} has incompatible JSON serialization metadata`);
|
|
33
|
+
}
|
|
34
|
+
form.append(definition.name, JSON.stringify(value));
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
if (definition.serialization === "file")
|
|
38
|
+
throw new Error(`Multipart field ${definition.name} has incompatible file serialization metadata`);
|
|
39
|
+
if (definition.schema.type === "array" || definition.schema.type === "object") {
|
|
40
|
+
throw new Error(`Multipart field ${definition.name} has incompatible text serialization metadata`);
|
|
41
|
+
}
|
|
42
|
+
if (definition.schema.format === "binary")
|
|
43
|
+
throw new Error(`Multipart field ${definition.name} has incompatible binary metadata`);
|
|
44
|
+
form.append(definition.name, typeof value === "string" ? value : JSON.stringify(value));
|
|
45
|
+
}
|
|
46
|
+
function assertCount(definition, count) {
|
|
47
|
+
if (!definition.array && count > 1)
|
|
48
|
+
throw new Error(`Multipart field ${definition.name} accepts exactly one value`);
|
|
49
|
+
if (definition.array && typeof definition.schema.minItems === "number" && count < definition.schema.minItems) {
|
|
50
|
+
throw new Error(`Multipart field ${definition.name} requires at least ${definition.schema.minItems} value(s)`);
|
|
51
|
+
}
|
|
52
|
+
if (definition.array && typeof definition.schema.maxItems === "number" && count > definition.schema.maxItems) {
|
|
53
|
+
throw new Error(`Multipart field ${definition.name} accepts at most ${definition.schema.maxItems} value(s)`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function combinedText(rawValues, definition) {
|
|
57
|
+
if (!definition.array) {
|
|
58
|
+
assertCount(definition, rawValues.length);
|
|
59
|
+
return parseText(rawValues[0], definition.schema, definition.name);
|
|
60
|
+
}
|
|
61
|
+
if (!definition.schema.items)
|
|
62
|
+
throw new Error(`Multipart field ${definition.name} has no item schema`);
|
|
63
|
+
const values = rawValues.flatMap((raw) => {
|
|
64
|
+
const parsed = parseText(raw, definition.schema, definition.name);
|
|
65
|
+
return Array.isArray(parsed) ? parsed : [parsed];
|
|
66
|
+
});
|
|
67
|
+
assertCount(definition, values.length);
|
|
68
|
+
return values;
|
|
69
|
+
}
|
|
70
|
+
export async function prepareMultipartBody(operation, args) {
|
|
71
|
+
const body = new FormData();
|
|
72
|
+
const supplied = new Set();
|
|
73
|
+
const displayBody = {};
|
|
74
|
+
for (const [name, rawValues] of args.multipartValues) {
|
|
75
|
+
const definition = operation.requestBody?.multipartFields?.find((field) => field.name === name);
|
|
76
|
+
if (!definition)
|
|
77
|
+
throw new Error(`Unknown multipart field: ${name}`);
|
|
78
|
+
if (definition.file) {
|
|
79
|
+
if (definition.serialization !== "file")
|
|
80
|
+
throw new Error(`Multipart field ${name} has incompatible file serialization metadata`);
|
|
81
|
+
assertCount(definition, rawValues.length);
|
|
82
|
+
for (const filePath of rawValues) {
|
|
83
|
+
try {
|
|
84
|
+
body.append(name, await openAsBlob(filePath), basename(filePath));
|
|
85
|
+
}
|
|
86
|
+
catch (cause) {
|
|
87
|
+
const flag = fieldNameToFlag(name);
|
|
88
|
+
throw new Error(`Cannot read ${filePath} for ${flag}. Check that the path exists and is readable.`, { cause });
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
displayBody[name] = definition.array ? rawValues : rawValues[0];
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
const parsed = combinedText(rawValues, definition);
|
|
95
|
+
assertValid(parsed, definition.schema, schemaDefinitions, `requestBody.${name}`, { coerceStrings: true });
|
|
96
|
+
appendText(body, definition, parsed);
|
|
97
|
+
displayBody[name] = parsed;
|
|
98
|
+
}
|
|
99
|
+
supplied.add(name);
|
|
100
|
+
}
|
|
101
|
+
for (const field of operation.requestBody?.multipartFields ?? []) {
|
|
102
|
+
if (field.required && !supplied.has(field.name))
|
|
103
|
+
throw new Error(`Missing required option: ${fieldNameToFlag(field.name)}`);
|
|
104
|
+
}
|
|
105
|
+
return { body, displayBody };
|
|
106
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
function primitive(value) {
|
|
2
|
+
if (value === null || value === undefined)
|
|
3
|
+
return "";
|
|
4
|
+
if (typeof value === "object")
|
|
5
|
+
return JSON.stringify(value);
|
|
6
|
+
if (typeof value === "string")
|
|
7
|
+
return value.replace(/\s+/g, " ").trim();
|
|
8
|
+
return String(value);
|
|
9
|
+
}
|
|
10
|
+
export function resolveOutputFormat(format) {
|
|
11
|
+
return format ?? "json";
|
|
12
|
+
}
|
|
13
|
+
function tableRows(value, pagination) {
|
|
14
|
+
if (Array.isArray(value))
|
|
15
|
+
return value;
|
|
16
|
+
if (pagination && Array.isArray(value?.[pagination.itemsField]))
|
|
17
|
+
return value[pagination.itemsField];
|
|
18
|
+
return value && typeof value === "object" ? [value] : [{ value }];
|
|
19
|
+
}
|
|
20
|
+
function renderTable(value, pagination) {
|
|
21
|
+
const rows = tableRows(value, pagination);
|
|
22
|
+
if (!rows.length)
|
|
23
|
+
return "(no results)";
|
|
24
|
+
const columns = [...new Set(rows.flatMap((row) => Object.keys(row)))].slice(0, 10);
|
|
25
|
+
const widths = columns.map((column) => Math.min(40, Math.max(column.length, ...rows.map((row) => primitive(row[column]).length))));
|
|
26
|
+
const renderCell = (value, width) => {
|
|
27
|
+
const text = primitive(value);
|
|
28
|
+
const visible = text.length > width && width > 1 ? `${text.slice(0, width - 1)}…` : text.slice(0, width);
|
|
29
|
+
return visible.padEnd(width);
|
|
30
|
+
};
|
|
31
|
+
const renderRow = (row) => columns.map((column, index) => renderCell(row[column], widths[index])).join(" ").trimEnd();
|
|
32
|
+
return [renderRow(Object.fromEntries(columns.map((column) => [column, column]))), ...rows.map(renderRow)].join("\n");
|
|
33
|
+
}
|
|
34
|
+
export function renderOutput(value, format, pagination) {
|
|
35
|
+
if (format === "json")
|
|
36
|
+
return JSON.stringify(value, null, 2);
|
|
37
|
+
if (format === "jsonl") {
|
|
38
|
+
const items = pagination && value?.[pagination.itemsField];
|
|
39
|
+
const rows = Array.isArray(value) ? value : Array.isArray(items) ? items : [value];
|
|
40
|
+
return rows.map((row) => JSON.stringify(row)).join("\n");
|
|
41
|
+
}
|
|
42
|
+
return renderTable(value, pagination);
|
|
43
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { createInterface } from "node:readline/promises";
|
|
2
|
+
function fieldLabel(name) {
|
|
3
|
+
return name
|
|
4
|
+
.split("_")
|
|
5
|
+
.map((part) => part ? part[0].toUpperCase() + part.slice(1) : part)
|
|
6
|
+
.join(" ");
|
|
7
|
+
}
|
|
8
|
+
function compact(value) {
|
|
9
|
+
return value.replace(/\s+/g, " ").trim();
|
|
10
|
+
}
|
|
11
|
+
export function parsePromptValue(field, input) {
|
|
12
|
+
const value = input.trim();
|
|
13
|
+
if (!value)
|
|
14
|
+
throw new Error("A value is required");
|
|
15
|
+
if (field.nullable && value === "null")
|
|
16
|
+
return null;
|
|
17
|
+
let parsed;
|
|
18
|
+
if (field.type === "boolean") {
|
|
19
|
+
if (!/^(true|false)$/i.test(value))
|
|
20
|
+
throw new Error("Enter true or false");
|
|
21
|
+
parsed = value.toLowerCase() === "true";
|
|
22
|
+
}
|
|
23
|
+
else if (field.type === "integer" || field.type === "number") {
|
|
24
|
+
parsed = Number(value);
|
|
25
|
+
if (!Number.isFinite(parsed) || field.type === "integer" && !Number.isInteger(parsed)) {
|
|
26
|
+
throw new Error(`Enter a valid ${field.type}`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
else if (field.array || ["object", "oneOf", "anyOf", "unknown"].includes(field.type)) {
|
|
30
|
+
try {
|
|
31
|
+
parsed = JSON.parse(value);
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
throw new Error("Enter a valid JSON value");
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
parsed = value;
|
|
39
|
+
}
|
|
40
|
+
if (field.enum?.length && !field.enum.some((candidate) => JSON.stringify(candidate) === JSON.stringify(parsed))) {
|
|
41
|
+
throw new Error(`Choose one of: ${field.enum.map((value) => typeof value === "string" ? value : JSON.stringify(value)).join(", ")}`);
|
|
42
|
+
}
|
|
43
|
+
return parsed;
|
|
44
|
+
}
|
|
45
|
+
function terminalDriver() {
|
|
46
|
+
const readline = createInterface({ input: process.stdin, output: process.stdout });
|
|
47
|
+
return {
|
|
48
|
+
write: (value) => process.stdout.write(value),
|
|
49
|
+
question: (value) => readline.question(value),
|
|
50
|
+
close: () => readline.close(),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
export async function promptForRequiredJsonBody(operation, confirm, initial = {}, suppliedDriver) {
|
|
54
|
+
const fields = operation.requestBody?.fields?.filter((field) => field.required && !Object.hasOwn(initial, field.name)) ?? [];
|
|
55
|
+
if (!fields.length) {
|
|
56
|
+
return initial;
|
|
57
|
+
}
|
|
58
|
+
const driver = suppliedDriver ?? terminalDriver();
|
|
59
|
+
const result = { ...initial };
|
|
60
|
+
driver.write(`\n${operation.description.trim()}\n${fields.length} missing required field(s)\n`);
|
|
61
|
+
try {
|
|
62
|
+
for (const field of fields) {
|
|
63
|
+
driver.write(`\n${fieldLabel(field.name)} (required, ${field.type})\n`);
|
|
64
|
+
if (field.description)
|
|
65
|
+
driver.write(`${compact(field.description)}\n`);
|
|
66
|
+
if (field.example !== undefined)
|
|
67
|
+
driver.write(`Example: ${typeof field.example === "string" ? field.example : JSON.stringify(field.example)}\n`);
|
|
68
|
+
if (field.enum?.length)
|
|
69
|
+
driver.write(`Allowed: ${field.enum.map((value) => typeof value === "string" ? value : JSON.stringify(value)).join(", ")}\n`);
|
|
70
|
+
while (true) {
|
|
71
|
+
const input = await driver.question("> ");
|
|
72
|
+
try {
|
|
73
|
+
result[field.name] = parsePromptValue(field, input);
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
driver.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if (confirm) {
|
|
82
|
+
const answer = (await driver.question("\nSend request? [Y/n] ")).trim().toLowerCase();
|
|
83
|
+
if (answer && answer !== "y" && answer !== "yes")
|
|
84
|
+
throw new Error("Request cancelled");
|
|
85
|
+
}
|
|
86
|
+
return result;
|
|
87
|
+
}
|
|
88
|
+
finally {
|
|
89
|
+
driver.close();
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { schemaDefinitions } from "../generated/operations.js";
|
|
3
|
+
import { prepareMultipartBody } from "./multipart.js";
|
|
4
|
+
import { promptForRequiredJsonBody } from "./prompt.js";
|
|
5
|
+
import { isSensitiveName, redactSecrets } from "./security.js";
|
|
6
|
+
import { assertValid } from "./validate.js";
|
|
7
|
+
async function readStdin() {
|
|
8
|
+
const chunks = [];
|
|
9
|
+
for await (const chunk of process.stdin)
|
|
10
|
+
chunks.push(Buffer.from(chunk));
|
|
11
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
12
|
+
}
|
|
13
|
+
async function readBody(file) {
|
|
14
|
+
return file === "-" ? readStdin() : readFile(file, "utf8");
|
|
15
|
+
}
|
|
16
|
+
function isRecord(value) {
|
|
17
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
18
|
+
}
|
|
19
|
+
function parameterValue(parameter, rawValues) {
|
|
20
|
+
let value;
|
|
21
|
+
if (parameter.array) {
|
|
22
|
+
if (rawValues.length === 1 && rawValues[0].trim().startsWith("[")) {
|
|
23
|
+
const parsed = JSON.parse(rawValues[0]);
|
|
24
|
+
if (!Array.isArray(parsed))
|
|
25
|
+
throw new Error(`${parameter.flag} expects a JSON array or repeated values`);
|
|
26
|
+
value = parsed.map((item) => String(item));
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
value = rawValues;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
else if (parameter.type === "object") {
|
|
33
|
+
if (rawValues.length !== 1)
|
|
34
|
+
throw new Error(`${parameter.flag} expects one JSON object`);
|
|
35
|
+
const parsed = JSON.parse(rawValues[0]);
|
|
36
|
+
if (!isRecord(parsed))
|
|
37
|
+
throw new Error(`${parameter.flag} expects a JSON object`);
|
|
38
|
+
value = parsed;
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
value = rawValues.at(-1) ?? "";
|
|
42
|
+
}
|
|
43
|
+
assertValid(value, parameter.validationSchema, schemaDefinitions, `parameter.${parameter.name}`, { coerceStrings: true });
|
|
44
|
+
return value;
|
|
45
|
+
}
|
|
46
|
+
function scalar(value) {
|
|
47
|
+
return typeof value === "string" ? value : JSON.stringify(value);
|
|
48
|
+
}
|
|
49
|
+
function simpleValue(parameter, rawValues, encode) {
|
|
50
|
+
const value = parameterValue(parameter, rawValues);
|
|
51
|
+
if (Array.isArray(value))
|
|
52
|
+
return value.map((item) => encode(scalar(item))).join(",");
|
|
53
|
+
if (isRecord(value)) {
|
|
54
|
+
return Object.entries(value)
|
|
55
|
+
.flatMap(([key, item]) => parameter.explode ? [`${encode(key)}=${encode(scalar(item))}`] : [encode(key), encode(scalar(item))])
|
|
56
|
+
.join(",");
|
|
57
|
+
}
|
|
58
|
+
return encode(value);
|
|
59
|
+
}
|
|
60
|
+
function queryPairs(parameter, rawValues) {
|
|
61
|
+
const value = parameterValue(parameter, rawValues);
|
|
62
|
+
if (Array.isArray(value)) {
|
|
63
|
+
if (parameter.style === "form" && parameter.explode)
|
|
64
|
+
return value.map((item) => [parameter.name, scalar(item)]);
|
|
65
|
+
const separator = parameter.style === "spaceDelimited" ? " " : parameter.style === "pipeDelimited" ? "|" : ",";
|
|
66
|
+
return [[parameter.name, value.map(scalar).join(separator)]];
|
|
67
|
+
}
|
|
68
|
+
if (isRecord(value)) {
|
|
69
|
+
if (parameter.style === "deepObject")
|
|
70
|
+
return Object.entries(value).map(([key, item]) => [`${parameter.name}[${key}]`, scalar(item)]);
|
|
71
|
+
if (parameter.explode)
|
|
72
|
+
return Object.entries(value).map(([key, item]) => [key, scalar(item)]);
|
|
73
|
+
return [[parameter.name, Object.entries(value).flatMap(([key, item]) => [key, scalar(item)]).join(",")]];
|
|
74
|
+
}
|
|
75
|
+
return [[parameter.name, value]];
|
|
76
|
+
}
|
|
77
|
+
export async function prepareRequest(operation, args, apiKey, defaultBaseUrl) {
|
|
78
|
+
let path = operation.path;
|
|
79
|
+
for (const [name, rawValue] of args.pathValues) {
|
|
80
|
+
const parameter = operation.parameters.find((candidate) => candidate.in === "path" && candidate.name === name);
|
|
81
|
+
path = path.replace(`{${name}}`, simpleValue(parameter, [rawValue], encodeURIComponent));
|
|
82
|
+
}
|
|
83
|
+
const url = new URL(path, args.baseUrl ?? defaultBaseUrl);
|
|
84
|
+
for (const [name, values] of args.queryValues) {
|
|
85
|
+
const parameter = operation.parameters.find((candidate) => candidate.in === "query" && candidate.name === name);
|
|
86
|
+
for (const [key, value] of queryPairs(parameter, values))
|
|
87
|
+
url.searchParams.append(key, value);
|
|
88
|
+
}
|
|
89
|
+
if (operation.authenticated && !apiKey && !args.dryRun) {
|
|
90
|
+
throw new Error("No Retell API key is configured. Run 'retell auth login' or set RETELL_API_KEY.");
|
|
91
|
+
}
|
|
92
|
+
const headers = new Headers({ Accept: "application/json" });
|
|
93
|
+
if (operation.authenticated && apiKey)
|
|
94
|
+
headers.set("Authorization", `Bearer ${apiKey}`);
|
|
95
|
+
for (const [name, values] of args.headerValues) {
|
|
96
|
+
const parameter = operation.parameters.find((candidate) => candidate.in === "header" && candidate.name === name);
|
|
97
|
+
headers.set(name, simpleValue(parameter, values, (value) => value));
|
|
98
|
+
}
|
|
99
|
+
const cookies = [];
|
|
100
|
+
for (const [name, values] of args.cookieValues) {
|
|
101
|
+
const parameter = operation.parameters.find((candidate) => candidate.in === "cookie" && candidate.name === name);
|
|
102
|
+
for (const [key, value] of queryPairs(parameter, values))
|
|
103
|
+
cookies.push(`${encodeURIComponent(key)}=${encodeURIComponent(value)}`);
|
|
104
|
+
}
|
|
105
|
+
if (cookies.length)
|
|
106
|
+
headers.set("Cookie", cookies.join("; "));
|
|
107
|
+
const request = { url: url.toString(), init: { method: operation.method, headers, redirect: "manual" } };
|
|
108
|
+
if (operation.requestBody?.contentType === "application/json") {
|
|
109
|
+
let bodyText;
|
|
110
|
+
if (args.bodyFile)
|
|
111
|
+
bodyText = await readBody(args.bodyFile);
|
|
112
|
+
else if (!process.stdin.isTTY)
|
|
113
|
+
bodyText = await readStdin();
|
|
114
|
+
let parsed;
|
|
115
|
+
if (bodyText?.trim()) {
|
|
116
|
+
parsed = JSON.parse(bodyText);
|
|
117
|
+
}
|
|
118
|
+
if (args.bodyValues.size) {
|
|
119
|
+
if (parsed === undefined)
|
|
120
|
+
parsed = {};
|
|
121
|
+
if (!isRecord(parsed))
|
|
122
|
+
throw new Error("JSON body flags can only be combined with an object request body.");
|
|
123
|
+
for (const [name, value] of args.bodyValues)
|
|
124
|
+
parsed[name] = value;
|
|
125
|
+
}
|
|
126
|
+
const requiredFields = operation.requestBody.fields?.filter((field) => field.required) ?? [];
|
|
127
|
+
if (parsed !== undefined && requiredFields.length && !isRecord(parsed)) {
|
|
128
|
+
throw new Error("This operation requires a JSON object request body.");
|
|
129
|
+
}
|
|
130
|
+
const supplied = isRecord(parsed) ? parsed : {};
|
|
131
|
+
const missing = requiredFields.filter((field) => !Object.hasOwn(supplied, field.name));
|
|
132
|
+
if (missing.length && process.stdin.isTTY && process.stdout.isTTY) {
|
|
133
|
+
parsed = await promptForRequiredJsonBody(operation, !args.dryRun, supplied);
|
|
134
|
+
}
|
|
135
|
+
else if (missing.length) {
|
|
136
|
+
throw new Error(`Missing required JSON body option(s): ${missing.map((field) => field.flag).join(", ")}. Use flags, --body FILE, or stdin.`);
|
|
137
|
+
}
|
|
138
|
+
else if (parsed === undefined && operation.requestBody.required && process.stdin.isTTY && process.stdout.isTTY) {
|
|
139
|
+
parsed = await promptForRequiredJsonBody(operation, !args.dryRun);
|
|
140
|
+
}
|
|
141
|
+
else if (parsed === undefined && operation.requestBody.required) {
|
|
142
|
+
throw new Error(`A JSON request body is required. Use body flags, --body FILE, or pipe JSON to stdin.`);
|
|
143
|
+
}
|
|
144
|
+
if (parsed !== undefined) {
|
|
145
|
+
assertValid(parsed, operation.requestBody.validationSchema, schemaDefinitions, "requestBody");
|
|
146
|
+
request.init.body = JSON.stringify(parsed);
|
|
147
|
+
headers.set("Content-Type", "application/json");
|
|
148
|
+
request.displayBody = parsed;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
if (operation.requestBody?.contentType === "multipart/form-data") {
|
|
152
|
+
const multipart = await prepareMultipartBody(operation, args);
|
|
153
|
+
request.init.body = multipart.body;
|
|
154
|
+
request.displayBody = multipart.displayBody;
|
|
155
|
+
}
|
|
156
|
+
return request;
|
|
157
|
+
}
|
|
158
|
+
export function dryRunOutput(operation, request) {
|
|
159
|
+
const headers = Object.fromEntries(new Headers(request.init.headers).entries());
|
|
160
|
+
if (operation.authenticated)
|
|
161
|
+
headers.authorization = headers.authorization ? "Bearer [REDACTED]" : "Bearer <RETELL_API_KEY>";
|
|
162
|
+
for (const name of Object.keys(headers)) {
|
|
163
|
+
if (name !== "authorization" && isSensitiveName(name))
|
|
164
|
+
headers[name] = "[REDACTED]";
|
|
165
|
+
}
|
|
166
|
+
const url = new URL(request.url);
|
|
167
|
+
for (const name of [...new Set(url.searchParams.keys())]) {
|
|
168
|
+
if (isSensitiveName(name))
|
|
169
|
+
url.searchParams.set(name, "[REDACTED]");
|
|
170
|
+
}
|
|
171
|
+
return {
|
|
172
|
+
operationId: operation.operationId,
|
|
173
|
+
command: operation.command,
|
|
174
|
+
method: operation.method,
|
|
175
|
+
url: url.toString(),
|
|
176
|
+
headers,
|
|
177
|
+
body: redactSecrets(request.displayBody),
|
|
178
|
+
};
|
|
179
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
function isRecord(value) {
|
|
2
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3
|
+
}
|
|
4
|
+
export function isSensitiveName(name) {
|
|
5
|
+
return /(?:^|[-_])(authorization|cookie|api[-_]?key|token|secret|password)(?:$|[-_])/i.test(name);
|
|
6
|
+
}
|
|
7
|
+
export function redactSecrets(value) {
|
|
8
|
+
if (Array.isArray(value))
|
|
9
|
+
return value.map(redactSecrets);
|
|
10
|
+
if (!isRecord(value))
|
|
11
|
+
return value;
|
|
12
|
+
return Object.fromEntries(Object.entries(value).map(([name, child]) => [name, isSensitiveName(name) ? "[REDACTED]" : redactSecrets(child)]));
|
|
13
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
function isRecord(value) {
|
|
2
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3
|
+
}
|
|
4
|
+
function sameValue(left, right) {
|
|
5
|
+
if (Object.is(left, right))
|
|
6
|
+
return true;
|
|
7
|
+
if (Array.isArray(left) && Array.isArray(right)) {
|
|
8
|
+
return left.length === right.length && left.every((value, index) => sameValue(value, right[index]));
|
|
9
|
+
}
|
|
10
|
+
if (isRecord(left) && isRecord(right)) {
|
|
11
|
+
const leftKeys = Object.keys(left).sort();
|
|
12
|
+
const rightKeys = Object.keys(right).sort();
|
|
13
|
+
return sameValue(leftKeys, rightKeys) && leftKeys.every((key) => sameValue(left[key], right[key]));
|
|
14
|
+
}
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
function coerce(value, type, enabled) {
|
|
18
|
+
if (!enabled || typeof value !== "string")
|
|
19
|
+
return value;
|
|
20
|
+
if (type === "boolean" && (value === "true" || value === "false"))
|
|
21
|
+
return value === "true";
|
|
22
|
+
if (type === "integer" && /^-?(?:0|[1-9]\d*)$/.test(value))
|
|
23
|
+
return Number(value);
|
|
24
|
+
if (type === "number" && /^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?$/.test(value))
|
|
25
|
+
return Number(value);
|
|
26
|
+
if (type === "null" && value === "null")
|
|
27
|
+
return null;
|
|
28
|
+
return value;
|
|
29
|
+
}
|
|
30
|
+
function typeMatches(value, type) {
|
|
31
|
+
if (type === "null")
|
|
32
|
+
return value === null;
|
|
33
|
+
if (type === "array")
|
|
34
|
+
return Array.isArray(value);
|
|
35
|
+
if (type === "object")
|
|
36
|
+
return isRecord(value);
|
|
37
|
+
if (type === "integer")
|
|
38
|
+
return typeof value === "number" && Number.isFinite(value) && Number.isInteger(value);
|
|
39
|
+
if (type === "number")
|
|
40
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
41
|
+
return typeof value === type;
|
|
42
|
+
}
|
|
43
|
+
function childPath(path, key) {
|
|
44
|
+
return typeof key === "number" ? `${path}[${key}]` : `${path}.${key}`;
|
|
45
|
+
}
|
|
46
|
+
function validate(value, schema, definitions, path, options, depth) {
|
|
47
|
+
if (depth > 100)
|
|
48
|
+
return [`${path} exceeds the supported validation depth`];
|
|
49
|
+
if (schema === true || schema === undefined)
|
|
50
|
+
return [];
|
|
51
|
+
if (schema === false)
|
|
52
|
+
return [`${path} is forbidden by the OpenAPI schema`];
|
|
53
|
+
if (!isRecord(schema))
|
|
54
|
+
return [`${path} has an invalid generated validation schema`];
|
|
55
|
+
const errors = [];
|
|
56
|
+
if (schema.readOnly === true)
|
|
57
|
+
errors.push(`${path} is read-only and cannot be sent in a request`);
|
|
58
|
+
if (value === null && schema.nullable === true)
|
|
59
|
+
return errors;
|
|
60
|
+
if (typeof schema.$ref === "string") {
|
|
61
|
+
const prefix = "#/components/schemas/";
|
|
62
|
+
if (!schema.$ref.startsWith(prefix))
|
|
63
|
+
return [`${path} uses unsupported schema reference ${schema.$ref}`];
|
|
64
|
+
const name = schema.$ref.slice(prefix.length).replace(/~1/g, "/").replace(/~0/g, "~");
|
|
65
|
+
const target = definitions[name];
|
|
66
|
+
if (!target)
|
|
67
|
+
return [`${path} references missing schema ${name}`];
|
|
68
|
+
errors.push(...validate(value, target, definitions, path, options, depth + 1));
|
|
69
|
+
}
|
|
70
|
+
const variants = (key) => Array.isArray(schema[key]) ? schema[key] : [];
|
|
71
|
+
for (const part of variants("allOf"))
|
|
72
|
+
errors.push(...validate(value, part, definitions, path, options, depth + 1));
|
|
73
|
+
const anyOf = variants("anyOf");
|
|
74
|
+
if (anyOf.length) {
|
|
75
|
+
const branchErrors = anyOf.map((part) => validate(value, part, definitions, path, options, depth + 1));
|
|
76
|
+
if (!branchErrors.some((issues) => issues.length === 0)) {
|
|
77
|
+
const closest = branchErrors.reduce((best, issues) => issues.length < best.length ? issues : best);
|
|
78
|
+
errors.push(`${path} does not match any allowed schema`, ...closest);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
const oneOf = variants("oneOf");
|
|
82
|
+
if (oneOf.length) {
|
|
83
|
+
const branchErrors = oneOf.map((part) => validate(value, part, definitions, path, options, depth + 1));
|
|
84
|
+
const matches = branchErrors.filter((issues) => issues.length === 0).length;
|
|
85
|
+
if (matches !== 1) {
|
|
86
|
+
errors.push(`${path} must match exactly one allowed schema (matched ${matches})`);
|
|
87
|
+
if (matches === 0)
|
|
88
|
+
errors.push(...branchErrors.reduce((best, issues) => issues.length < best.length ? issues : best));
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (schema.not !== undefined && validate(value, schema.not, definitions, path, options, depth + 1).length === 0) {
|
|
92
|
+
errors.push(`${path} matches a forbidden schema`);
|
|
93
|
+
}
|
|
94
|
+
const rawTypes = Array.isArray(schema.type) ? schema.type : typeof schema.type === "string" ? [schema.type] : [];
|
|
95
|
+
const candidateTypes = rawTypes.filter((type) => typeof type === "string");
|
|
96
|
+
const effectiveValues = candidateTypes.map((type) => coerce(value, type, options.coerceStrings === true));
|
|
97
|
+
const matchedIndex = candidateTypes.findIndex((type, index) => typeMatches(effectiveValues[index], type));
|
|
98
|
+
if (candidateTypes.length && matchedIndex < 0) {
|
|
99
|
+
errors.push(`${path} must be ${candidateTypes.join(" or ")}; received ${Array.isArray(value) ? "array" : value === null ? "null" : typeof value}`);
|
|
100
|
+
return errors;
|
|
101
|
+
}
|
|
102
|
+
const effective = matchedIndex >= 0 ? effectiveValues[matchedIndex] : value;
|
|
103
|
+
if (Array.isArray(schema.enum) && !schema.enum.some((allowed) => sameValue(effective, allowed))) {
|
|
104
|
+
errors.push(`${path} must be one of ${schema.enum.map((item) => JSON.stringify(item)).join(", ")}`);
|
|
105
|
+
}
|
|
106
|
+
if (Object.hasOwn(schema, "const") && !sameValue(effective, schema.const))
|
|
107
|
+
errors.push(`${path} must equal ${JSON.stringify(schema.const)}`);
|
|
108
|
+
if (typeof effective === "number") {
|
|
109
|
+
const minimum = typeof schema.minimum === "number" ? schema.minimum : undefined;
|
|
110
|
+
const maximum = typeof schema.maximum === "number" ? schema.maximum : undefined;
|
|
111
|
+
if (minimum !== undefined && (schema.exclusiveMinimum === true ? effective <= minimum : effective < minimum)) {
|
|
112
|
+
errors.push(`${path} must be ${schema.exclusiveMinimum === true ? ">" : ">="} ${minimum}`);
|
|
113
|
+
}
|
|
114
|
+
if (maximum !== undefined && (schema.exclusiveMaximum === true ? effective >= maximum : effective > maximum)) {
|
|
115
|
+
errors.push(`${path} must be ${schema.exclusiveMaximum === true ? "<" : "<="} ${maximum}`);
|
|
116
|
+
}
|
|
117
|
+
if (typeof schema.exclusiveMinimum === "number" && effective <= schema.exclusiveMinimum)
|
|
118
|
+
errors.push(`${path} must be > ${schema.exclusiveMinimum}`);
|
|
119
|
+
if (typeof schema.exclusiveMaximum === "number" && effective >= schema.exclusiveMaximum)
|
|
120
|
+
errors.push(`${path} must be < ${schema.exclusiveMaximum}`);
|
|
121
|
+
if (typeof schema.multipleOf === "number" && Math.abs(effective / schema.multipleOf - Math.round(effective / schema.multipleOf)) > 1e-10) {
|
|
122
|
+
errors.push(`${path} must be a multiple of ${schema.multipleOf}`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (typeof effective === "string") {
|
|
126
|
+
if (typeof schema.minLength === "number" && [...effective].length < schema.minLength)
|
|
127
|
+
errors.push(`${path} must contain at least ${schema.minLength} characters`);
|
|
128
|
+
if (typeof schema.maxLength === "number" && [...effective].length > schema.maxLength)
|
|
129
|
+
errors.push(`${path} must contain at most ${schema.maxLength} characters`);
|
|
130
|
+
if (typeof schema.pattern === "string" && !new RegExp(schema.pattern).test(effective))
|
|
131
|
+
errors.push(`${path} must match ${schema.pattern}`);
|
|
132
|
+
}
|
|
133
|
+
if (Array.isArray(effective)) {
|
|
134
|
+
if (typeof schema.minItems === "number" && effective.length < schema.minItems)
|
|
135
|
+
errors.push(`${path} must contain at least ${schema.minItems} items`);
|
|
136
|
+
if (typeof schema.maxItems === "number" && effective.length > schema.maxItems)
|
|
137
|
+
errors.push(`${path} must contain at most ${schema.maxItems} items`);
|
|
138
|
+
if (schema.uniqueItems === true && effective.some((item, index) => effective.slice(0, index).some((prior) => sameValue(item, prior)))) {
|
|
139
|
+
errors.push(`${path} must contain unique items`);
|
|
140
|
+
}
|
|
141
|
+
if (schema.items !== undefined) {
|
|
142
|
+
effective.forEach((item, index) => errors.push(...validate(item, schema.items, definitions, childPath(path, index), options, depth + 1)));
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if (isRecord(effective)) {
|
|
146
|
+
const keys = Object.keys(effective);
|
|
147
|
+
if (typeof schema.minProperties === "number" && keys.length < schema.minProperties)
|
|
148
|
+
errors.push(`${path} must contain at least ${schema.minProperties} properties`);
|
|
149
|
+
if (typeof schema.maxProperties === "number" && keys.length > schema.maxProperties)
|
|
150
|
+
errors.push(`${path} must contain at most ${schema.maxProperties} properties`);
|
|
151
|
+
const properties = isRecord(schema.properties) ? schema.properties : {};
|
|
152
|
+
const required = Array.isArray(schema.required) ? schema.required.filter((name) => typeof name === "string") : [];
|
|
153
|
+
for (const name of required)
|
|
154
|
+
if (!Object.hasOwn(effective, name))
|
|
155
|
+
errors.push(`${childPath(path, name)} is required`);
|
|
156
|
+
for (const [name, child] of Object.entries(effective)) {
|
|
157
|
+
if (Object.hasOwn(properties, name))
|
|
158
|
+
errors.push(...validate(child, properties[name], definitions, childPath(path, name), options, depth + 1));
|
|
159
|
+
else if (schema.additionalProperties === false)
|
|
160
|
+
errors.push(`${childPath(path, name)} is not allowed`);
|
|
161
|
+
else if (isRecord(schema.additionalProperties) || typeof schema.additionalProperties === "boolean") {
|
|
162
|
+
errors.push(...validate(child, schema.additionalProperties, definitions, childPath(path, name), options, depth + 1));
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return errors;
|
|
167
|
+
}
|
|
168
|
+
export function assertValid(value, schema, definitions, path, options = {}) {
|
|
169
|
+
const errors = [...new Set(validate(value, schema, definitions, path, options, 0))];
|
|
170
|
+
if (errors.length)
|
|
171
|
+
throw new Error(`OpenAPI validation failed: ${errors.slice(0, 8).join("; ")}${errors.length > 8 ? `; and ${errors.length - 8} more` : ""}`);
|
|
172
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
export function cliVersion() {
|
|
3
|
+
const packageUrl = new URL("../../../package.json", import.meta.url);
|
|
4
|
+
const metadata = JSON.parse(readFileSync(packageUrl, "utf8"));
|
|
5
|
+
if (typeof metadata.version !== "string" || !metadata.version) {
|
|
6
|
+
throw new Error("CLI package version is missing from package.json");
|
|
7
|
+
}
|
|
8
|
+
return metadata.version;
|
|
9
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@retell-ai/retell-cli",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Generated command line interface for the Retell API",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/RetellAI/retell-cli.git"
|
|
9
|
+
},
|
|
10
|
+
"publishConfig": {
|
|
11
|
+
"access": "public"
|
|
12
|
+
},
|
|
13
|
+
"type": "module",
|
|
14
|
+
"bin": {
|
|
15
|
+
"retell": "dist/src/index.js"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist/src",
|
|
19
|
+
"LICENSE",
|
|
20
|
+
"NOTICE",
|
|
21
|
+
"THIRD_PARTY_NOTICES.md"
|
|
22
|
+
],
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=22"
|
|
25
|
+
},
|
|
26
|
+
"scripts": {
|
|
27
|
+
"build": "tsc -p tsconfig.json",
|
|
28
|
+
"license:check": "node scripts/check-licenses.mjs",
|
|
29
|
+
"prepack": "pnpm build",
|
|
30
|
+
"release:check": "node scripts/check-release-version.mjs",
|
|
31
|
+
"test": "node --test dist/test/*.test.js"
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"@napi-rs/keyring": "^1.3.0"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@types/node": "^24.0.0",
|
|
38
|
+
"typescript": "^5.9.0"
|
|
39
|
+
}
|
|
40
|
+
}
|