retell-cli 1.0.0 → 1.0.2
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/CHANGELOG.md +362 -0
- package/README.md +196 -12
- package/dist/index.js +806 -96
- package/package.json +7 -3
package/dist/index.js
CHANGED
|
@@ -25,8 +25,8 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
25
25
|
|
|
26
26
|
// src/index.ts
|
|
27
27
|
var import_commander = require("commander");
|
|
28
|
-
var
|
|
29
|
-
var
|
|
28
|
+
var import_fs5 = require("fs");
|
|
29
|
+
var import_path6 = require("path");
|
|
30
30
|
|
|
31
31
|
// src/commands/login.ts
|
|
32
32
|
var readline = __toESM(require("readline/promises"));
|
|
@@ -133,6 +133,84 @@ function configFileExists() {
|
|
|
133
133
|
|
|
134
134
|
// src/services/output-formatter.ts
|
|
135
135
|
var import_retell_sdk = __toESM(require("retell-sdk"));
|
|
136
|
+
var DANGEROUS_KEYS = ["__proto__", "constructor", "prototype"];
|
|
137
|
+
var ARRAY_INDEX_PATTERN = /^\d+$/;
|
|
138
|
+
function validateSafeKey(key) {
|
|
139
|
+
if (DANGEROUS_KEYS.includes(key)) {
|
|
140
|
+
throw new Error(`Dangerous key detected in path: ${key}`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
function hasNestedPath(obj, path) {
|
|
144
|
+
if (obj === null || obj === void 0 || typeof obj !== "object") {
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
const keys = path.split(".");
|
|
148
|
+
for (const key of keys) {
|
|
149
|
+
validateSafeKey(key);
|
|
150
|
+
}
|
|
151
|
+
let current = obj;
|
|
152
|
+
for (const key of keys) {
|
|
153
|
+
if (current === null || current === void 0) {
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
if (Array.isArray(current)) {
|
|
157
|
+
const index = parseInt(key, 10);
|
|
158
|
+
if (isNaN(index) || index < 0 || index >= current.length) {
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
current = current[index];
|
|
162
|
+
} else if (typeof current === "object") {
|
|
163
|
+
if (!(key in current)) {
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
current = current[key];
|
|
167
|
+
} else {
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return true;
|
|
172
|
+
}
|
|
173
|
+
function getNestedValue(obj, path) {
|
|
174
|
+
if (!obj || typeof obj !== "object") {
|
|
175
|
+
return void 0;
|
|
176
|
+
}
|
|
177
|
+
const keys = path.split(".");
|
|
178
|
+
let current = obj;
|
|
179
|
+
for (const key of keys) {
|
|
180
|
+
if (current === null || current === void 0) {
|
|
181
|
+
return void 0;
|
|
182
|
+
}
|
|
183
|
+
if (Array.isArray(current)) {
|
|
184
|
+
const index = parseInt(key, 10);
|
|
185
|
+
if (isNaN(index) || index < 0 || index >= current.length) {
|
|
186
|
+
return void 0;
|
|
187
|
+
}
|
|
188
|
+
current = current[index];
|
|
189
|
+
} else if (typeof current === "object") {
|
|
190
|
+
current = current[key];
|
|
191
|
+
} else {
|
|
192
|
+
return void 0;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return current;
|
|
196
|
+
}
|
|
197
|
+
function setNestedValue(obj, path, value) {
|
|
198
|
+
const keys = path.split(".");
|
|
199
|
+
const lastKey = keys.pop();
|
|
200
|
+
validateSafeKey(lastKey);
|
|
201
|
+
let current = obj;
|
|
202
|
+
for (let i = 0; i < keys.length; i++) {
|
|
203
|
+
const key = keys[i];
|
|
204
|
+
validateSafeKey(key);
|
|
205
|
+
if (!(key in current) || typeof current[key] !== "object" || current[key] === null) {
|
|
206
|
+
const nextKey = keys[i + 1];
|
|
207
|
+
const isNextKeyArrayIndex = nextKey && ARRAY_INDEX_PATTERN.test(nextKey);
|
|
208
|
+
current[key] = isNextKeyArrayIndex ? [] : {};
|
|
209
|
+
}
|
|
210
|
+
current = current[key];
|
|
211
|
+
}
|
|
212
|
+
current[lastKey] = value;
|
|
213
|
+
}
|
|
136
214
|
function outputJson(data) {
|
|
137
215
|
console.log(JSON.stringify(data, null, 2));
|
|
138
216
|
}
|
|
@@ -178,10 +256,48 @@ function handleSdkError(error) {
|
|
|
178
256
|
outputError(message, "API_ERROR");
|
|
179
257
|
}
|
|
180
258
|
if (error instanceof Error) {
|
|
259
|
+
if (error.name === "ValidationError") {
|
|
260
|
+
outputError(error.message, "VALIDATION_ERROR");
|
|
261
|
+
}
|
|
181
262
|
outputError(error.message, "UNKNOWN_ERROR");
|
|
182
263
|
}
|
|
183
264
|
outputError("An unexpected error occurred", "UNKNOWN_ERROR");
|
|
184
265
|
}
|
|
266
|
+
function filterFields(data, fields, options = {}) {
|
|
267
|
+
if (data === null || data === void 0) {
|
|
268
|
+
return data;
|
|
269
|
+
}
|
|
270
|
+
if (Array.isArray(data)) {
|
|
271
|
+
return data.map((item) => filterFields(item, fields, options));
|
|
272
|
+
}
|
|
273
|
+
if (typeof data !== "object") {
|
|
274
|
+
return data;
|
|
275
|
+
}
|
|
276
|
+
const result = {};
|
|
277
|
+
const warnings = [];
|
|
278
|
+
for (const field of fields) {
|
|
279
|
+
if (!hasNestedPath(data, field)) {
|
|
280
|
+
const availableFields = Object.keys(data);
|
|
281
|
+
const fieldList = availableFields.length > 0 ? `Available fields: ${availableFields.join(", ")}` : "No fields available";
|
|
282
|
+
const warning = `Field '${field}' not found in data. ${fieldList}`;
|
|
283
|
+
warnings.push(warning);
|
|
284
|
+
if (options.strict) {
|
|
285
|
+
throw new Error(warning);
|
|
286
|
+
}
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
const value = getNestedValue(data, field);
|
|
290
|
+
setNestedValue(result, field, value);
|
|
291
|
+
}
|
|
292
|
+
if (warnings.length > 0 && !options.strict) {
|
|
293
|
+
if (process.env.DEBUG) {
|
|
294
|
+
console.warn(JSON.stringify({ warnings }, null, 2));
|
|
295
|
+
} else {
|
|
296
|
+
warnings.forEach((warning) => console.warn(`Warning: ${warning}`));
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
return result;
|
|
300
|
+
}
|
|
185
301
|
|
|
186
302
|
// src/commands/login.ts
|
|
187
303
|
async function loginCommand() {
|
|
@@ -259,24 +375,33 @@ async function listTranscriptsCommand(options) {
|
|
|
259
375
|
const calls = await client.call.list({
|
|
260
376
|
limit: options.limit || 50
|
|
261
377
|
});
|
|
262
|
-
|
|
378
|
+
const output = options.fields ? filterFields(calls, options.fields.split(",").map((f) => f.trim())) : calls;
|
|
379
|
+
outputJson(output);
|
|
263
380
|
} catch (error) {
|
|
264
381
|
handleSdkError(error);
|
|
265
382
|
}
|
|
266
383
|
}
|
|
267
384
|
|
|
268
385
|
// src/commands/transcripts/get.ts
|
|
269
|
-
async function getTranscriptCommand(callId) {
|
|
386
|
+
async function getTranscriptCommand(callId, options = {}) {
|
|
270
387
|
try {
|
|
271
388
|
const client = getRetellClient();
|
|
272
389
|
const call = await client.call.retrieve(callId);
|
|
273
|
-
|
|
390
|
+
const output = options.fields ? filterFields(call, options.fields.split(",").map((f) => f.trim())) : call;
|
|
391
|
+
outputJson(output);
|
|
274
392
|
} catch (error) {
|
|
275
393
|
handleSdkError(error);
|
|
276
394
|
}
|
|
277
395
|
}
|
|
278
396
|
|
|
279
397
|
// src/commands/transcripts/analyze.ts
|
|
398
|
+
var DEFAULT_LATENCY_THRESHOLD = 2e3;
|
|
399
|
+
var DEFAULT_SILENCE_THRESHOLD = 5e3;
|
|
400
|
+
function validateThreshold(threshold, name) {
|
|
401
|
+
if (!Number.isInteger(threshold) || threshold <= 0) {
|
|
402
|
+
throw new Error(`${name} must be a positive integer (got: ${threshold})`);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
280
405
|
function extractTranscriptTurns(transcriptObject) {
|
|
281
406
|
if (!transcriptObject || !Array.isArray(transcriptObject)) {
|
|
282
407
|
return [];
|
|
@@ -287,10 +412,122 @@ function extractTranscriptTurns(transcriptObject) {
|
|
|
287
412
|
word_count: turn.content ? turn.content.split(/\s+/).length : 0
|
|
288
413
|
}));
|
|
289
414
|
}
|
|
290
|
-
|
|
415
|
+
function formatTimestamp(seconds) {
|
|
416
|
+
if (!seconds || seconds < 0)
|
|
417
|
+
return "N/A";
|
|
418
|
+
const totalSeconds = Math.floor(seconds);
|
|
419
|
+
const mins = Math.floor(totalSeconds / 60);
|
|
420
|
+
const secs = totalSeconds % 60;
|
|
421
|
+
const hrs = Math.floor(mins / 60);
|
|
422
|
+
const displayMins = mins % 60;
|
|
423
|
+
if (hrs > 0) {
|
|
424
|
+
return `${hrs.toString().padStart(2, "0")}:${displayMins.toString().padStart(2, "0")}:${secs.toString().padStart(2, "0")}`;
|
|
425
|
+
}
|
|
426
|
+
return `${displayMins.toString().padStart(2, "0")}:${secs.toString().padStart(2, "0")}`;
|
|
427
|
+
}
|
|
428
|
+
function detectLatencySpikes(call, config) {
|
|
429
|
+
const hotspots = [];
|
|
430
|
+
if (call.latency?.e2e?.p90 && call.latency.e2e.p90 > config.latencyThreshold) {
|
|
431
|
+
hotspots.push({
|
|
432
|
+
turn_index: -1,
|
|
433
|
+
timestamp: "N/A",
|
|
434
|
+
issue_type: "latency_spike",
|
|
435
|
+
metrics: {
|
|
436
|
+
latency_p90_e2e: call.latency.e2e.p90,
|
|
437
|
+
latency_p90_llm: call.latency?.llm?.p90 || 0,
|
|
438
|
+
latency_p90_tts: call.latency?.tts?.p90 || 0
|
|
439
|
+
}
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
return hotspots;
|
|
443
|
+
}
|
|
444
|
+
function detectLongSilences(call, config) {
|
|
445
|
+
const hotspots = [];
|
|
446
|
+
const transcript = call.transcript_object || [];
|
|
447
|
+
for (let i = 1; i < transcript.length; i++) {
|
|
448
|
+
const prevTurn = transcript[i - 1];
|
|
449
|
+
const currTurn = transcript[i];
|
|
450
|
+
const prevWords = prevTurn.words || [];
|
|
451
|
+
const currWords = currTurn.words || [];
|
|
452
|
+
if (prevWords.length === 0 || currWords.length === 0)
|
|
453
|
+
continue;
|
|
454
|
+
const prevEnd = prevWords[prevWords.length - 1]?.end;
|
|
455
|
+
const currStart = currWords[0]?.start;
|
|
456
|
+
if (prevEnd === void 0 || currStart === void 0)
|
|
457
|
+
continue;
|
|
458
|
+
const gapMs = (currStart - prevEnd) * 1e3;
|
|
459
|
+
if (gapMs > config.silenceThreshold) {
|
|
460
|
+
hotspots.push({
|
|
461
|
+
turn_index: i,
|
|
462
|
+
timestamp: formatTimestamp(currStart),
|
|
463
|
+
issue_type: "long_silence",
|
|
464
|
+
user_utterance: prevTurn.role === "user" ? prevTurn.content : currTurn.content,
|
|
465
|
+
agent_utterance: currTurn.role === "agent" ? currTurn.content : prevTurn.content,
|
|
466
|
+
metrics: {
|
|
467
|
+
silence_duration_ms: Math.round(gapMs)
|
|
468
|
+
}
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
return hotspots;
|
|
473
|
+
}
|
|
474
|
+
function detectSentimentIssues(call) {
|
|
475
|
+
const hotspots = [];
|
|
476
|
+
if (call.call_analysis?.user_sentiment === "Negative") {
|
|
477
|
+
hotspots.push({
|
|
478
|
+
turn_index: -1,
|
|
479
|
+
timestamp: "N/A",
|
|
480
|
+
issue_type: "sentiment",
|
|
481
|
+
metrics: {
|
|
482
|
+
sentiment: call.call_analysis.user_sentiment
|
|
483
|
+
}
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
return hotspots;
|
|
487
|
+
}
|
|
488
|
+
function detectAllHotspots(call, config) {
|
|
489
|
+
const hotspots = [
|
|
490
|
+
...detectLatencySpikes(call, config),
|
|
491
|
+
...detectLongSilences(call, config),
|
|
492
|
+
...detectSentimentIssues(call)
|
|
493
|
+
];
|
|
494
|
+
return hotspots.sort((a, b) => {
|
|
495
|
+
if (a.turn_index === -1)
|
|
496
|
+
return -1;
|
|
497
|
+
if (b.turn_index === -1)
|
|
498
|
+
return 1;
|
|
499
|
+
return a.turn_index - b.turn_index;
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
async function analyzeTranscriptCommand(callId, options = {}) {
|
|
291
503
|
try {
|
|
504
|
+
if (options.hotspotsOnly) {
|
|
505
|
+
const latencyThreshold = options.latencyThreshold ?? DEFAULT_LATENCY_THRESHOLD;
|
|
506
|
+
const silenceThreshold = options.silenceThreshold ?? DEFAULT_SILENCE_THRESHOLD;
|
|
507
|
+
validateThreshold(latencyThreshold, "Latency threshold");
|
|
508
|
+
validateThreshold(silenceThreshold, "Silence threshold");
|
|
509
|
+
}
|
|
292
510
|
const client = getRetellClient();
|
|
293
511
|
const call = await client.call.retrieve(callId);
|
|
512
|
+
if (options.raw) {
|
|
513
|
+
const output2 = options.fields ? filterFields(call, options.fields.split(",").map((f) => f.trim())) : call;
|
|
514
|
+
outputJson(output2);
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
if (options.hotspotsOnly) {
|
|
518
|
+
const config = {
|
|
519
|
+
latencyThreshold: options.latencyThreshold ?? DEFAULT_LATENCY_THRESHOLD,
|
|
520
|
+
silenceThreshold: options.silenceThreshold ?? DEFAULT_SILENCE_THRESHOLD
|
|
521
|
+
};
|
|
522
|
+
const hotspots = detectAllHotspots(call, config);
|
|
523
|
+
const result = {
|
|
524
|
+
call_id: callId,
|
|
525
|
+
hotspots
|
|
526
|
+
};
|
|
527
|
+
const output2 = options.fields ? filterFields(result, options.fields.split(",").map((f) => f.trim())) : result;
|
|
528
|
+
outputJson(output2);
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
294
531
|
const analysis = {
|
|
295
532
|
call_id: callId,
|
|
296
533
|
metadata: {
|
|
@@ -324,12 +561,154 @@ async function analyzeTranscriptCommand(callId) {
|
|
|
324
561
|
breakdown: call.call_cost?.product_costs || []
|
|
325
562
|
}
|
|
326
563
|
};
|
|
327
|
-
|
|
564
|
+
const output = options.fields ? filterFields(analysis, options.fields.split(",").map((f) => f.trim())) : analysis;
|
|
565
|
+
outputJson(output);
|
|
328
566
|
} catch (error) {
|
|
329
567
|
handleSdkError(error);
|
|
330
568
|
}
|
|
331
569
|
}
|
|
332
570
|
|
|
571
|
+
// src/commands/transcripts/search.ts
|
|
572
|
+
var import_zod2 = require("zod");
|
|
573
|
+
var MAX_LIMIT = 1e3;
|
|
574
|
+
var DEFAULT_LIMIT = 50;
|
|
575
|
+
var CallSchema = import_zod2.z.object({
|
|
576
|
+
call_id: import_zod2.z.string(),
|
|
577
|
+
call_status: import_zod2.z.string().optional(),
|
|
578
|
+
agent_id: import_zod2.z.string().optional(),
|
|
579
|
+
start_timestamp: import_zod2.z.number().optional(),
|
|
580
|
+
end_timestamp: import_zod2.z.number().optional()
|
|
581
|
+
// Allow additional fields (the API may return more fields than we validate)
|
|
582
|
+
}).passthrough();
|
|
583
|
+
var CallListSchema = import_zod2.z.array(CallSchema);
|
|
584
|
+
var ValidationError = class extends Error {
|
|
585
|
+
constructor(message) {
|
|
586
|
+
super(message);
|
|
587
|
+
this.name = "ValidationError";
|
|
588
|
+
}
|
|
589
|
+
};
|
|
590
|
+
function parseDate(dateStr) {
|
|
591
|
+
if (!dateStr) {
|
|
592
|
+
throw new ValidationError("Date string cannot be empty");
|
|
593
|
+
}
|
|
594
|
+
const dateOnlyPattern = /^\d{4}-\d{2}-\d{2}$/;
|
|
595
|
+
const iso8601Pattern = /^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:Z|[+-]\d{2}:?\d{2})?)?$/;
|
|
596
|
+
if (!iso8601Pattern.test(dateStr)) {
|
|
597
|
+
throw new ValidationError(`Invalid date format: "${dateStr}". Use YYYY-MM-DD or ISO 8601 format.`);
|
|
598
|
+
}
|
|
599
|
+
const date = new Date(dateStr);
|
|
600
|
+
if (isNaN(date.getTime())) {
|
|
601
|
+
throw new ValidationError(`Invalid date format: "${dateStr}". Use YYYY-MM-DD or ISO 8601 format.`);
|
|
602
|
+
}
|
|
603
|
+
return { date, isDateOnly: dateOnlyPattern.test(dateStr) };
|
|
604
|
+
}
|
|
605
|
+
function validateSearchOptions(options) {
|
|
606
|
+
const validStatuses = ["error", "ended", "ongoing"];
|
|
607
|
+
if (options.status && !validStatuses.includes(options.status)) {
|
|
608
|
+
throw new ValidationError(
|
|
609
|
+
`Invalid status: "${options.status}". Valid options: ${validStatuses.join(", ")}`
|
|
610
|
+
);
|
|
611
|
+
}
|
|
612
|
+
const parsedDates = {};
|
|
613
|
+
if (options.since !== void 0) {
|
|
614
|
+
parsedDates.sinceDate = parseDate(options.since);
|
|
615
|
+
}
|
|
616
|
+
if (options.until !== void 0) {
|
|
617
|
+
parsedDates.untilDate = parseDate(options.until);
|
|
618
|
+
if (parsedDates.untilDate.isDateOnly) {
|
|
619
|
+
const date = new Date(parsedDates.untilDate.date.getTime());
|
|
620
|
+
date.setUTCHours(23, 59, 59, 999);
|
|
621
|
+
parsedDates.untilDate.date = date;
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
if (parsedDates.sinceDate && parsedDates.untilDate) {
|
|
625
|
+
if (parsedDates.sinceDate.date > parsedDates.untilDate.date) {
|
|
626
|
+
throw new ValidationError(
|
|
627
|
+
`Invalid date range: --since ("${options.since}") is after --until ("${options.until}")`
|
|
628
|
+
);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
if (options.limit !== void 0 && (options.limit < 1 || !Number.isInteger(options.limit))) {
|
|
632
|
+
throw new ValidationError(`Limit must be a positive integer (got: "${options.limit}")`);
|
|
633
|
+
}
|
|
634
|
+
if (options.limit !== void 0 && options.limit > MAX_LIMIT) {
|
|
635
|
+
throw new ValidationError(`Limit cannot exceed ${MAX_LIMIT} (got: "${options.limit}")`);
|
|
636
|
+
}
|
|
637
|
+
return parsedDates;
|
|
638
|
+
}
|
|
639
|
+
async function searchTranscripts(options, parsedDates) {
|
|
640
|
+
const client = getRetellClient();
|
|
641
|
+
const apiParams = {
|
|
642
|
+
limit: options.limit || DEFAULT_LIMIT,
|
|
643
|
+
sort_order: "descending"
|
|
644
|
+
// Most recent first
|
|
645
|
+
};
|
|
646
|
+
const filterCriteria = {};
|
|
647
|
+
if (options.status) {
|
|
648
|
+
filterCriteria.call_status = [options.status];
|
|
649
|
+
}
|
|
650
|
+
if (options.agentId) {
|
|
651
|
+
filterCriteria.agent_id = [options.agentId];
|
|
652
|
+
}
|
|
653
|
+
if (parsedDates.sinceDate || parsedDates.untilDate) {
|
|
654
|
+
filterCriteria.start_timestamp = {};
|
|
655
|
+
if (parsedDates.sinceDate) {
|
|
656
|
+
filterCriteria.start_timestamp.lower_threshold = parsedDates.sinceDate.date.getTime();
|
|
657
|
+
}
|
|
658
|
+
if (parsedDates.untilDate) {
|
|
659
|
+
filterCriteria.start_timestamp.upper_threshold = parsedDates.untilDate.date.getTime();
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
if (Object.keys(filterCriteria).length > 0) {
|
|
663
|
+
apiParams.filter_criteria = filterCriteria;
|
|
664
|
+
}
|
|
665
|
+
const response = await client.call.list(apiParams);
|
|
666
|
+
const validation = CallListSchema.safeParse(response);
|
|
667
|
+
if (!validation.success) {
|
|
668
|
+
console.warn("Warning: API response validation failed:", validation.error.message);
|
|
669
|
+
throw new Error(`Invalid API response format: ${validation.error.message}`);
|
|
670
|
+
}
|
|
671
|
+
const results = validation.data;
|
|
672
|
+
const requestedLimit = options.limit || DEFAULT_LIMIT;
|
|
673
|
+
const result = {
|
|
674
|
+
results,
|
|
675
|
+
total_count: results.length,
|
|
676
|
+
filters_applied: {
|
|
677
|
+
...options.status && { status: options.status },
|
|
678
|
+
...options.agentId && { agent_id: options.agentId },
|
|
679
|
+
...options.since && { since: options.since },
|
|
680
|
+
...options.until && { until: options.until },
|
|
681
|
+
limit: requestedLimit
|
|
682
|
+
}
|
|
683
|
+
};
|
|
684
|
+
if (results.length === requestedLimit && results.length > 0) {
|
|
685
|
+
console.warn(
|
|
686
|
+
`Warning: Results limited to ${results.length}. There may be additional results available. Use --limit to increase (max: ${MAX_LIMIT}).`
|
|
687
|
+
);
|
|
688
|
+
}
|
|
689
|
+
return result;
|
|
690
|
+
}
|
|
691
|
+
async function searchTranscriptsCommand(options = {}) {
|
|
692
|
+
try {
|
|
693
|
+
const parsedDates = validateSearchOptions(options);
|
|
694
|
+
const searchResult = await searchTranscripts(options, parsedDates);
|
|
695
|
+
const output = options.fields ? {
|
|
696
|
+
results: searchResult.results.map(
|
|
697
|
+
(r) => filterFields(r, options.fields.split(",").map((f) => f.trim()))
|
|
698
|
+
),
|
|
699
|
+
total_count: searchResult.total_count,
|
|
700
|
+
filters_applied: searchResult.filters_applied
|
|
701
|
+
} : searchResult;
|
|
702
|
+
outputJson(output);
|
|
703
|
+
} catch (error) {
|
|
704
|
+
if (error instanceof ValidationError) {
|
|
705
|
+
handleSdkError(error);
|
|
706
|
+
} else {
|
|
707
|
+
handleSdkError(error);
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
|
|
333
712
|
// src/commands/agents/list.ts
|
|
334
713
|
async function listAgentsCommand(options = {}) {
|
|
335
714
|
try {
|
|
@@ -361,18 +740,20 @@ async function listAgentsCommand(options = {}) {
|
|
|
361
740
|
response_engine_id
|
|
362
741
|
};
|
|
363
742
|
});
|
|
364
|
-
|
|
743
|
+
const output = options.fields ? filterFields(formatted, options.fields.split(",").map((f) => f.trim())) : formatted;
|
|
744
|
+
outputJson(output);
|
|
365
745
|
} catch (error) {
|
|
366
746
|
handleSdkError(error);
|
|
367
747
|
}
|
|
368
748
|
}
|
|
369
749
|
|
|
370
750
|
// src/commands/agents/info.ts
|
|
371
|
-
async function agentInfoCommand(agentId) {
|
|
751
|
+
async function agentInfoCommand(agentId, options = {}) {
|
|
372
752
|
try {
|
|
373
753
|
const client = getRetellClient();
|
|
374
754
|
const agent = await client.agent.retrieve(agentId);
|
|
375
|
-
|
|
755
|
+
const output = options.fields ? filterFields(agent, options.fields.split(",").map((f) => f.trim())) : agent;
|
|
756
|
+
outputJson(output);
|
|
376
757
|
} catch (error) {
|
|
377
758
|
handleSdkError(error);
|
|
378
759
|
}
|
|
@@ -396,8 +777,8 @@ async function resolvePromptSource(agentId) {
|
|
|
396
777
|
llm_id: llm.llm_id,
|
|
397
778
|
version: llm.version,
|
|
398
779
|
general_prompt: llm.general_prompt,
|
|
399
|
-
begin_message: llm.begin_message,
|
|
400
|
-
states: llm.states
|
|
780
|
+
begin_message: llm.begin_message ?? void 0,
|
|
781
|
+
states: llm.states ?? void 0
|
|
401
782
|
}
|
|
402
783
|
};
|
|
403
784
|
}
|
|
@@ -413,7 +794,7 @@ async function resolvePromptSource(agentId) {
|
|
|
413
794
|
conversation_flow_id: flow.conversation_flow_id,
|
|
414
795
|
version: flow.version,
|
|
415
796
|
global_prompt: flow.global_prompt,
|
|
416
|
-
nodes: flow.nodes
|
|
797
|
+
nodes: flow.nodes ?? []
|
|
417
798
|
}
|
|
418
799
|
};
|
|
419
800
|
}
|
|
@@ -536,76 +917,45 @@ function getFilesCreated(type, promptSource) {
|
|
|
536
917
|
}
|
|
537
918
|
|
|
538
919
|
// src/commands/prompts/update.ts
|
|
920
|
+
var import_fs4 = require("fs");
|
|
921
|
+
var import_path4 = require("path");
|
|
922
|
+
|
|
923
|
+
// src/services/prompt-loader.ts
|
|
539
924
|
var import_fs3 = require("fs");
|
|
540
925
|
var import_path3 = require("path");
|
|
541
|
-
function
|
|
542
|
-
if (
|
|
543
|
-
throw new Error(
|
|
926
|
+
function loadLocalPrompts(agentId, agentDir) {
|
|
927
|
+
if (!(0, import_fs3.existsSync)(agentDir)) {
|
|
928
|
+
throw new Error(`Prompts directory not found: ${agentDir}. Run 'retell prompts pull ${agentId}' first.`);
|
|
544
929
|
}
|
|
545
|
-
|
|
546
|
-
|
|
930
|
+
const metadataPath = (0, import_path3.join)(agentDir, "metadata.json");
|
|
931
|
+
if (!(0, import_fs3.existsSync)(metadataPath)) {
|
|
932
|
+
throw new Error(`metadata.json not found in ${agentDir}. Directory may be corrupted.`);
|
|
933
|
+
}
|
|
934
|
+
let metadata;
|
|
547
935
|
try {
|
|
548
|
-
|
|
549
|
-
const baseDir = options.source || ".retell-prompts";
|
|
550
|
-
const agentDir = (0, import_path3.join)(baseDir, agentId);
|
|
551
|
-
if (!(0, import_fs3.existsSync)(agentDir)) {
|
|
552
|
-
outputError(
|
|
553
|
-
`Prompts directory not found: ${agentDir}. Run 'retell prompts pull ${agentId}' first.`,
|
|
554
|
-
"DIRECTORY_NOT_FOUND"
|
|
555
|
-
);
|
|
556
|
-
return;
|
|
557
|
-
}
|
|
558
|
-
const metadataPath = (0, import_path3.join)(agentDir, "metadata.json");
|
|
559
|
-
if (!(0, import_fs3.existsSync)(metadataPath)) {
|
|
560
|
-
outputError(
|
|
561
|
-
`metadata.json not found in ${agentDir}. Directory may be corrupted.`,
|
|
562
|
-
"METADATA_NOT_FOUND"
|
|
563
|
-
);
|
|
564
|
-
return;
|
|
565
|
-
}
|
|
566
|
-
const metadata = JSON.parse((0, import_fs3.readFileSync)(metadataPath, "utf-8"));
|
|
567
|
-
const promptSource = await resolvePromptSource(agentId);
|
|
568
|
-
if (promptSource.type === "custom-llm") {
|
|
569
|
-
outputError(promptSource.error, "CUSTOM_LLM_NOT_SUPPORTED");
|
|
570
|
-
return;
|
|
571
|
-
}
|
|
572
|
-
if (metadata.type !== promptSource.type) {
|
|
573
|
-
outputError(
|
|
574
|
-
`Type mismatch: local files are ${metadata.type}, but agent uses ${promptSource.type}. Pull prompts again to sync.`,
|
|
575
|
-
"TYPE_MISMATCH"
|
|
576
|
-
);
|
|
577
|
-
return;
|
|
578
|
-
}
|
|
579
|
-
const client = getRetellClient();
|
|
580
|
-
if (promptSource.type === "retell-llm") {
|
|
581
|
-
const prompts2 = loadRetellLlmPrompts(agentDir);
|
|
582
|
-
await client.llm.update(promptSource.llmId, prompts2);
|
|
583
|
-
outputJson({
|
|
584
|
-
message: "Prompts updated successfully (draft version)",
|
|
585
|
-
agent_id: agentId,
|
|
586
|
-
agent_name: promptSource.agentName,
|
|
587
|
-
type: "retell-llm",
|
|
588
|
-
llm_id: promptSource.llmId,
|
|
589
|
-
note: `Run 'retell agent-publish ${agentId}' to publish changes to production`
|
|
590
|
-
});
|
|
591
|
-
} else if (promptSource.type === "conversation-flow") {
|
|
592
|
-
const prompts2 = loadConversationFlowPrompts(agentDir);
|
|
593
|
-
await client.conversationFlow.update(promptSource.flowId, prompts2);
|
|
594
|
-
outputJson({
|
|
595
|
-
message: "Prompts updated successfully (draft version)",
|
|
596
|
-
agent_id: agentId,
|
|
597
|
-
agent_name: promptSource.agentName,
|
|
598
|
-
type: "conversation-flow",
|
|
599
|
-
conversation_flow_id: promptSource.flowId,
|
|
600
|
-
note: `Run 'retell agent-publish ${agentId}' to publish changes to production`
|
|
601
|
-
});
|
|
602
|
-
}
|
|
936
|
+
metadata = JSON.parse((0, import_fs3.readFileSync)(metadataPath, "utf-8"));
|
|
603
937
|
} catch (error) {
|
|
604
938
|
if (error instanceof SyntaxError) {
|
|
605
|
-
|
|
606
|
-
return;
|
|
939
|
+
throw new Error(`Invalid JSON in metadata.json: ${error.message}`);
|
|
607
940
|
}
|
|
608
|
-
|
|
941
|
+
throw new Error(`Failed to read metadata.json: ${error.message}`);
|
|
942
|
+
}
|
|
943
|
+
if (metadata.type === "retell-llm") {
|
|
944
|
+
const prompts2 = loadRetellLlmPrompts(agentDir);
|
|
945
|
+
return {
|
|
946
|
+
type: "retell-llm",
|
|
947
|
+
metadata,
|
|
948
|
+
prompts: prompts2
|
|
949
|
+
};
|
|
950
|
+
} else if (metadata.type === "conversation-flow") {
|
|
951
|
+
const prompts2 = loadConversationFlowPrompts(agentDir);
|
|
952
|
+
return {
|
|
953
|
+
type: "conversation-flow",
|
|
954
|
+
metadata,
|
|
955
|
+
prompts: prompts2
|
|
956
|
+
};
|
|
957
|
+
} else {
|
|
958
|
+
throw new Error(`Unknown agent type in metadata: ${metadata.type}`);
|
|
609
959
|
}
|
|
610
960
|
}
|
|
611
961
|
function loadRetellLlmPrompts(agentDir) {
|
|
@@ -694,17 +1044,330 @@ function loadConversationFlowPrompts(agentDir) {
|
|
|
694
1044
|
return prompts2;
|
|
695
1045
|
}
|
|
696
1046
|
|
|
1047
|
+
// src/services/prompt-diff.ts
|
|
1048
|
+
function deepEqual(obj1, obj2, maxDepth = 100) {
|
|
1049
|
+
if (obj1 === obj2)
|
|
1050
|
+
return true;
|
|
1051
|
+
if (obj1 === null || obj2 === null)
|
|
1052
|
+
return false;
|
|
1053
|
+
if (typeof obj1 !== "object" || typeof obj2 !== "object")
|
|
1054
|
+
return false;
|
|
1055
|
+
let currentDepth = 0;
|
|
1056
|
+
const sortedStringify = (obj) => {
|
|
1057
|
+
if (++currentDepth > maxDepth) {
|
|
1058
|
+
throw new Error(
|
|
1059
|
+
`Maximum depth (${maxDepth}) exceeded during comparison. This may indicate a circular reference or extremely deep nesting.`
|
|
1060
|
+
);
|
|
1061
|
+
}
|
|
1062
|
+
if (obj === null)
|
|
1063
|
+
return "null";
|
|
1064
|
+
if (typeof obj !== "object")
|
|
1065
|
+
return JSON.stringify(obj);
|
|
1066
|
+
if (Array.isArray(obj)) {
|
|
1067
|
+
return "[" + obj.map(sortedStringify).join(",") + "]";
|
|
1068
|
+
}
|
|
1069
|
+
const keys = Object.keys(obj).sort();
|
|
1070
|
+
const pairs = keys.map((k) => JSON.stringify(k) + ":" + sortedStringify(obj[k]));
|
|
1071
|
+
return "{" + pairs.join(",") + "}";
|
|
1072
|
+
};
|
|
1073
|
+
try {
|
|
1074
|
+
return sortedStringify(obj1) === sortedStringify(obj2);
|
|
1075
|
+
} catch (error) {
|
|
1076
|
+
if (error instanceof Error && error.message.includes("Maximum depth")) {
|
|
1077
|
+
console.warn("Warning:", error.message, "Falling back to reference equality.");
|
|
1078
|
+
return obj1 === obj2;
|
|
1079
|
+
}
|
|
1080
|
+
throw error;
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
function generateDiff(agentId, localPrompts, remotePrompts) {
|
|
1084
|
+
if (remotePrompts.type === "custom-llm") {
|
|
1085
|
+
throw new Error("Cannot diff custom LLM agents");
|
|
1086
|
+
}
|
|
1087
|
+
if (localPrompts.type !== remotePrompts.type) {
|
|
1088
|
+
throw new Error(
|
|
1089
|
+
`Type mismatch: local files are ${localPrompts.type}, but agent uses ${remotePrompts.type}`
|
|
1090
|
+
);
|
|
1091
|
+
}
|
|
1092
|
+
if (localPrompts.type === "retell-llm" && remotePrompts.type === "retell-llm") {
|
|
1093
|
+
return generateRetellLlmDiff(agentId, localPrompts, remotePrompts);
|
|
1094
|
+
} else if (localPrompts.type === "conversation-flow" && remotePrompts.type === "conversation-flow") {
|
|
1095
|
+
return generateConversationFlowDiff(agentId, localPrompts, remotePrompts);
|
|
1096
|
+
}
|
|
1097
|
+
throw new Error(`Unsupported agent type: ${localPrompts.type}`);
|
|
1098
|
+
}
|
|
1099
|
+
function generateRetellLlmDiff(agentId, localPrompts, remotePrompts) {
|
|
1100
|
+
const changes = {};
|
|
1101
|
+
if (localPrompts.prompts.general_prompt !== remotePrompts.prompts.general_prompt) {
|
|
1102
|
+
changes.general_prompt = {
|
|
1103
|
+
old: remotePrompts.prompts.general_prompt,
|
|
1104
|
+
new: localPrompts.prompts.general_prompt,
|
|
1105
|
+
change_type: "modified"
|
|
1106
|
+
};
|
|
1107
|
+
}
|
|
1108
|
+
const localBeginMessage = localPrompts.prompts.begin_message || null;
|
|
1109
|
+
const remoteBeginMessage = remotePrompts.prompts.begin_message || null;
|
|
1110
|
+
if (localBeginMessage !== remoteBeginMessage) {
|
|
1111
|
+
if (localBeginMessage && !remoteBeginMessage) {
|
|
1112
|
+
changes.begin_message = {
|
|
1113
|
+
old: null,
|
|
1114
|
+
new: localBeginMessage,
|
|
1115
|
+
change_type: "added"
|
|
1116
|
+
};
|
|
1117
|
+
} else if (!localBeginMessage && remoteBeginMessage) {
|
|
1118
|
+
changes.begin_message = {
|
|
1119
|
+
old: remoteBeginMessage,
|
|
1120
|
+
new: null,
|
|
1121
|
+
change_type: "removed"
|
|
1122
|
+
};
|
|
1123
|
+
} else {
|
|
1124
|
+
changes.begin_message = {
|
|
1125
|
+
old: remoteBeginMessage,
|
|
1126
|
+
new: localBeginMessage,
|
|
1127
|
+
change_type: "modified"
|
|
1128
|
+
};
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
const localStates = localPrompts.prompts.states || [];
|
|
1132
|
+
const remoteStates = remotePrompts.prompts.states || [];
|
|
1133
|
+
const localStatesMap = new Map(localStates.map((s) => [s.name, s.state_prompt]));
|
|
1134
|
+
const remoteStatesMap = new Map(remoteStates.map((s) => [s.name, s.state_prompt]));
|
|
1135
|
+
for (const [stateName, localPrompt] of localStatesMap) {
|
|
1136
|
+
const remotePrompt = remoteStatesMap.get(stateName);
|
|
1137
|
+
const fieldKey = `states.${stateName}`;
|
|
1138
|
+
if (remotePrompt === void 0) {
|
|
1139
|
+
changes[fieldKey] = {
|
|
1140
|
+
old: null,
|
|
1141
|
+
new: localPrompt,
|
|
1142
|
+
change_type: "added"
|
|
1143
|
+
};
|
|
1144
|
+
} else if (localPrompt !== remotePrompt) {
|
|
1145
|
+
changes[fieldKey] = {
|
|
1146
|
+
old: remotePrompt,
|
|
1147
|
+
new: localPrompt,
|
|
1148
|
+
change_type: "modified"
|
|
1149
|
+
};
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
for (const [stateName, remotePrompt] of remoteStatesMap) {
|
|
1153
|
+
if (!localStatesMap.has(stateName)) {
|
|
1154
|
+
const fieldKey = `states.${stateName}`;
|
|
1155
|
+
changes[fieldKey] = {
|
|
1156
|
+
old: remotePrompt,
|
|
1157
|
+
new: null,
|
|
1158
|
+
change_type: "removed"
|
|
1159
|
+
};
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
return {
|
|
1163
|
+
agent_id: agentId,
|
|
1164
|
+
agent_type: "retell-llm",
|
|
1165
|
+
has_changes: Object.keys(changes).length > 0,
|
|
1166
|
+
changes
|
|
1167
|
+
};
|
|
1168
|
+
}
|
|
1169
|
+
function generateConversationFlowDiff(agentId, localPrompts, remotePrompts) {
|
|
1170
|
+
const changes = {};
|
|
1171
|
+
if (localPrompts.prompts.global_prompt !== remotePrompts.prompts.global_prompt) {
|
|
1172
|
+
changes.global_prompt = {
|
|
1173
|
+
old: remotePrompts.prompts.global_prompt,
|
|
1174
|
+
new: localPrompts.prompts.global_prompt,
|
|
1175
|
+
change_type: "modified"
|
|
1176
|
+
};
|
|
1177
|
+
}
|
|
1178
|
+
const localNodes = localPrompts.prompts.nodes || [];
|
|
1179
|
+
const remoteNodes = remotePrompts.prompts.nodes || [];
|
|
1180
|
+
const localNodesMap = new Map(localNodes.map((n) => [n.id, n]));
|
|
1181
|
+
const remoteNodesMap = new Map(remoteNodes.map((n) => [n.id, n]));
|
|
1182
|
+
for (const [nodeId, localNode] of localNodesMap) {
|
|
1183
|
+
const remoteNode = remoteNodesMap.get(nodeId);
|
|
1184
|
+
const fieldKey = `nodes.${nodeId}`;
|
|
1185
|
+
if (remoteNode === void 0) {
|
|
1186
|
+
changes[fieldKey] = {
|
|
1187
|
+
old: null,
|
|
1188
|
+
new: localNode,
|
|
1189
|
+
change_type: "added"
|
|
1190
|
+
};
|
|
1191
|
+
} else {
|
|
1192
|
+
if (!deepEqual(localNode, remoteNode)) {
|
|
1193
|
+
changes[fieldKey] = {
|
|
1194
|
+
old: remoteNode,
|
|
1195
|
+
new: localNode,
|
|
1196
|
+
change_type: "modified"
|
|
1197
|
+
};
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
for (const [nodeId, remoteNode] of remoteNodesMap) {
|
|
1202
|
+
if (!localNodesMap.has(nodeId)) {
|
|
1203
|
+
const fieldKey = `nodes.${nodeId}`;
|
|
1204
|
+
changes[fieldKey] = {
|
|
1205
|
+
old: remoteNode,
|
|
1206
|
+
new: null,
|
|
1207
|
+
change_type: "removed"
|
|
1208
|
+
};
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
return {
|
|
1212
|
+
agent_id: agentId,
|
|
1213
|
+
agent_type: "conversation-flow",
|
|
1214
|
+
has_changes: Object.keys(changes).length > 0,
|
|
1215
|
+
changes
|
|
1216
|
+
};
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
// src/commands/prompts/update.ts
|
|
1220
|
+
function validateAgentId2(agentId) {
|
|
1221
|
+
if (agentId.includes("..") || agentId.includes("/") || agentId.includes("\\")) {
|
|
1222
|
+
throw new Error("Invalid agent ID: cannot contain path separators or traversal sequences");
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
async function updatePromptsCommand(agentId, options) {
|
|
1226
|
+
try {
|
|
1227
|
+
validateAgentId2(agentId);
|
|
1228
|
+
const baseDir = options.source || ".retell-prompts";
|
|
1229
|
+
const agentDir = (0, import_path4.join)(baseDir, agentId);
|
|
1230
|
+
if (!(0, import_fs4.existsSync)(agentDir)) {
|
|
1231
|
+
outputError(
|
|
1232
|
+
`Prompts directory not found: ${agentDir}. Run 'retell prompts pull ${agentId}' first.`,
|
|
1233
|
+
"DIRECTORY_NOT_FOUND"
|
|
1234
|
+
);
|
|
1235
|
+
return;
|
|
1236
|
+
}
|
|
1237
|
+
const metadataPath = (0, import_path4.join)(agentDir, "metadata.json");
|
|
1238
|
+
if (!(0, import_fs4.existsSync)(metadataPath)) {
|
|
1239
|
+
outputError(
|
|
1240
|
+
`metadata.json not found in ${agentDir}. Directory may be corrupted.`,
|
|
1241
|
+
"METADATA_NOT_FOUND"
|
|
1242
|
+
);
|
|
1243
|
+
return;
|
|
1244
|
+
}
|
|
1245
|
+
const metadata = JSON.parse((0, import_fs4.readFileSync)(metadataPath, "utf-8"));
|
|
1246
|
+
const promptSource = await resolvePromptSource(agentId);
|
|
1247
|
+
if (promptSource.type === "custom-llm") {
|
|
1248
|
+
outputError(promptSource.error, "CUSTOM_LLM_NOT_SUPPORTED");
|
|
1249
|
+
return;
|
|
1250
|
+
}
|
|
1251
|
+
if (metadata.type !== promptSource.type) {
|
|
1252
|
+
outputError(
|
|
1253
|
+
`Type mismatch: local files are ${metadata.type}, but agent uses ${promptSource.type}. Pull prompts again to sync.`,
|
|
1254
|
+
"TYPE_MISMATCH"
|
|
1255
|
+
);
|
|
1256
|
+
return;
|
|
1257
|
+
}
|
|
1258
|
+
if (options.dryRun) {
|
|
1259
|
+
let localPrompts2;
|
|
1260
|
+
try {
|
|
1261
|
+
localPrompts2 = loadLocalPrompts(agentId, agentDir);
|
|
1262
|
+
} catch (error) {
|
|
1263
|
+
outputError(error.message, "LOCAL_PROMPTS_ERROR");
|
|
1264
|
+
return;
|
|
1265
|
+
}
|
|
1266
|
+
let diff;
|
|
1267
|
+
try {
|
|
1268
|
+
diff = generateDiff(agentId, localPrompts2, promptSource);
|
|
1269
|
+
} catch (error) {
|
|
1270
|
+
outputError(error.message, "DIFF_GENERATION_ERROR");
|
|
1271
|
+
return;
|
|
1272
|
+
}
|
|
1273
|
+
outputJson({
|
|
1274
|
+
message: "Dry run - no changes applied",
|
|
1275
|
+
...diff
|
|
1276
|
+
});
|
|
1277
|
+
return;
|
|
1278
|
+
}
|
|
1279
|
+
let localPrompts;
|
|
1280
|
+
try {
|
|
1281
|
+
localPrompts = loadLocalPrompts(agentId, agentDir);
|
|
1282
|
+
} catch (error) {
|
|
1283
|
+
outputError(error.message, "LOCAL_PROMPTS_ERROR");
|
|
1284
|
+
return;
|
|
1285
|
+
}
|
|
1286
|
+
const client = getRetellClient();
|
|
1287
|
+
if (promptSource.type === "retell-llm" && localPrompts.type === "retell-llm") {
|
|
1288
|
+
await client.llm.update(promptSource.llmId, localPrompts.prompts);
|
|
1289
|
+
outputJson({
|
|
1290
|
+
message: "Prompts updated successfully (draft version)",
|
|
1291
|
+
agent_id: agentId,
|
|
1292
|
+
agent_name: promptSource.agentName,
|
|
1293
|
+
type: "retell-llm",
|
|
1294
|
+
llm_id: promptSource.llmId,
|
|
1295
|
+
note: `Run 'retell agent-publish ${agentId}' to publish changes to production`
|
|
1296
|
+
});
|
|
1297
|
+
} else if (promptSource.type === "conversation-flow" && localPrompts.type === "conversation-flow") {
|
|
1298
|
+
await client.conversationFlow.update(promptSource.flowId, localPrompts.prompts);
|
|
1299
|
+
outputJson({
|
|
1300
|
+
message: "Prompts updated successfully (draft version)",
|
|
1301
|
+
agent_id: agentId,
|
|
1302
|
+
agent_name: promptSource.agentName,
|
|
1303
|
+
type: "conversation-flow",
|
|
1304
|
+
conversation_flow_id: promptSource.flowId,
|
|
1305
|
+
note: `Run 'retell agent-publish ${agentId}' to publish changes to production`
|
|
1306
|
+
});
|
|
1307
|
+
}
|
|
1308
|
+
} catch (error) {
|
|
1309
|
+
if (error instanceof SyntaxError) {
|
|
1310
|
+
outputError(`Invalid JSON in file: ${error.message}`, "INVALID_JSON");
|
|
1311
|
+
return;
|
|
1312
|
+
}
|
|
1313
|
+
handleSdkError(error);
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
// src/commands/prompts/diff.ts
|
|
1318
|
+
var import_path5 = require("path");
|
|
1319
|
+
function validateAgentId3(agentId) {
|
|
1320
|
+
if (agentId.includes("..") || agentId.includes("/") || agentId.includes("\\")) {
|
|
1321
|
+
throw new Error("Invalid agent ID: cannot contain path separators or traversal sequences");
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
async function diffPromptsCommand(agentId, options) {
|
|
1325
|
+
try {
|
|
1326
|
+
validateAgentId3(agentId);
|
|
1327
|
+
const baseDir = options.source || ".retell-prompts";
|
|
1328
|
+
const agentDir = (0, import_path5.join)(baseDir, agentId);
|
|
1329
|
+
const localPrompts = loadLocalPrompts(agentId, agentDir);
|
|
1330
|
+
const remotePrompts = await resolvePromptSource(agentId);
|
|
1331
|
+
if (remotePrompts.type === "custom-llm") {
|
|
1332
|
+
outputError(remotePrompts.error, "CUSTOM_LLM_NOT_SUPPORTED");
|
|
1333
|
+
}
|
|
1334
|
+
if (localPrompts.type !== remotePrompts.type) {
|
|
1335
|
+
outputError(
|
|
1336
|
+
`Type mismatch: local files are ${localPrompts.type}, but agent uses ${remotePrompts.type}. Pull prompts again to sync.`,
|
|
1337
|
+
"TYPE_MISMATCH"
|
|
1338
|
+
);
|
|
1339
|
+
}
|
|
1340
|
+
const diff = generateDiff(agentId, localPrompts, remotePrompts);
|
|
1341
|
+
let output = diff;
|
|
1342
|
+
if (options.fields) {
|
|
1343
|
+
const fieldList = options.fields.split(",").map((f) => f.trim());
|
|
1344
|
+
output = filterFields(diff, fieldList);
|
|
1345
|
+
}
|
|
1346
|
+
outputJson(output);
|
|
1347
|
+
} catch (error) {
|
|
1348
|
+
handleSdkError(error);
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
|
|
697
1352
|
// src/commands/agent/publish.ts
|
|
698
1353
|
async function publishAgentCommand(agentId) {
|
|
1354
|
+
const client = getRetellClient();
|
|
699
1355
|
try {
|
|
700
|
-
|
|
701
|
-
|
|
1356
|
+
await client.agent.publish(agentId);
|
|
1357
|
+
} catch (error) {
|
|
1358
|
+
if (error instanceof Error && error.message.includes("invalid json response body") && error.message.includes("Unexpected end of JSON input")) {
|
|
1359
|
+
} else {
|
|
1360
|
+
handleSdkError(error);
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
try {
|
|
1364
|
+
const agent = await client.agent.retrieve(agentId);
|
|
702
1365
|
outputJson({
|
|
703
1366
|
message: "Agent published successfully",
|
|
704
|
-
agent_id:
|
|
705
|
-
agent_name:
|
|
706
|
-
version:
|
|
707
|
-
is_published:
|
|
1367
|
+
agent_id: agent.agent_id,
|
|
1368
|
+
agent_name: agent.agent_name || "Unknown",
|
|
1369
|
+
version: agent.version || "Unknown",
|
|
1370
|
+
is_published: agent.is_published ?? true,
|
|
708
1371
|
note: "Draft version incremented and ready for new changes"
|
|
709
1372
|
});
|
|
710
1373
|
} catch (error) {
|
|
@@ -714,7 +1377,7 @@ async function publishAgentCommand(agentId) {
|
|
|
714
1377
|
|
|
715
1378
|
// src/index.ts
|
|
716
1379
|
var packageJson = JSON.parse(
|
|
717
|
-
(0,
|
|
1380
|
+
(0, import_fs5.readFileSync)((0, import_path6.join)(__dirname, "../package.json"), "utf-8")
|
|
718
1381
|
);
|
|
719
1382
|
var program = new import_commander.Command();
|
|
720
1383
|
program.name("retell").description("Retell AI CLI - Manage transcripts and agent prompts").version(packageJson.version, "-v, --version", "Display version number").helpOption("-h, --help", "Display help for command").option("--json", "Output as JSON (default)", true);
|
|
@@ -727,10 +1390,11 @@ Examples:
|
|
|
727
1390
|
await loginCommand();
|
|
728
1391
|
});
|
|
729
1392
|
var transcripts = program.command("transcripts").description("Manage call transcripts");
|
|
730
|
-
transcripts.command("list").description("List all call transcripts").option("-l, --limit <number>", "Maximum number of calls to return (default: 50)", "50").addHelpText("after", `
|
|
1393
|
+
transcripts.command("list").description("List all call transcripts").option("-l, --limit <number>", "Maximum number of calls to return (default: 50)", "50").option("--fields <fields>", "Comma-separated list of fields to return (e.g., call_id,call_status,metadata.duration)").addHelpText("after", `
|
|
731
1394
|
Examples:
|
|
732
1395
|
$ retell transcripts list
|
|
733
1396
|
$ retell transcripts list --limit 100
|
|
1397
|
+
$ retell transcripts list --fields call_id,call_status
|
|
734
1398
|
$ retell transcripts list | jq '.[] | select(.call_status == "error")'
|
|
735
1399
|
`).action(async (options) => {
|
|
736
1400
|
const limit = parseInt(options.limit, 10);
|
|
@@ -739,28 +1403,62 @@ Examples:
|
|
|
739
1403
|
process.exit(1);
|
|
740
1404
|
}
|
|
741
1405
|
await listTranscriptsCommand({
|
|
742
|
-
limit
|
|
1406
|
+
limit,
|
|
1407
|
+
fields: options.fields
|
|
743
1408
|
});
|
|
744
1409
|
});
|
|
745
|
-
transcripts.command("get <call_id>").description("Get a specific call transcript").addHelpText("after", `
|
|
1410
|
+
transcripts.command("get <call_id>").description("Get a specific call transcript").option("--fields <fields>", "Comma-separated list of fields to return (e.g., call_id,metadata.duration,analysis)").addHelpText("after", `
|
|
746
1411
|
Examples:
|
|
747
1412
|
$ retell transcripts get call_abc123
|
|
1413
|
+
$ retell transcripts get call_abc123 --fields call_id,metadata.duration
|
|
748
1414
|
$ retell transcripts get call_abc123 | jq '.transcript_object'
|
|
749
|
-
`).action(async (callId) => {
|
|
750
|
-
await getTranscriptCommand(callId
|
|
1415
|
+
`).action(async (callId, options) => {
|
|
1416
|
+
await getTranscriptCommand(callId, {
|
|
1417
|
+
fields: options.fields
|
|
1418
|
+
});
|
|
751
1419
|
});
|
|
752
|
-
transcripts.command("analyze <call_id>").description("Analyze a call transcript with performance metrics and insights").addHelpText("after", `
|
|
1420
|
+
transcripts.command("analyze <call_id>").description("Analyze a call transcript with performance metrics and insights").option("--fields <fields>", "Comma-separated list of fields to return (e.g., call_id,performance,analysis.summary)").option("--raw", "Return unmodified API response instead of enriched analysis").option("--hotspots-only", "Return only conversation hotspots/issues for troubleshooting").option("--latency-threshold <ms>", `Latency threshold in ms for hotspot detection (default: ${DEFAULT_LATENCY_THRESHOLD})`, String(DEFAULT_LATENCY_THRESHOLD)).option("--silence-threshold <ms>", `Silence threshold in ms for hotspot detection (default: ${DEFAULT_SILENCE_THRESHOLD})`, String(DEFAULT_SILENCE_THRESHOLD)).addHelpText("after", `
|
|
753
1421
|
Examples:
|
|
754
1422
|
$ retell transcripts analyze call_abc123
|
|
1423
|
+
$ retell transcripts analyze call_abc123 --fields call_id,performance
|
|
1424
|
+
$ retell transcripts analyze call_abc123 --raw
|
|
1425
|
+
$ retell transcripts analyze call_abc123 --raw --fields call_id,transcript_object
|
|
1426
|
+
$ retell transcripts analyze call_abc123 --hotspots-only
|
|
1427
|
+
$ retell transcripts analyze call_abc123 --hotspots-only --latency-threshold 1500
|
|
1428
|
+
$ retell transcripts analyze call_abc123 --hotspots-only --fields hotspots
|
|
755
1429
|
$ retell transcripts analyze call_abc123 | jq '.performance.latency_p50_ms'
|
|
756
|
-
`).action(async (callId) => {
|
|
757
|
-
await analyzeTranscriptCommand(callId
|
|
1430
|
+
`).action(async (callId, options) => {
|
|
1431
|
+
await analyzeTranscriptCommand(callId, {
|
|
1432
|
+
fields: options.fields,
|
|
1433
|
+
raw: options.raw,
|
|
1434
|
+
hotspotsOnly: options.hotspotsOnly,
|
|
1435
|
+
latencyThreshold: options.latencyThreshold ? parseInt(options.latencyThreshold) : void 0,
|
|
1436
|
+
silenceThreshold: options.silenceThreshold ? parseInt(options.silenceThreshold) : void 0
|
|
1437
|
+
});
|
|
1438
|
+
});
|
|
1439
|
+
transcripts.command("search").description("Search transcripts with advanced filtering").option("--status <status>", "Filter by call status (error, ended, ongoing)").option("--agent-id <id>", "Filter by agent ID").option("--since <date>", "Filter calls after this date (YYYY-MM-DD or ISO format)").option("--until <date>", "Filter calls before this date (YYYY-MM-DD or ISO format)").option("--limit <number>", "Maximum number of results (default: 50)", "50").option("--fields <fields>", "Comma-separated list of fields to return").addHelpText("after", `
|
|
1440
|
+
Examples:
|
|
1441
|
+
$ retell transcripts search --status error
|
|
1442
|
+
$ retell transcripts search --agent-id agent_123 --since 2025-11-01
|
|
1443
|
+
$ retell transcripts search --status error --limit 10
|
|
1444
|
+
$ retell transcripts search --status error --fields call_id,agent_id,call_status
|
|
1445
|
+
$ retell transcripts search --since 2025-11-01 --until 2025-11-15
|
|
1446
|
+
`).action(async (options) => {
|
|
1447
|
+
await searchTranscriptsCommand({
|
|
1448
|
+
status: options.status,
|
|
1449
|
+
agentId: options.agentId,
|
|
1450
|
+
since: options.since,
|
|
1451
|
+
until: options.until,
|
|
1452
|
+
limit: options.limit ? Number(options.limit) : void 0,
|
|
1453
|
+
fields: options.fields
|
|
1454
|
+
});
|
|
758
1455
|
});
|
|
759
1456
|
var agents = program.command("agents").description("Manage agents");
|
|
760
|
-
agents.command("list").description("List all agents").option("-l, --limit <number>", "Maximum number of agents to return (default: 100)", "100").addHelpText("after", `
|
|
1457
|
+
agents.command("list").description("List all agents").option("-l, --limit <number>", "Maximum number of agents to return (default: 100)", "100").option("--fields <fields>", "Comma-separated list of fields to return (e.g., agent_id,agent_name,response_engine_type)").addHelpText("after", `
|
|
761
1458
|
Examples:
|
|
762
1459
|
$ retell agents list
|
|
763
1460
|
$ retell agents list --limit 10
|
|
1461
|
+
$ retell agents list --fields agent_id,agent_name
|
|
764
1462
|
$ retell agents list | jq '.[] | select(.response_engine.type == "retell-llm")'
|
|
765
1463
|
`).action(async (options) => {
|
|
766
1464
|
const limit = parseInt(options.limit, 10);
|
|
@@ -769,15 +1467,19 @@ Examples:
|
|
|
769
1467
|
process.exit(1);
|
|
770
1468
|
}
|
|
771
1469
|
await listAgentsCommand({
|
|
772
|
-
limit
|
|
1470
|
+
limit,
|
|
1471
|
+
fields: options.fields
|
|
773
1472
|
});
|
|
774
1473
|
});
|
|
775
|
-
agents.command("info <agent_id>").description("Get detailed agent information").addHelpText("after", `
|
|
1474
|
+
agents.command("info <agent_id>").description("Get detailed agent information").option("--fields <fields>", "Comma-separated list of fields to return (e.g., agent_name,response_engine.type,voice_config)").addHelpText("after", `
|
|
776
1475
|
Examples:
|
|
777
1476
|
$ retell agents info agent_123abc
|
|
1477
|
+
$ retell agents info agent_123abc --fields agent_name,response_engine.type
|
|
778
1478
|
$ retell agents info agent_123abc | jq '.response_engine.type'
|
|
779
|
-
`).action(async (agentId) => {
|
|
780
|
-
await agentInfoCommand(agentId
|
|
1479
|
+
`).action(async (agentId, options) => {
|
|
1480
|
+
await agentInfoCommand(agentId, {
|
|
1481
|
+
fields: options.fields
|
|
1482
|
+
});
|
|
781
1483
|
});
|
|
782
1484
|
var prompts = program.command("prompts").description("Manage agent prompts");
|
|
783
1485
|
prompts.command("pull <agent_id>").description("Download agent prompts to a local file").option("-o, --output <path>", "Output file path (default: .retell-prompts/<agent_id>.json)", ".retell-prompts").addHelpText("after", `
|
|
@@ -787,6 +1489,14 @@ Examples:
|
|
|
787
1489
|
`).action(async (agentId, options) => {
|
|
788
1490
|
await pullPromptsCommand(agentId, options);
|
|
789
1491
|
});
|
|
1492
|
+
prompts.command("diff <agent_id>").description("Show differences between local and remote prompts").option("-s, --source <path>", "Source directory path (default: .retell-prompts)", ".retell-prompts").option("-f, --fields <fields>", "Comma-separated list of fields to return").addHelpText("after", `
|
|
1493
|
+
Examples:
|
|
1494
|
+
$ retell prompts diff agent_123abc
|
|
1495
|
+
$ retell prompts diff agent_123abc --source ./custom-prompts
|
|
1496
|
+
$ retell prompts diff agent_123abc --fields has_changes,changes.general_prompt
|
|
1497
|
+
`).action(async (agentId, options) => {
|
|
1498
|
+
await diffPromptsCommand(agentId, options);
|
|
1499
|
+
});
|
|
790
1500
|
prompts.command("update <agent_id>").description("Update agent prompts from a local file").option("-s, --source <path>", "Source file path (default: .retell-prompts/<agent_id>.json)", ".retell-prompts").option("--dry-run", "Preview changes without applying them", false).addHelpText("after", `
|
|
791
1501
|
Examples:
|
|
792
1502
|
$ retell prompts update agent_123abc --source my-prompts.json --dry-run
|