@aicopycoders/exodus 2026.8.1200 → 2026.8.1400
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.
|
@@ -8,7 +8,7 @@ Core — discover & run:
|
|
|
8
8
|
exodus workflow list [--json] List the brand's saved workflows
|
|
9
9
|
exodus workflow describe <workflowId|name> [--json] Inputs, prerequisites, outputs
|
|
10
10
|
exodus workflow bots [--category <cat>] [--slug <slug>] [--json] Bot catalog / one bot's spec
|
|
11
|
-
exodus workflow run <workflowId|name> [--input key=value ...] [--fill <name>] [--
|
|
11
|
+
exodus workflow run <workflowId|name> [--input key=value ...] [--fill <name>] [--auto-approve] [--wait] [--out <dir>] [--json] Run it
|
|
12
12
|
exodus workflow status [--id <runId>] [--out <dir>] [--json] Poll a run / read its outputs
|
|
13
13
|
exodus workflow export <workflowId|name> [--version <n>] [--out <file>] [--json] Dump the contract (YAML)
|
|
14
14
|
|
|
@@ -50,8 +50,6 @@ Flag notes that add agent-level guidance (everything else is in --help):
|
|
|
50
50
|
earlier upload is relayed as-is. Accepted: image PNG/JPEG/
|
|
51
51
|
WebP/GIF ≤15MB, video MP4/MOV/WebM ≤200MB, audio MP3/M4A/
|
|
52
52
|
WAV/OGG ≤50MB, document PDF/TXT/MD/DOC/DOCX ≤25MB.
|
|
53
|
-
--terminal <nodeId> (run) Repeatable. Scope the run to the upstream closure of
|
|
54
|
-
these end node(s); omit to run the whole graph.
|
|
55
53
|
--auto-approve (run) Send this ONE run off unattended: every Checkpoint it
|
|
56
54
|
reaches is approved for you, exactly as it stands, and the
|
|
57
55
|
run records that nobody looked. Without it a Checkpoint
|
|
@@ -133,7 +131,7 @@ npx @aicopycoders/exodus workflow run "Product Shots" --input hero=./photos/hero
|
|
|
133
131
|
|
|
134
132
|
A bare path is the argument (a leading `@` is accepted and ignored — a file field never text-expands). Accepted files: **image** PNG/JPEG/WebP/GIF ≤15MB · **video** MP4/MOV/WebM ≤200MB · **audio** MP3/M4A/WAV/OGG ≤50MB · **document** PDF/TXT/MD/DOC/DOCX ≤25MB. **Every file you passed is checked first — all of them — before a single byte moves**, so one bad path can't leave the others half-uploaded; you get every problem in one message and fix them in one edit. If an upload itself dies partway, the message names the asset ids that DID land so you can pass those instead of re-uploading them.
|
|
135
133
|
|
|
136
|
-
There is no file picker and no prompt: a required file input with no `--input` fails immediately, naming the flag to pass
|
|
134
|
+
There is no file picker and no prompt: a required file input with no `--input` fails immediately, naming the flag to pass. If you already uploaded the file on an earlier run, pass the asset id instead of the path and it's relayed as-is.
|
|
137
135
|
|
|
138
136
|
One edge case worth knowing: the CLI reads the workflow's input list before it uploads anything. If that read fails (the server is down or flaking) and any value you passed looks like a file path, the run stops right there rather than sending your path as literal text. Retry, or pass an already-uploaded asset id.
|
|
139
137
|
|
|
@@ -149,7 +147,7 @@ npx @aicopycoders/exodus workflow run "Launch Flow" --input brief="new cortisol
|
|
|
149
147
|
npx @aicopycoders/exodus workflow status --id <runId>
|
|
150
148
|
```
|
|
151
149
|
|
|
152
|
-
|
|
150
|
+
There is no way to run only part of a graph: a run executes the whole workflow or it doesn't start.
|
|
153
151
|
|
|
154
152
|
**Read the `Deliveries:` block first.** Each Output node on the canvas is a named, typed promise — "Final ad copy · text", "Hero image · asset" — and a finished run answers every one of them: `delivered` with the artifacts underneath, or `unfulfilled` with the reason the upstream node gave. That block is the honest scorecard; the `Outputs:` block below it is the same artifacts as one flat pile, kept for chaining. If a run says `completed` but a slot says `unfulfilled`, the workflow did NOT deliver what it promised — say so.
|
|
155
153
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type ApiResponse } from "../lib/client.js";
|
|
2
2
|
import { type PollOptions, type PollResult } from "../lib/poll.js";
|
|
3
3
|
import { LEGACY_WORKFLOW_RUN_STATUS_VALUES, type RunStatus } from "../lib/runStatus.js";
|
|
4
|
+
import { type RunDeliverySummary } from "../lib/runVerdict.js";
|
|
4
5
|
import { type Channel } from "../lib/channel.js";
|
|
5
6
|
export declare const helpText: string;
|
|
6
7
|
export type WorkflowNodeKind = "brief" | "brief-form" | "asset" | "bot" | "primer" | "image" | "image-rig" | "rig" | "storyboard" | "reference" | "scene-frames" | "video" | "voiceover" | "output" | "call" | "show-set" | "show-cast" | "show-voices" | "product-truth" | "transform" | "formatter" | "splitter" | "collector" | "checkpoint" | "prompt";
|
|
@@ -140,6 +141,7 @@ export interface WorkflowCatalog {
|
|
|
140
141
|
nodeKinds: string[];
|
|
141
142
|
briefSources: string[];
|
|
142
143
|
primerKinds: string[];
|
|
144
|
+
primerAwarenessLanes: string[];
|
|
143
145
|
imageModels: string[];
|
|
144
146
|
aspectRatios: string[];
|
|
145
147
|
imageQuantityModes: string[];
|
|
@@ -358,6 +360,7 @@ export interface WorkflowRun {
|
|
|
358
360
|
nodes: WorkflowRunNode[];
|
|
359
361
|
outputs?: WorkflowRunOutput[];
|
|
360
362
|
deliveries?: WorkflowRunDelivery[];
|
|
363
|
+
deliverySummary?: RunDeliverySummary;
|
|
361
364
|
sessions?: WorkflowRunSession[];
|
|
362
365
|
pauseReason?: WorkflowPauseReason;
|
|
363
366
|
pausedNodeId?: string;
|
|
@@ -400,7 +403,6 @@ export interface FlowResult {
|
|
|
400
403
|
}
|
|
401
404
|
interface RunFlowOptions {
|
|
402
405
|
inputs: Record<string, string>;
|
|
403
|
-
terminalNodeIds?: string[];
|
|
404
406
|
fill?: string;
|
|
405
407
|
autoApprove?: boolean;
|
|
406
408
|
imageRigOverrides?: Record<string, unknown>;
|
|
@@ -421,7 +423,7 @@ export declare const ASSET_UPLOAD_POLICY: Record<WorkflowMediaType, {
|
|
|
421
423
|
export declare const NO_DELIVERIES_WARNING: string;
|
|
422
424
|
export declare function noDeliveriesWarning(described: unknown): string | undefined;
|
|
423
425
|
export declare function serverWarningsToPrint(serverWarnings: unknown, alreadyPrinted: string[]): string[];
|
|
424
|
-
export declare function
|
|
426
|
+
export declare function rejectTerminalFlag(args: string[]): void;
|
|
425
427
|
export declare function parseAutoApproveFlag(args: string[]): boolean;
|
|
426
428
|
export declare function parseRigOverridesFlag(args: string[], readFile?: (path: string) => string): Record<string, unknown> | undefined;
|
|
427
429
|
export declare function parseFillFlag(args: string[]): string | undefined;
|
|
@@ -4,7 +4,8 @@ import nodePath from "node:path";
|
|
|
4
4
|
import { apiGet, apiGetText, apiPost, apiPostDashboard, getDashboardUrl, } from "../lib/client.js";
|
|
5
5
|
import { formatApiError } from "../lib/format.js";
|
|
6
6
|
import { pollUntilDone } from "../lib/poll.js";
|
|
7
|
-
import { normalizeRunStatus,
|
|
7
|
+
import { normalizeRunStatus, storedWorkflowStatusForms, TERMINAL_RUN_STATUSES, } from "../lib/runStatus.js";
|
|
8
|
+
import { runVerdict } from "../lib/runVerdict.js";
|
|
8
9
|
import { workflowToYaml, parseWorkflowText } from "../lib/workflowText.js";
|
|
9
10
|
import { missingRouteLine } from "../lib/route-support.js";
|
|
10
11
|
import { getChannel } from "../lib/channel.js";
|
|
@@ -18,7 +19,7 @@ Usage:
|
|
|
18
19
|
exodus workflow templates [list] [--json]
|
|
19
20
|
exodus workflow templates export <key> [--out <file>] [--json]
|
|
20
21
|
exodus workflow schema [--kind <kind>] [--face <face>] [--json]
|
|
21
|
-
exodus workflow run <workflowId|name> [--fill <name>] [--input key=value ...] [--input <fileField>=<path> ...] [--
|
|
22
|
+
exodus workflow run <workflowId|name> [--fill <name>] [--input key=value ...] [--input <fileField>=<path> ...] [--rig-overrides <json|@file>] [--auto-approve] [--wait] [--out <dir>] [--json]
|
|
22
23
|
exodus workflow status [--id <runId>] [--out <dir>] [--json]
|
|
23
24
|
exodus workflow versions <workflowId|name> [--json]
|
|
24
25
|
exodus workflow export <workflowId|name> [--version <n>] [--out <file>] [--json]
|
|
@@ -66,10 +67,6 @@ Flags:
|
|
|
66
67
|
lock. Names are exact (case-sensitive); an unknown one
|
|
67
68
|
is rejected before the run starts. Fills are created and
|
|
68
69
|
named in the dashboard's run dialog.
|
|
69
|
-
--terminal <nodeId> (run) Repeatable. Scope the run to the upstream closure
|
|
70
|
-
of these end node(s) — only nodes feeding a picked
|
|
71
|
-
terminal execute; the rest are recorded out-of-scope.
|
|
72
|
-
Omit to run the whole graph.
|
|
73
70
|
--rig-overrides <json> (run, triggers fire) Change what an Image Rig box fires
|
|
74
71
|
for THIS ONE run, without editing the saved workflow —
|
|
75
72
|
a different number of images, a different meme format,
|
|
@@ -159,7 +156,6 @@ Examples:
|
|
|
159
156
|
exodus workflow run "Product Shots" --input hero=https://example.com/hero.png
|
|
160
157
|
exodus workflow run "Launch Flow" --fill "Weekly promo" --wait
|
|
161
158
|
exodus workflow run "Launch Flow" --fill "Weekly promo" --input brief="new offer"
|
|
162
|
-
exodus workflow run "Launch Flow" --terminal bot-3 --terminal image-2
|
|
163
159
|
exodus workflow run "Launch Flow" --auto-approve --wait
|
|
164
160
|
exodus workflow run "Product Shots" --rig-overrides '{"rig_1":{"lines":{"line_1":{"count":3}}}}'
|
|
165
161
|
exodus workflow run "Product Shots" --rig-overrides @rig.json --wait
|
|
@@ -429,17 +425,6 @@ function gateParkedNodeId(status, pauseReason, pausedNodeId) {
|
|
|
429
425
|
}
|
|
430
426
|
return undefined;
|
|
431
427
|
}
|
|
432
|
-
function runVerdict(run) {
|
|
433
|
-
const presented = workflowRunPresentation(run.status, run.pauseReason);
|
|
434
|
-
if ((presented.status === "succeeded" || presented.status === "succeeded-with-warnings") &&
|
|
435
|
-
(run.counts?.failed ?? 0) > 0 &&
|
|
436
|
-
run.deliveries !== undefined &&
|
|
437
|
-
run.deliveries.length > 0 &&
|
|
438
|
-
!run.deliveries.some((d) => d.status === "delivered")) {
|
|
439
|
-
return `${RUN_STATUS_LABELS.failed} — nothing delivered`;
|
|
440
|
-
}
|
|
441
|
-
return presented.detail ? `${presented.label} — ${presented.detail}` : presented.label;
|
|
442
|
-
}
|
|
443
428
|
const RETIRED_GATE_VERB_POINTER = "The Gate node retired in 2.0 — runs now pause at a Checkpoint box on the canvas. " +
|
|
444
429
|
"Use: exodus workflow checkpoint <runId> [approve|edit|retry|cancel]";
|
|
445
430
|
export function formatPauseNotice(pauseReason, runId, dashboardUrl) {
|
|
@@ -732,7 +717,7 @@ export function serverWarningsToPrint(serverWarnings, alreadyPrinted) {
|
|
|
732
717
|
}
|
|
733
718
|
return out;
|
|
734
719
|
}
|
|
735
|
-
async function prepareRunInputs(workflowId, raw, deps, note
|
|
720
|
+
async function prepareRunInputs(workflowId, raw, deps, note) {
|
|
736
721
|
const described = await deps.get(`${DESCRIBE_PATH}?id=${encodeURIComponent(workflowId)}`);
|
|
737
722
|
if (!described.ok) {
|
|
738
723
|
const fileish = Object.keys(raw).filter((key) => looksLikeFileArgument(raw[key], deps));
|
|
@@ -761,16 +746,14 @@ async function prepareRunInputs(workflowId, raw, deps, note, scoped) {
|
|
|
761
746
|
if (descriptor?.type === "multi-select")
|
|
762
747
|
multiFields.add(descriptor.fieldName);
|
|
763
748
|
}
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
(first.assetType ? ` (${acceptedLabel(first.assetType)})` : ""));
|
|
773
|
-
}
|
|
749
|
+
const missing = [...assetFields.values()].filter((d) => d.required && (raw[d.fieldName] ?? "").trim() === "");
|
|
750
|
+
if (missing.length > 0) {
|
|
751
|
+
const names = missing.map((d) => d.fieldName).join(", ");
|
|
752
|
+
const first = missing[0];
|
|
753
|
+
throw new Error(`Missing required file input(s): ${names}. Pass each one as a local file ` +
|
|
754
|
+
`or a URL — e.g. --input ${first.fieldName}=./path/to/file or ` +
|
|
755
|
+
`--input ${first.fieldName}=https://…` +
|
|
756
|
+
(first.assetType ? ` (${acceptedLabel(first.assetType)})` : ""));
|
|
774
757
|
}
|
|
775
758
|
const prepared = {};
|
|
776
759
|
const planned = [];
|
|
@@ -829,29 +812,13 @@ async function prepareRunInputs(workflowId, raw, deps, note, scoped) {
|
|
|
829
812
|
}
|
|
830
813
|
return { inputs: prepared, warnings };
|
|
831
814
|
}
|
|
832
|
-
export function
|
|
833
|
-
const
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
if (arg === "--terminal") {
|
|
838
|
-
raw = args[i + 1];
|
|
839
|
-
i++;
|
|
840
|
-
}
|
|
841
|
-
else if (arg.startsWith("--terminal=")) {
|
|
842
|
-
raw = arg.slice("--terminal=".length);
|
|
843
|
-
}
|
|
844
|
-
else {
|
|
845
|
-
continue;
|
|
815
|
+
export function rejectTerminalFlag(args) {
|
|
816
|
+
for (const arg of args) {
|
|
817
|
+
if (arg === "--terminal" || arg.startsWith("--terminal=")) {
|
|
818
|
+
throw new Error("--terminal is no longer supported: a run now executes the whole " +
|
|
819
|
+
"workflow or it doesn't start. Re-run without --terminal.");
|
|
846
820
|
}
|
|
847
|
-
if (raw === undefined)
|
|
848
|
-
throw new Error("--terminal requires a node id");
|
|
849
|
-
const id = raw.trim();
|
|
850
|
-
if (!id)
|
|
851
|
-
throw new Error("--terminal requires a node id");
|
|
852
|
-
ids.push(id);
|
|
853
821
|
}
|
|
854
|
-
return ids;
|
|
855
822
|
}
|
|
856
823
|
export function parseAutoApproveFlag(args) {
|
|
857
824
|
for (const arg of args) {
|
|
@@ -1362,6 +1329,9 @@ function formatBotVocabulary(catalog) {
|
|
|
1362
1329
|
lines.push(` node kinds: ${v.nodeKinds.join(", ")}`);
|
|
1363
1330
|
lines.push(` brief sources: ${v.briefSources.join(", ")}`);
|
|
1364
1331
|
lines.push(` primer kinds: ${v.primerKinds.join(", ")}`);
|
|
1332
|
+
if (v.primerAwarenessLanes?.length) {
|
|
1333
|
+
lines.push(` body primer awareness lanes (config.primerAwareness, body kind only): ${v.primerAwarenessLanes.join(", ")}`);
|
|
1334
|
+
}
|
|
1365
1335
|
lines.push(` image models: ${v.imageModels.join(", ")}`);
|
|
1366
1336
|
lines.push(` aspect ratios: ${v.aspectRatios.join(", ")}`);
|
|
1367
1337
|
lines.push(` custom bot: set a bot node's slug to "${catalog.customBot.slug}" and ` +
|
|
@@ -1717,7 +1687,6 @@ export async function runFlow(workflowRef, opts, deps) {
|
|
|
1717
1687
|
return resolveIdErrorResult(e, opts.json ?? false);
|
|
1718
1688
|
}
|
|
1719
1689
|
const preface = [];
|
|
1720
|
-
const scoped = (opts.terminalNodeIds?.length ?? 0) > 0;
|
|
1721
1690
|
let prepared;
|
|
1722
1691
|
try {
|
|
1723
1692
|
prepared = await prepareRunInputs(workflowId, opts.inputs, deps, (line) => {
|
|
@@ -1727,7 +1696,7 @@ export async function runFlow(workflowRef, opts, deps) {
|
|
|
1727
1696
|
opts.onProgressLine(line);
|
|
1728
1697
|
else
|
|
1729
1698
|
preface.push(line);
|
|
1730
|
-
}
|
|
1699
|
+
});
|
|
1731
1700
|
}
|
|
1732
1701
|
catch (e) {
|
|
1733
1702
|
const message = e instanceof Error ? e.message : String(e);
|
|
@@ -1749,9 +1718,6 @@ export async function runFlow(workflowRef, opts, deps) {
|
|
|
1749
1718
|
workflowId,
|
|
1750
1719
|
...(Object.keys(inputs).length > 0 ? { inputs } : {}),
|
|
1751
1720
|
...(opts.fill && opts.fill.trim() !== "" ? { fill: opts.fill.trim() } : {}),
|
|
1752
|
-
...(opts.terminalNodeIds && opts.terminalNodeIds.length > 0
|
|
1753
|
-
? { terminalNodeIds: opts.terminalNodeIds }
|
|
1754
|
-
: {}),
|
|
1755
1721
|
...(opts.autoApprove === true ? { autoApprove: true } : {}),
|
|
1756
1722
|
...(opts.imageRigOverrides ? { imageRigOverrides: opts.imageRigOverrides } : {}),
|
|
1757
1723
|
launchedVia: "cli",
|
|
@@ -2138,7 +2104,15 @@ export async function triggersSetEnabledFlow(workflowRef, n, enabled, opts, deps
|
|
|
2138
2104
|
return triggerErrorResult(res, verb, opts.json);
|
|
2139
2105
|
if (opts.json)
|
|
2140
2106
|
return { code: 0, lines: [JSON.stringify(res.data)] };
|
|
2141
|
-
|
|
2107
|
+
const lines = [`Trigger ${n} ${enabled ? "enabled" : "disabled"}.`];
|
|
2108
|
+
const warnings = res.data?.warnings;
|
|
2109
|
+
if (Array.isArray(warnings)) {
|
|
2110
|
+
for (const w of warnings) {
|
|
2111
|
+
if (typeof w === "string")
|
|
2112
|
+
lines.push(` ⚠ ${w}`);
|
|
2113
|
+
}
|
|
2114
|
+
}
|
|
2115
|
+
return { code: 0, lines };
|
|
2142
2116
|
}
|
|
2143
2117
|
export async function triggersFireFlow(workflowRef, opts, deps) {
|
|
2144
2118
|
const verb = "workflow triggers fire";
|
|
@@ -2378,7 +2352,7 @@ function asWorkflowRun(data) {
|
|
|
2378
2352
|
}
|
|
2379
2353
|
function describePark(run) {
|
|
2380
2354
|
if (normalizeRunStatus(run.status) !== "awaiting-approval") {
|
|
2381
|
-
return `status: ${
|
|
2355
|
+
return `status: ${runVerdict(run)}`;
|
|
2382
2356
|
}
|
|
2383
2357
|
switch (run.pauseReason) {
|
|
2384
2358
|
case "taste":
|
|
@@ -2872,13 +2846,12 @@ export async function run(flags) {
|
|
|
2872
2846
|
process.exit(1);
|
|
2873
2847
|
}
|
|
2874
2848
|
let inputs;
|
|
2875
|
-
let terminalNodeIds;
|
|
2876
2849
|
let fill;
|
|
2877
2850
|
let autoApprove;
|
|
2878
2851
|
let imageRigOverrides;
|
|
2879
2852
|
try {
|
|
2880
2853
|
inputs = parseRawInputFlags(process.argv.slice(3));
|
|
2881
|
-
|
|
2854
|
+
rejectTerminalFlag(process.argv.slice(3));
|
|
2882
2855
|
fill = parseFillFlag(process.argv.slice(3));
|
|
2883
2856
|
autoApprove = parseAutoApproveFlag(process.argv.slice(3));
|
|
2884
2857
|
imageRigOverrides = parseRigOverridesFlag(process.argv.slice(3), defaultDeps.readFile);
|
|
@@ -2889,7 +2862,6 @@ export async function run(flags) {
|
|
|
2889
2862
|
}
|
|
2890
2863
|
return printResult(await runFlow(workflowRef, {
|
|
2891
2864
|
inputs,
|
|
2892
|
-
terminalNodeIds,
|
|
2893
2865
|
fill,
|
|
2894
2866
|
autoApprove,
|
|
2895
2867
|
imageRigOverrides,
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export interface RunDeliverySummary {
|
|
2
|
+
total: number;
|
|
3
|
+
delivered: number;
|
|
4
|
+
}
|
|
5
|
+
export interface RunVerdictDelivery {
|
|
6
|
+
status: string;
|
|
7
|
+
}
|
|
8
|
+
export interface RunVerdictInput {
|
|
9
|
+
status?: string | null;
|
|
10
|
+
pauseReason?: string | null;
|
|
11
|
+
counts?: {
|
|
12
|
+
done?: number;
|
|
13
|
+
failed?: number;
|
|
14
|
+
skipped?: number;
|
|
15
|
+
total?: number;
|
|
16
|
+
outOfScope?: number;
|
|
17
|
+
} | null;
|
|
18
|
+
deliveries?: readonly RunVerdictDelivery[];
|
|
19
|
+
deliverySummary?: RunDeliverySummary | null;
|
|
20
|
+
}
|
|
21
|
+
export declare function summarizeDeliveries(deliveries?: readonly RunVerdictDelivery[] | null): RunDeliverySummary | undefined;
|
|
22
|
+
export declare function runVerdict(run: RunVerdictInput): string;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { RUN_STATUS_LABELS, workflowRunPresentation } from "./runStatus.js";
|
|
2
|
+
export function summarizeDeliveries(deliveries) {
|
|
3
|
+
if (deliveries === undefined || deliveries === null)
|
|
4
|
+
return undefined;
|
|
5
|
+
return {
|
|
6
|
+
total: deliveries.length,
|
|
7
|
+
delivered: deliveries.filter((d) => d.status === "delivered").length,
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
export function runVerdict(run) {
|
|
11
|
+
const presented = workflowRunPresentation(run.status, run.pauseReason);
|
|
12
|
+
const delivery = summarizeDeliveries(run.deliveries) ?? run.deliverySummary ?? undefined;
|
|
13
|
+
if ((presented.status === "succeeded" || presented.status === "succeeded-with-warnings") &&
|
|
14
|
+
(run.counts?.failed ?? 0) > 0 &&
|
|
15
|
+
delivery !== undefined &&
|
|
16
|
+
delivery.total > 0 &&
|
|
17
|
+
delivery.delivered === 0) {
|
|
18
|
+
return `${RUN_STATUS_LABELS.failed} — nothing delivered`;
|
|
19
|
+
}
|
|
20
|
+
return presented.detail ? `${presented.label} — ${presented.detail}` : presented.label;
|
|
21
|
+
}
|