@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,59 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { catalogMetadata, operations as generatedOperations } from "./generated/operations.js";
|
|
3
|
+
import { parseArguments } from "./runtime/args.js";
|
|
4
|
+
import { handleAuth } from "./runtime/auth.js";
|
|
5
|
+
import { resolveApiKey } from "./runtime/credentials.js";
|
|
6
|
+
import { commandHelp, rootHelp } from "./runtime/help.js";
|
|
7
|
+
import { sendRequest } from "./runtime/http.js";
|
|
8
|
+
import { renderOutput, resolveOutputFormat } from "./runtime/output.js";
|
|
9
|
+
import { dryRunOutput, prepareRequest } from "./runtime/request.js";
|
|
10
|
+
import { cliVersion } from "./runtime/version.js";
|
|
11
|
+
const operations = generatedOperations;
|
|
12
|
+
async function main() {
|
|
13
|
+
const [first, ...rest] = process.argv.slice(2);
|
|
14
|
+
if (!first || first === "--help" || first === "-h" || first === "help" && !rest[0]) {
|
|
15
|
+
process.stdout.write(rootHelp(operations));
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
if (first === "--version" || first === "version") {
|
|
19
|
+
process.stdout.write(`retell ${cliVersion()} (OpenAPI ${catalogMetadata.apiVersion}, catalog v${catalogMetadata.schemaVersion})\n`);
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
if (first === "auth") {
|
|
23
|
+
await handleAuth(rest);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
const commandName = first === "help" ? rest.shift() : first;
|
|
27
|
+
const operation = operations.find((candidate) => candidate.command === commandName);
|
|
28
|
+
if (!operation)
|
|
29
|
+
throw new Error(`Unknown command: ${commandName}. Run 'retell --help' to list commands.`);
|
|
30
|
+
if (first === "help" || rest.includes("--help") || rest.includes("-h")) {
|
|
31
|
+
process.stdout.write(commandHelp(operation));
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const args = parseArguments(operation, rest);
|
|
35
|
+
const credential = await resolveApiKey();
|
|
36
|
+
const request = await prepareRequest(operation, args, credential.value, catalogMetadata.server);
|
|
37
|
+
if (args.dryRun) {
|
|
38
|
+
process.stdout.write(`${JSON.stringify(dryRunOutput(operation, request), null, 2)}\n`);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const response = await sendRequest(operation, request.url, request.init);
|
|
42
|
+
const text = await response.text();
|
|
43
|
+
const value = text ? (() => { try {
|
|
44
|
+
return JSON.parse(text);
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return text;
|
|
48
|
+
} })() : null;
|
|
49
|
+
if (!response.ok) {
|
|
50
|
+
const requestId = response.headers.get("x-request-id");
|
|
51
|
+
throw new Error(`Retell API request failed (${response.status}${requestId ? `, request ${requestId}` : ""}): ${typeof value === "string" ? value : JSON.stringify(value)}`);
|
|
52
|
+
}
|
|
53
|
+
const format = resolveOutputFormat(args.output);
|
|
54
|
+
process.stdout.write(`${renderOutput(value, format, operation.pagination)}\n`);
|
|
55
|
+
}
|
|
56
|
+
main().catch((error) => {
|
|
57
|
+
process.stderr.write(`Error: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
58
|
+
process.exitCode = 1;
|
|
59
|
+
});
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { fieldNameToFlag } from "./naming.js";
|
|
2
|
+
import { parsePromptValue } from "./prompt.js";
|
|
3
|
+
function takeValue(tokens, index, flag) {
|
|
4
|
+
const value = tokens[index + 1];
|
|
5
|
+
if (value === undefined || value.startsWith("--"))
|
|
6
|
+
throw new Error(`${flag} requires a value`);
|
|
7
|
+
return [value, index + 1];
|
|
8
|
+
}
|
|
9
|
+
function parameterForFlag(operation, flag) {
|
|
10
|
+
return operation.parameters.find((parameter) => parameter.in !== "path" && parameter.flag === flag);
|
|
11
|
+
}
|
|
12
|
+
function bodyFieldForFlag(operation, flag) {
|
|
13
|
+
return operation.requestBody?.contentType === "application/json"
|
|
14
|
+
? operation.requestBody.fields?.find((field) => field.flag === flag)
|
|
15
|
+
: undefined;
|
|
16
|
+
}
|
|
17
|
+
function multipartFieldForFlag(operation, flag) {
|
|
18
|
+
return operation.requestBody?.contentType === "multipart/form-data"
|
|
19
|
+
? operation.requestBody.multipartFields?.find((field) => fieldNameToFlag(field.name) === flag)
|
|
20
|
+
: undefined;
|
|
21
|
+
}
|
|
22
|
+
export function parseArguments(operation, tokens) {
|
|
23
|
+
const result = {
|
|
24
|
+
pathValues: new Map(),
|
|
25
|
+
queryValues: new Map(),
|
|
26
|
+
headerValues: new Map(),
|
|
27
|
+
cookieValues: new Map(),
|
|
28
|
+
bodyValues: new Map(),
|
|
29
|
+
multipartValues: new Map(),
|
|
30
|
+
dryRun: false,
|
|
31
|
+
};
|
|
32
|
+
const positionals = [];
|
|
33
|
+
for (let index = 0; index < tokens.length; index += 1) {
|
|
34
|
+
const token = tokens[index];
|
|
35
|
+
if (!token.startsWith("-")) {
|
|
36
|
+
positionals.push(token);
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
if (token === "--dry-run") {
|
|
40
|
+
result.dryRun = true;
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (token === "--body" || token === "--output" || token === "--base-url") {
|
|
44
|
+
const [value, consumed] = takeValue(tokens, index, token);
|
|
45
|
+
index = consumed;
|
|
46
|
+
if (token === "--body")
|
|
47
|
+
result.bodyFile = value;
|
|
48
|
+
if (token === "--base-url")
|
|
49
|
+
result.baseUrl = value;
|
|
50
|
+
if (token === "--output") {
|
|
51
|
+
if (!new Set(["table", "json", "jsonl"]).has(value))
|
|
52
|
+
throw new Error(`Unsupported output format: ${value}`);
|
|
53
|
+
result.output = value;
|
|
54
|
+
}
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
const parameter = parameterForFlag(operation, token);
|
|
58
|
+
const bodyField = bodyFieldForFlag(operation, token);
|
|
59
|
+
const multipartField = multipartFieldForFlag(operation, token);
|
|
60
|
+
if (!parameter && !bodyField && !multipartField)
|
|
61
|
+
throw new Error(`Unknown option for ${operation.command}: ${token}`);
|
|
62
|
+
let value;
|
|
63
|
+
const type = parameter?.type ?? bodyField?.type ?? multipartField.type;
|
|
64
|
+
if (type === "boolean" && (tokens[index + 1] === undefined || tokens[index + 1].startsWith("-"))) {
|
|
65
|
+
value = "true";
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
const taken = takeValue(tokens, index, token);
|
|
69
|
+
value = taken[0];
|
|
70
|
+
index = taken[1];
|
|
71
|
+
}
|
|
72
|
+
if (parameter) {
|
|
73
|
+
const values = parameter.in === "query" ? result.queryValues : parameter.in === "header" ? result.headerValues : result.cookieValues;
|
|
74
|
+
values.set(parameter.name, [...(values.get(parameter.name) ?? []), value]);
|
|
75
|
+
}
|
|
76
|
+
else if (bodyField)
|
|
77
|
+
result.bodyValues.set(bodyField.name, parsePromptValue(bodyField, value));
|
|
78
|
+
else
|
|
79
|
+
result.multipartValues.set(multipartField.name, [...(result.multipartValues.get(multipartField.name) ?? []), value]);
|
|
80
|
+
}
|
|
81
|
+
const pathParameters = operation.parameters.filter((parameter) => parameter.in === "path");
|
|
82
|
+
if (positionals.length !== pathParameters.length) {
|
|
83
|
+
throw new Error(`${operation.command} expects ${pathParameters.length} path argument(s), received ${positionals.length}`);
|
|
84
|
+
}
|
|
85
|
+
pathParameters.forEach((parameter, index) => result.pathValues.set(parameter.name, positionals[index]));
|
|
86
|
+
for (const parameter of operation.parameters.filter((item) => item.in !== "path" && item.required)) {
|
|
87
|
+
const values = parameter.in === "query" ? result.queryValues : parameter.in === "header" ? result.headerValues : result.cookieValues;
|
|
88
|
+
if (!values.has(parameter.name))
|
|
89
|
+
throw new Error(`Missing required option: ${parameter.flag}`);
|
|
90
|
+
}
|
|
91
|
+
return result;
|
|
92
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { clearApiKey, maskApiKey, resolveApiKey, storeApiKey } from "./credentials.js";
|
|
2
|
+
function authHelp() {
|
|
3
|
+
return `Manage Retell API authentication.\n\n` +
|
|
4
|
+
`Usage:\n` +
|
|
5
|
+
` retell auth login [--with-token]\n` +
|
|
6
|
+
` retell auth whoami\n` +
|
|
7
|
+
` retell auth logout\n\n` +
|
|
8
|
+
`Commands:\n` +
|
|
9
|
+
` login Prompt for and securely store a Retell API key\n` +
|
|
10
|
+
` whoami Show whether authentication is configured and its source\n` +
|
|
11
|
+
` logout Delete the stored Retell API key\n\n` +
|
|
12
|
+
`RETELL_API_KEY overrides any stored credential.\n`;
|
|
13
|
+
}
|
|
14
|
+
async function readAllStdin() {
|
|
15
|
+
const chunks = [];
|
|
16
|
+
for await (const chunk of process.stdin)
|
|
17
|
+
chunks.push(Buffer.from(chunk));
|
|
18
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
19
|
+
}
|
|
20
|
+
async function promptHidden(prompt) {
|
|
21
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY || !process.stdin.setRawMode) {
|
|
22
|
+
throw new Error("Interactive login requires a terminal. Pipe the key to 'retell auth login --with-token' instead.");
|
|
23
|
+
}
|
|
24
|
+
process.stdout.write(prompt);
|
|
25
|
+
process.stdin.setRawMode(true);
|
|
26
|
+
process.stdin.resume();
|
|
27
|
+
process.stdin.setEncoding("utf8");
|
|
28
|
+
return new Promise((resolve, reject) => {
|
|
29
|
+
let value = "";
|
|
30
|
+
const cleanup = () => {
|
|
31
|
+
process.stdin.setRawMode(false);
|
|
32
|
+
process.stdin.pause();
|
|
33
|
+
process.stdin.removeListener("data", onData);
|
|
34
|
+
process.stdout.write("\n");
|
|
35
|
+
};
|
|
36
|
+
const onData = (chunk) => {
|
|
37
|
+
for (const character of chunk) {
|
|
38
|
+
if (character === "\u0003") {
|
|
39
|
+
cleanup();
|
|
40
|
+
reject(new Error("Login cancelled"));
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
if (character === "\r" || character === "\n") {
|
|
44
|
+
cleanup();
|
|
45
|
+
resolve(value);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
if (character === "\u007f" || character === "\b")
|
|
49
|
+
value = value.slice(0, -1);
|
|
50
|
+
else
|
|
51
|
+
value += character;
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
process.stdin.on("data", onData);
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
export async function handleAuth(tokens) {
|
|
58
|
+
const [command, ...options] = tokens;
|
|
59
|
+
if (!command || command === "--help" || command === "-h") {
|
|
60
|
+
process.stdout.write(authHelp());
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
if (command === "login") {
|
|
64
|
+
if (options.some((option) => option !== "--with-token" && option !== "--help" && option !== "-h")) {
|
|
65
|
+
throw new Error(`Unknown auth login option: ${options.find((option) => !["--with-token", "--help", "-h"].includes(option))}`);
|
|
66
|
+
}
|
|
67
|
+
if (options.includes("--help") || options.includes("-h")) {
|
|
68
|
+
process.stdout.write(`Store a Retell API key.\n\nUsage:\n retell auth login\n printf '%s' "$RETELL_API_KEY" | retell auth login --with-token\n`);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const current = await resolveApiKey();
|
|
72
|
+
if (current.source !== "unset") {
|
|
73
|
+
process.stdout.write(`Already authenticated.\nSource: ${current.source}\nAPI key: ${maskApiKey(current.value)}\n`);
|
|
74
|
+
}
|
|
75
|
+
if (current.source === "env") {
|
|
76
|
+
const guidance = "RETELL_API_KEY overrides stored credentials. Run 'unset RETELL_API_KEY' in this shell before logging in with a different key.";
|
|
77
|
+
if (options.includes("--with-token"))
|
|
78
|
+
throw new Error(guidance);
|
|
79
|
+
process.stdout.write(`${guidance}\n`);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
const token = options.includes("--with-token")
|
|
83
|
+
? await readAllStdin()
|
|
84
|
+
: await promptHidden(current.source === "unset" ? "Retell API key: " : "New Retell API key (press Enter to keep current): ");
|
|
85
|
+
if (!token.trim() && current.source !== "unset") {
|
|
86
|
+
process.stdout.write("Existing Retell API key kept unchanged.\n");
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
const source = await storeApiKey(token);
|
|
90
|
+
process.stdout.write(source === "keyring"
|
|
91
|
+
? "Retell API key stored in the OS keychain.\n"
|
|
92
|
+
: "OS keychain unavailable; Retell API key stored in ~/.config/retell/credentials.json with mode 0600.\n");
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
if (command === "whoami") {
|
|
96
|
+
const credential = await resolveApiKey();
|
|
97
|
+
process.stdout.write(`Authentication: ${credential.source === "unset" ? "not configured" : "configured"}\n`);
|
|
98
|
+
process.stdout.write(`Source: ${credential.source}\n`);
|
|
99
|
+
process.stdout.write(`API key: ${maskApiKey(credential.value)}\n`);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
if (command === "logout") {
|
|
103
|
+
const remainingSource = await clearApiKey();
|
|
104
|
+
if (remainingSource === "env") {
|
|
105
|
+
process.stdout.write("Stored Retell API credentials cleared.\n");
|
|
106
|
+
process.stdout.write("WARNING: Authentication is still active through RETELL_API_KEY. Run 'unset RETELL_API_KEY' in this shell to finish logging out.\n");
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
if (remainingSource !== "unset") {
|
|
110
|
+
throw new Error(`Failed to clear the ${remainingSource} credential; authentication is still active.`);
|
|
111
|
+
}
|
|
112
|
+
process.stdout.write("Logged out. Stored Retell API credentials were cleared.\n");
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
throw new Error(`Unknown auth command: ${command}. Run 'retell auth --help'.`);
|
|
116
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { chmod, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
4
|
+
const KEYRING_SERVICE = "retell-cli";
|
|
5
|
+
const KEYRING_ACCOUNT = "api-key";
|
|
6
|
+
function credentialsPath() {
|
|
7
|
+
const configDir = process.env.RETELL_CONFIG_DIR ?? join(homedir(), ".config", "retell");
|
|
8
|
+
return join(configDir, "credentials.json");
|
|
9
|
+
}
|
|
10
|
+
export function normalizeApiKey(value) {
|
|
11
|
+
return value.trim().replace(/^Bearer\s+/i, "").trim();
|
|
12
|
+
}
|
|
13
|
+
export function maskApiKey(value) {
|
|
14
|
+
if (!value)
|
|
15
|
+
return "not configured";
|
|
16
|
+
if (value.length <= 8)
|
|
17
|
+
return "********";
|
|
18
|
+
return `${value.slice(0, 3)}${"*".repeat(Math.min(12, value.length - 7))}${value.slice(-4)}`;
|
|
19
|
+
}
|
|
20
|
+
async function keyringEntry() {
|
|
21
|
+
if (process.env.RETELL_DISABLE_KEYRING === "1")
|
|
22
|
+
throw new Error("keyring disabled");
|
|
23
|
+
const { Entry } = await import("@napi-rs/keyring");
|
|
24
|
+
return new Entry(KEYRING_SERVICE, KEYRING_ACCOUNT);
|
|
25
|
+
}
|
|
26
|
+
async function readKeyring() {
|
|
27
|
+
try {
|
|
28
|
+
const entry = await keyringEntry();
|
|
29
|
+
const value = normalizeApiKey(entry.getPassword() ?? "");
|
|
30
|
+
return value || undefined;
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
async function writeKeyring(value) {
|
|
37
|
+
try {
|
|
38
|
+
const entry = await keyringEntry();
|
|
39
|
+
entry.setPassword(value);
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
async function deleteKeyring() {
|
|
47
|
+
try {
|
|
48
|
+
const entry = await keyringEntry();
|
|
49
|
+
entry.deletePassword();
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
// Missing or unavailable keychains are treated as already logged out.
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
async function readConfigCredential() {
|
|
56
|
+
try {
|
|
57
|
+
const parsed = JSON.parse(await readFile(credentialsPath(), "utf8"));
|
|
58
|
+
const value = normalizeApiKey(parsed.api_key ?? "");
|
|
59
|
+
return value || undefined;
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
async function writeConfigCredential(value) {
|
|
66
|
+
const path = credentialsPath();
|
|
67
|
+
const directory = dirname(path);
|
|
68
|
+
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
69
|
+
await chmod(directory, 0o700);
|
|
70
|
+
const temporary = `${path}.${process.pid}.tmp`;
|
|
71
|
+
await writeFile(temporary, `${JSON.stringify({ api_key: value }, null, 2)}\n`, { mode: 0o600 });
|
|
72
|
+
await rename(temporary, path);
|
|
73
|
+
await chmod(path, 0o600);
|
|
74
|
+
}
|
|
75
|
+
export async function resolveApiKey() {
|
|
76
|
+
const environmentValue = normalizeApiKey(process.env.RETELL_API_KEY ?? "");
|
|
77
|
+
if (environmentValue)
|
|
78
|
+
return { value: environmentValue, source: "env" };
|
|
79
|
+
const keyringValue = await readKeyring();
|
|
80
|
+
if (keyringValue)
|
|
81
|
+
return { value: keyringValue, source: "keyring" };
|
|
82
|
+
const configValue = await readConfigCredential();
|
|
83
|
+
if (configValue)
|
|
84
|
+
return { value: configValue, source: "config" };
|
|
85
|
+
return { source: "unset" };
|
|
86
|
+
}
|
|
87
|
+
export async function storeApiKey(input) {
|
|
88
|
+
const value = normalizeApiKey(input);
|
|
89
|
+
if (!value)
|
|
90
|
+
throw new Error("API key cannot be empty");
|
|
91
|
+
if (await writeKeyring(value)) {
|
|
92
|
+
await rm(credentialsPath(), { force: true });
|
|
93
|
+
return "keyring";
|
|
94
|
+
}
|
|
95
|
+
await writeConfigCredential(value);
|
|
96
|
+
return "config";
|
|
97
|
+
}
|
|
98
|
+
export async function clearApiKey() {
|
|
99
|
+
await deleteKeyring();
|
|
100
|
+
await rm(credentialsPath(), { force: true });
|
|
101
|
+
return (await resolveApiKey()).source;
|
|
102
|
+
}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { fieldNameToFlag } from "./naming.js";
|
|
2
|
+
import { isSensitiveName } from "./security.js";
|
|
3
|
+
function shortDescription(description) {
|
|
4
|
+
return description.replace(/\s+/g, " ").split(/(?<=[.!?])\s/)[0].trim();
|
|
5
|
+
}
|
|
6
|
+
function displayDefault(value) {
|
|
7
|
+
return typeof value === "string" ? value : JSON.stringify(value);
|
|
8
|
+
}
|
|
9
|
+
function bodyFieldType(type) {
|
|
10
|
+
return ["object", "array", "oneOf", "anyOf", "unknown"].includes(type) ? "<json>" : `<${type}>`;
|
|
11
|
+
}
|
|
12
|
+
function shellValue(value) {
|
|
13
|
+
const rendered = typeof value === "string" ? value : JSON.stringify(value);
|
|
14
|
+
return /^[A-Za-z0-9_./:+-]+$/.test(rendered) ? rendered : `'${rendered.replaceAll("'", "'\\''")}'`;
|
|
15
|
+
}
|
|
16
|
+
function multipartSample(schema) {
|
|
17
|
+
if (schema.enum?.length)
|
|
18
|
+
return schema.enum[0];
|
|
19
|
+
if (schema.type === "boolean")
|
|
20
|
+
return true;
|
|
21
|
+
if (schema.type === "integer" || schema.type === "number")
|
|
22
|
+
return 1;
|
|
23
|
+
if (schema.type === "object") {
|
|
24
|
+
const required = new Set(schema.required ?? []);
|
|
25
|
+
const properties = Object.entries(schema.properties ?? {}).filter(([name]) => required.has(name));
|
|
26
|
+
return Object.fromEntries(properties.map(([name, property]) => [name, multipartSample(property)]));
|
|
27
|
+
}
|
|
28
|
+
if (schema.type === "array")
|
|
29
|
+
return schema.items ? [multipartSample(schema.items)] : [];
|
|
30
|
+
return "example";
|
|
31
|
+
}
|
|
32
|
+
function multipartValueSchema(field) {
|
|
33
|
+
return field.array && field.schema.items ? field.schema.items : field.schema;
|
|
34
|
+
}
|
|
35
|
+
function multipartOptionType(field) {
|
|
36
|
+
if (field.file)
|
|
37
|
+
return "path";
|
|
38
|
+
const valueSchema = multipartValueSchema(field);
|
|
39
|
+
return valueSchema.type === "object" || valueSchema.type === "array" ? "json" : valueSchema.type;
|
|
40
|
+
}
|
|
41
|
+
function multipartOptionDescription(field) {
|
|
42
|
+
const parts = [];
|
|
43
|
+
const valueSchema = multipartValueSchema(field);
|
|
44
|
+
if (field.file) {
|
|
45
|
+
parts.push(field.array ? "Local file path; repeat to upload multiple files." : "Local file path.");
|
|
46
|
+
}
|
|
47
|
+
else if (field.array) {
|
|
48
|
+
const item = valueSchema.type === "object" || valueSchema.type === "array" ? `JSON ${valueSchema.type}` : valueSchema.type;
|
|
49
|
+
parts.push(`Provide one ${item} per use; repeat for more.`);
|
|
50
|
+
}
|
|
51
|
+
else if (valueSchema.type === "object" || valueSchema.type === "array") {
|
|
52
|
+
parts.push(`Provide a JSON ${valueSchema.type}.`);
|
|
53
|
+
}
|
|
54
|
+
else if (valueSchema.type === "boolean") {
|
|
55
|
+
parts.push("Use without a value for true, or pass true/false.");
|
|
56
|
+
}
|
|
57
|
+
if (field.description)
|
|
58
|
+
parts.push(field.description.replace(/\s+/g, " ").trim());
|
|
59
|
+
return parts.join(" ");
|
|
60
|
+
}
|
|
61
|
+
function multipartExample(operation, positional) {
|
|
62
|
+
const fields = operation.requestBody?.multipartFields ?? [];
|
|
63
|
+
const selected = [
|
|
64
|
+
...fields.filter((field) => field.required),
|
|
65
|
+
fields.find((field) => !field.file && field.serialization === "json"),
|
|
66
|
+
fields.find((field) => field.file),
|
|
67
|
+
].filter((field) => field !== undefined)
|
|
68
|
+
.filter((field, index, all) => all.indexOf(field) === index);
|
|
69
|
+
if (!selected.length)
|
|
70
|
+
return [];
|
|
71
|
+
const command = ` retell ${operation.command}${positional.replaceAll(/[<>]/g, "")}`;
|
|
72
|
+
const options = selected.map((field) => {
|
|
73
|
+
if (field.file)
|
|
74
|
+
return `${fieldNameToFlag(field.name)} ./path/to/file`;
|
|
75
|
+
const schema = multipartValueSchema(field);
|
|
76
|
+
const sample = field.serialization === "json" ? JSON.stringify(multipartSample(schema)) : String(multipartSample(schema));
|
|
77
|
+
return `${fieldNameToFlag(field.name)} ${shellValue(sample)}`;
|
|
78
|
+
});
|
|
79
|
+
if (options.length === 1)
|
|
80
|
+
return [`${command} ${options[0]}`];
|
|
81
|
+
return [`${command} \\`, ...options.map((option, index) => ` ${option}${index < options.length - 1 ? " \\" : ""}`)];
|
|
82
|
+
}
|
|
83
|
+
export function rootHelp(operations) {
|
|
84
|
+
const width = Math.max(...operations.map((operation) => operation.command.length));
|
|
85
|
+
const commands = operations
|
|
86
|
+
.map((operation) => ` ${operation.command.padEnd(width)} ${shortDescription(operation.description)}`)
|
|
87
|
+
.join("\n");
|
|
88
|
+
return `Retell CLI\n\n` +
|
|
89
|
+
`Usage:\n retell <command> [arguments] [options]\n\n` +
|
|
90
|
+
`Authentication:\n Run 'retell auth login' or set RETELL_API_KEY before invoking an API command.\n\n` +
|
|
91
|
+
`Authentication commands:\n` +
|
|
92
|
+
` auth login Store a Retell API key\n` +
|
|
93
|
+
` auth whoami Show authentication status\n` +
|
|
94
|
+
` auth logout Clear stored credentials\n\n` +
|
|
95
|
+
`Commands:\n${commands}\n\n` +
|
|
96
|
+
`Global options:\n` +
|
|
97
|
+
` --version Show the CLI and OpenAPI catalog versions\n` +
|
|
98
|
+
` -h, --help Show help\n\n` +
|
|
99
|
+
`Input conventions:\n` +
|
|
100
|
+
` Path parameters are positional arguments. Query parameters and JSON fields use named flags.\n` +
|
|
101
|
+
` Pass nested objects and arrays as JSON strings. Multipart file options accept local paths.\n\n` +
|
|
102
|
+
`Run 'retell <command> --help' for command-specific help.\n`;
|
|
103
|
+
}
|
|
104
|
+
export function commandHelp(operation) {
|
|
105
|
+
const pathParameters = operation.parameters.filter((parameter) => parameter.in === "path");
|
|
106
|
+
const optionParameters = operation.parameters.filter((parameter) => parameter.in !== "path");
|
|
107
|
+
const positional = pathParameters.map((parameter) => ` <${parameter.name.toUpperCase()}>`).join("");
|
|
108
|
+
const lines = [
|
|
109
|
+
operation.description.trim(),
|
|
110
|
+
"",
|
|
111
|
+
"Usage:",
|
|
112
|
+
` retell ${operation.command}${positional} [options]`,
|
|
113
|
+
"",
|
|
114
|
+
`OpenAPI operation: ${operation.operationId}`,
|
|
115
|
+
`Request: ${operation.method} ${operation.path}`,
|
|
116
|
+
];
|
|
117
|
+
if (operation.deprecated)
|
|
118
|
+
lines.push("", "WARNING: This operation is deprecated.");
|
|
119
|
+
if (pathParameters.length) {
|
|
120
|
+
lines.push("", "Arguments:");
|
|
121
|
+
for (const parameter of pathParameters) {
|
|
122
|
+
lines.push(` ${parameter.name.toUpperCase().padEnd(28)} ${parameter.description ?? "Required path parameter"}`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
const requiredBodyFields = operation.requestBody?.fields?.filter((field) => field.required) ?? [];
|
|
126
|
+
if (!pathParameters.length && requiredBodyFields.length && requiredBodyFields.every((field) => field.example !== undefined && !isSensitiveName(field.name))) {
|
|
127
|
+
const flags = requiredBodyFields.map((field) => `${field.flag} ${shellValue(field.example)}`).join(" ");
|
|
128
|
+
lines.push("", "Example:", ` retell ${operation.command} ${flags}`);
|
|
129
|
+
}
|
|
130
|
+
if (operation.requestBody?.contentType === "multipart/form-data") {
|
|
131
|
+
const example = multipartExample(operation, positional);
|
|
132
|
+
if (example.length)
|
|
133
|
+
lines.push("", "Example:", ...example);
|
|
134
|
+
}
|
|
135
|
+
if (optionParameters.length) {
|
|
136
|
+
lines.push("", "Request options:");
|
|
137
|
+
for (const parameter of optionParameters) {
|
|
138
|
+
const type = parameter.array ? `<${parameter.arrayItemType ?? "value"}>...` : parameter.type === "object" ? "<json>" : `<${parameter.type}>`;
|
|
139
|
+
const details = [parameter.required ? "Required." : "Optional.", parameter.description, parameter.in !== "query" ? `${parameter.in[0].toUpperCase()}${parameter.in.slice(1)} parameter.` : undefined];
|
|
140
|
+
if (parameter.enum?.length)
|
|
141
|
+
details.push(`Allowed: ${parameter.enum.map(displayDefault).join(", ")}`);
|
|
142
|
+
if (parameter.default !== undefined)
|
|
143
|
+
details.push(`Default: ${displayDefault(parameter.default)}`);
|
|
144
|
+
lines.push(` ${(parameter.flag + " " + type).padEnd(40)} ${details.filter(Boolean).join(" ")}`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
if (operation.requestBody?.contentType === "application/json") {
|
|
148
|
+
const required = operation.requestBody.required ? "required" : "optional";
|
|
149
|
+
const bodyDetails = [
|
|
150
|
+
operation.requestBody.schemaName ? `Schema: ${operation.requestBody.schemaName}.` : undefined,
|
|
151
|
+
operation.requestBody.propertyCount ? `${operation.requestBody.propertyCount} top-level fields.` : undefined,
|
|
152
|
+
operation.requestBody.requiredFields?.length ? `Required: ${operation.requestBody.requiredFields.join(", ")}.` : undefined,
|
|
153
|
+
].filter(Boolean);
|
|
154
|
+
const simple = operation.requestBody.fields?.find((field) => !["object", "array", "oneOf", "anyOf", "unknown"].includes(field.type));
|
|
155
|
+
const structured = operation.requestBody.fields?.find((field) => ["object", "array", "oneOf", "anyOf", "unknown"].includes(field.type));
|
|
156
|
+
lines.push("", "Input methods:");
|
|
157
|
+
lines.push(` ${"Named options".padEnd(20)} Pass individual fields with the options listed below.`);
|
|
158
|
+
if (simple)
|
|
159
|
+
lines.push(` ${"Scalar example".padEnd(20)} ${simple.flag} VALUE`);
|
|
160
|
+
if (structured)
|
|
161
|
+
lines.push(` ${"JSON example".padEnd(20)} ${structured.flag} '${structured.type === "array" ? "[\"value\"]" : "{\"key\":\"value\"}"}'`);
|
|
162
|
+
lines.push(` ${"--body <FILE|->".padEnd(20)} Provide the complete JSON payload (${required}); use '-' for stdin.`);
|
|
163
|
+
if (bodyDetails.length)
|
|
164
|
+
lines.push(` ${"".padEnd(20)} ${bodyDetails.join(" ")}`);
|
|
165
|
+
if (operation.requestBody.requiredFields?.length)
|
|
166
|
+
lines.push(` ${"".padEnd(20)} Missing required fields are prompted in an interactive terminal.`);
|
|
167
|
+
}
|
|
168
|
+
if (operation.requestBody?.contentType === "multipart/form-data") {
|
|
169
|
+
lines.push("", "Input methods:", ` ${"Named options".padEnd(20)} Pass individual fields with the options listed below.`, ` ${"File options".padEnd(20)} Pass a local path; the CLI reads and uploads that file.`, ` ${"Repeated options".padEnd(20)} Repeat an option to provide multiple values or files.`, ` ${"JSON values".padEnd(20)} Wrap JSON in single quotes so the shell passes it unchanged.`);
|
|
170
|
+
}
|
|
171
|
+
const inputFields = operation.requestBody?.contentType === "application/json"
|
|
172
|
+
? operation.requestBody.fields ?? []
|
|
173
|
+
: operation.requestBody?.contentType === "multipart/form-data"
|
|
174
|
+
? operation.requestBody.multipartFields ?? []
|
|
175
|
+
: [];
|
|
176
|
+
for (const [heading, required] of [["Required input options:", true], ["Optional input options:", false]]) {
|
|
177
|
+
const fields = inputFields.filter((field) => field.required === required);
|
|
178
|
+
if (!fields.length)
|
|
179
|
+
continue;
|
|
180
|
+
lines.push("", heading);
|
|
181
|
+
for (const field of fields) {
|
|
182
|
+
if ("file" in field) {
|
|
183
|
+
const label = `${fieldNameToFlag(field.name)} <${multipartOptionType(field)}>`;
|
|
184
|
+
lines.push(` ${label.padEnd(40)} ${multipartOptionDescription(field)}`.trimEnd());
|
|
185
|
+
if (field.enum?.length)
|
|
186
|
+
lines.push(` ${"".padEnd(40)} Allowed values: ${field.enum.map(displayDefault).join(", ")}`);
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
const label = `${field.flag} ${bodyFieldType(field.type)}`;
|
|
190
|
+
lines.push(` ${label.padEnd(40)} ${field.description?.replace(/\s+/g, " ").trim() ?? ""}`.trimEnd());
|
|
191
|
+
if (field.enum?.length)
|
|
192
|
+
lines.push(` ${"".padEnd(40)} Allowed: ${field.enum.map(displayDefault).join(", ")}`);
|
|
193
|
+
if (field.default !== undefined)
|
|
194
|
+
lines.push(` ${"".padEnd(40)} Default: ${displayDefault(field.default)}`);
|
|
195
|
+
if (field.example !== undefined)
|
|
196
|
+
lines.push(` ${"".padEnd(40)} Example: ${displayDefault(field.example)}`);
|
|
197
|
+
if (field.nullable)
|
|
198
|
+
lines.push(` ${"".padEnd(40)} Accepts: null`);
|
|
199
|
+
if (isSensitiveName(field.name))
|
|
200
|
+
lines.push(` ${"".padEnd(40)} Sensitive: prefer JSON through stdin with --body - to avoid shell history.`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
lines.push("", "Common options:");
|
|
205
|
+
lines.push(` ${"--output <FORMAT>".padEnd(28)} json (default), table, or jsonl`, ` ${"--base-url <URL>".padEnd(28)} Override the API base URL; credentials are sent to this URL`, ` ${"--dry-run".padEnd(28)} Print the request without sending it`, ` ${"-h, --help".padEnd(28)} Show command help`);
|
|
206
|
+
if (operation.pagination) {
|
|
207
|
+
lines.push(`Pagination: items=${operation.pagination.itemsField}, cursor=${operation.pagination.cursorField}, has-more=${operation.pagination.hasMoreField}.`);
|
|
208
|
+
}
|
|
209
|
+
return `${lines.join("\n")}\n`;
|
|
210
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
const DEFAULT_TIMEOUT_MS = 60_000;
|
|
2
|
+
const MULTIPART_TIMEOUT_MS = 300_000;
|
|
3
|
+
export function requestTimeoutMs(operation) {
|
|
4
|
+
return operation.requestBody?.contentType === "multipart/form-data"
|
|
5
|
+
? MULTIPART_TIMEOUT_MS
|
|
6
|
+
: DEFAULT_TIMEOUT_MS;
|
|
7
|
+
}
|
|
8
|
+
function errorDetail(error) {
|
|
9
|
+
if (!(error instanceof Error))
|
|
10
|
+
return String(error);
|
|
11
|
+
const cause = error.cause;
|
|
12
|
+
if (cause instanceof Error) {
|
|
13
|
+
const code = "code" in cause && typeof cause.code === "string" ? ` (${cause.code})` : "";
|
|
14
|
+
return `${cause.message}${code}`;
|
|
15
|
+
}
|
|
16
|
+
return error.message;
|
|
17
|
+
}
|
|
18
|
+
export async function sendRequest(operation, url, init, fetchImplementation = fetch) {
|
|
19
|
+
const timeoutMs = requestTimeoutMs(operation);
|
|
20
|
+
try {
|
|
21
|
+
return await fetchImplementation(url, {
|
|
22
|
+
...init,
|
|
23
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
if (error instanceof Error && (error.name === "TimeoutError" || error.name === "AbortError")) {
|
|
28
|
+
throw new Error(`Retell API request timed out after ${timeoutMs / 1_000} seconds`);
|
|
29
|
+
}
|
|
30
|
+
throw new Error(`Retell API network request failed: ${errorDetail(error)}`);
|
|
31
|
+
}
|
|
32
|
+
}
|