conduyt 1.65.0 → 1.66.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/README.md +20 -0
- package/dist/client.js +54 -37
- package/dist/index.js +93 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -44,6 +44,8 @@ conduyt notifications list [--page n] [--per-page 1-200] # prints { data, meta
|
|
|
44
44
|
conduyt deals list --pipeline <id> # list deals
|
|
45
45
|
conduyt deals list --health-summary --pipeline <id> # Pipeline health strip for the filtered set (totalOpenDeals, atRiskValue, checks[]); add --health-check stale --health-limit 20 for one check's deals, --health-cursor <nextCursor> to page, --health-rows-only for just the page
|
|
46
46
|
conduyt deals health --pipeline <id> --check needs_action # GET /deals/inspection: the six checks over open deals; --check adds its deals, --cursor/--offset pages
|
|
47
|
+
conduyt deals health-fix <dealId> --check stale --expected-updated-at <iso> --stage-id <uuid> # Pipeline health quick fix (PATCH /deals/{id} with checkOrigin): stale = --stage-id, unassigned = --assigned, missing_close_date = --close-date, missing_contact = --contact; 409 deal_changed / deal_check_cleared and 400 health_fix_mismatch exit 1 verbatim, never retried
|
|
48
|
+
conduyt deals health-next-action <dealId> --check needs_action --expected-updated-at <iso> --title "Call back" --idempotency-key <key> # "Set next action" (POST /tasks with checkOrigin): one non-done quick-fix task per deal, 409 deal_has_next_action / deal_changed / deal_check_cleared exit 1 verbatim
|
|
47
49
|
conduyt deals card-fields set value,contact,custom:next_contact_at # the kanban card fields (1-8, built-ins or custom:<fieldKey>); `card-fields get` reads them
|
|
48
50
|
conduyt pipelines stage-rules set <pipelineId> <stageId> --required-fields contactId,custom:loan_amount --required-documents 2 [--lost-reason-allow-other true] # what a deal needs to enter the stage; other stages untouched; `stage-rules get [pipelineId] [stageId]` reads
|
|
49
51
|
conduyt deals bulk-update --file updates.json # update up to 100 deals at once ([{id, stageId, value, ...}]); per-item results, exit 1 if any failed
|
|
@@ -93,6 +95,24 @@ Pass `--input '{"...":"..."}'` to `insights` and `--body '{"...":"..."}'` to `ap
|
|
|
93
95
|
|
|
94
96
|
All output is JSON, ready to pipe into `jq`.
|
|
95
97
|
|
|
98
|
+
## Error output grammar
|
|
99
|
+
|
|
100
|
+
A refused call (HTTP 4xx/5xx) prints one line on stderr and exits 1:
|
|
101
|
+
|
|
102
|
+
```
|
|
103
|
+
Error: Conduyt API <status>: <server message>[ [Retry-After: <N>s]][ <details>]
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
`[Retry-After: Ns]` appears only when the server sent the header (shown, never acted on: nothing is retried). `<details>` appears
|
|
107
|
+
only when the body had more than `error`: it is ONE JSON object and the FINAL token. To extract it, try each `{` from the left
|
|
108
|
+
until the rest of the line parses as JSON (a server message may itself contain a brace). Scalars (`code`, revision) come
|
|
109
|
+
first, nested objects whole; `HealthFixRefusal.row` is always whole, any other nested object too large for the bound is left
|
|
110
|
+
out and named in `_omitted`. Every HTTP refusal takes this path (JSON calls, text exports and streams alike). The settings-conflict commands merge their hint and the current settings INTO that final object (`{...details, hint, current}`), never after it. Example:
|
|
111
|
+
|
|
112
|
+
```
|
|
113
|
+
Error: Conduyt API 503: Contact merge in progress [Retry-After: 5s] {"code":"merge_in_progress"}
|
|
114
|
+
```
|
|
115
|
+
|
|
96
116
|
## License
|
|
97
117
|
|
|
98
118
|
MIT
|
package/dist/client.js
CHANGED
|
@@ -7,6 +7,55 @@ function ambiguousTimeout(err, method, path, timeoutMs, what) {
|
|
|
7
7
|
}
|
|
8
8
|
return err;
|
|
9
9
|
}
|
|
10
|
+
/** Room for the nested objects of an error body (a 409 contact_changed's `contact`, a stage-requirements list). */
|
|
11
|
+
const ERROR_DETAILS_BOUND = 1000;
|
|
12
|
+
/** Error-body keys that are the actionable state itself and are always rendered WHOLE: HealthFixRefusal.row (the deal's
|
|
13
|
+
* current health row, which a caller needs to choose again). */
|
|
14
|
+
const ALWAYS_WHOLE_KEYS = new Set(["row"]);
|
|
15
|
+
/** Everything but `error` as ONE valid JSON object: scalars first (a very long string is shortened inside its quotes), then
|
|
16
|
+
* nested objects whole while they fit ERROR_DETAILS_BOUND, plus ALWAYS_WHOLE_KEYS; one that does not fit is left out and
|
|
17
|
+
* named in `_omitted`. JSON is never cut mid-object. */
|
|
18
|
+
export function renderErrorDetails(body) {
|
|
19
|
+
const entries = Object.entries(body).filter(([k]) => k !== "error");
|
|
20
|
+
if (entries.length === 0)
|
|
21
|
+
return "";
|
|
22
|
+
const isScalar = (v) => v === null || typeof v !== "object";
|
|
23
|
+
const out = {};
|
|
24
|
+
for (const [k, v] of entries.filter(([, v]) => isScalar(v)))
|
|
25
|
+
out[k] = typeof v === "string" && v.length > ERROR_DETAILS_BOUND ? v.slice(0, ERROR_DETAILS_BOUND) + "...[shortened]" : v;
|
|
26
|
+
const omitted = [];
|
|
27
|
+
for (const [k, v] of entries.filter(([, v]) => !isScalar(v))) {
|
|
28
|
+
if (ALWAYS_WHOLE_KEYS.has(k) || JSON.stringify({ ...out, [k]: v }).length <= ERROR_DETAILS_BOUND)
|
|
29
|
+
out[k] = v;
|
|
30
|
+
else
|
|
31
|
+
omitted.push(k);
|
|
32
|
+
}
|
|
33
|
+
if (omitted.length > 0)
|
|
34
|
+
out._omitted = omitted;
|
|
35
|
+
return JSON.stringify(out);
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* The ONE place an HTTP refusal becomes an Error (request, postText and stream all use it). Grammar (stable):
|
|
39
|
+
* `Conduyt API <status>: <message>[ [Retry-After: <N>s]][ <details>]`; <details> is ONE JSON object and the FINAL token
|
|
40
|
+
* (renderErrorDetails). Retry-After is shown, never acted on. A structured body without `error` IS the details object.
|
|
41
|
+
*/
|
|
42
|
+
export function formatHttpError(res, text) {
|
|
43
|
+
const json = text ? safeParse(text) : null;
|
|
44
|
+
const ra = res.headers.get("retry-after");
|
|
45
|
+
const raN = ra === null || ra.trim() === "" ? NaN : Number(ra);
|
|
46
|
+
const raTag = Number.isFinite(raN) && raN >= 0 ? ` [Retry-After: ${raN}s]` : "";
|
|
47
|
+
const obj = json && typeof json === "object" && !Array.isArray(json) ? json : null;
|
|
48
|
+
if (obj && typeof obj.error === "string" && obj.error) {
|
|
49
|
+
const extras = renderErrorDetails(obj);
|
|
50
|
+
return new Error(`Conduyt API ${res.status}: ${obj.error}${raTag}${extras ? ` ${extras}` : ""}`);
|
|
51
|
+
}
|
|
52
|
+
// several endpoints answer the data a caller needs IN the error body (custom-field delete 409 valueCount / dependencyCount)
|
|
53
|
+
const extras = obj ? renderErrorDetails(obj) : "";
|
|
54
|
+
if (extras)
|
|
55
|
+
return new Error(`Conduyt API ${res.status}:${raTag} ${extras}`);
|
|
56
|
+
const plain = typeof json === "string" && json ? json.slice(0, 500) : "";
|
|
57
|
+
return new Error(`Conduyt API ${res.status}: ${plain || res.statusText || `HTTP ${res.status}`}${raTag}`);
|
|
58
|
+
}
|
|
10
59
|
export class ConduytClient {
|
|
11
60
|
baseUrl;
|
|
12
61
|
apiKey;
|
|
@@ -25,6 +74,7 @@ export class ConduytClient {
|
|
|
25
74
|
res = await fetch(url, {
|
|
26
75
|
method,
|
|
27
76
|
headers: {
|
|
77
|
+
...(opts?.headers ?? {}),
|
|
28
78
|
Authorization: `Bearer ${this.apiKey}`,
|
|
29
79
|
"Content-Type": "application/json",
|
|
30
80
|
},
|
|
@@ -45,31 +95,7 @@ export class ConduytClient {
|
|
|
45
95
|
}
|
|
46
96
|
const json = text ? safeParse(text) : null;
|
|
47
97
|
if (!res.ok) {
|
|
48
|
-
|
|
49
|
-
// a caller needs to proceed IN the error body (e.g. custom-field delete
|
|
50
|
-
// 409 carries valueCount/dependencyCount for the --confirm flow).
|
|
51
|
-
// Reducing those to statusText would strand the user.
|
|
52
|
-
const structured = json && typeof json === "object" && !json.error
|
|
53
|
-
? JSON.stringify(json)
|
|
54
|
-
: null;
|
|
55
|
-
let msg = json?.error || structured || res.statusText || `HTTP ${res.status}`;
|
|
56
|
-
// Bodies with BOTH an error message and structured context (e.g. the
|
|
57
|
-
// STAGE_REQUIREMENTS 422 carries lostReasonOptions) keep the context —
|
|
58
|
-
// same principle as `structured` above, bounded against huge bodies.
|
|
59
|
-
if (json && typeof json === "object" && json.error) {
|
|
60
|
-
// Scalars first (code, revision), nested objects last: a 409 contact_changed also carries the whole current
|
|
61
|
-
// `contact`, and the bound below may cut that, never the code or the current revision.
|
|
62
|
-
const isScalar = (v) => v === null || typeof v !== "object";
|
|
63
|
-
const entries = Object.entries(json).filter(([k]) => k !== "error");
|
|
64
|
-
const extras = Object.fromEntries([
|
|
65
|
-
...entries.filter(([, v]) => isScalar(v)),
|
|
66
|
-
...entries.filter(([, v]) => !isScalar(v)),
|
|
67
|
-
]);
|
|
68
|
-
if (Object.keys(extras).length > 0) {
|
|
69
|
-
msg = `${msg} ${JSON.stringify(extras).slice(0, 1000)}`;
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
throw new Error(`Conduyt API ${res.status}: ${msg}`);
|
|
98
|
+
throw formatHttpError(res, text);
|
|
73
99
|
}
|
|
74
100
|
return json;
|
|
75
101
|
}
|
|
@@ -98,14 +124,8 @@ export class ConduytClient {
|
|
|
98
124
|
body: body === undefined ? undefined : JSON.stringify(body),
|
|
99
125
|
});
|
|
100
126
|
const text = await res.text();
|
|
101
|
-
if (!res.ok)
|
|
102
|
-
|
|
103
|
-
try {
|
|
104
|
-
msg = JSON.parse(text).error ?? msg;
|
|
105
|
-
}
|
|
106
|
-
catch { /* keep the raw text */ }
|
|
107
|
-
throw new Error(`Conduyt API ${res.status}: ${msg}`);
|
|
108
|
-
}
|
|
127
|
+
if (!res.ok)
|
|
128
|
+
throw formatHttpError(res, text);
|
|
109
129
|
return { text, filename: res.headers.get("x-report-filename"), contentType: res.headers.get("content-type") };
|
|
110
130
|
}
|
|
111
131
|
// Stream `ai chat`'s text/event-stream response to stdout as clean answer
|
|
@@ -127,10 +147,7 @@ export class ConduytClient {
|
|
|
127
147
|
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
128
148
|
});
|
|
129
149
|
if (!res.ok || !res.body) {
|
|
130
|
-
|
|
131
|
-
const json = text ? safeParse(text) : null;
|
|
132
|
-
const msg = json?.error || res.statusText || `HTTP ${res.status}`;
|
|
133
|
-
throw new Error(`Conduyt API ${res.status}: ${msg}`);
|
|
150
|
+
throw formatHttpError(res, await res.text());
|
|
134
151
|
}
|
|
135
152
|
const reader = res.body.getReader();
|
|
136
153
|
const decoder = new TextDecoder();
|
package/dist/index.js
CHANGED
|
@@ -536,6 +536,73 @@ deals
|
|
|
536
536
|
const qs = usp.toString();
|
|
537
537
|
return client.get(`/api/v1/deals/inspection${qs ? `?${qs}` : ""}`);
|
|
538
538
|
}));
|
|
539
|
+
const HEALTH_FIX_REFUSALS = "Error line grammar: `Error: Conduyt API <status>: <message>[ [Retry-After: Ns]][ <one JSON object, the final token>]` (README, Error output grammar). Refusals exit 1 with the server's status, message and code printed verbatim, sent once (never retried): 409 deal_changed (the deal is no longer at --expected-updated-at; the error carries updatedAt and the deal's current health row: read it and choose again against row.updatedAt), 409 deal_check_cleared (the deal no longer fails that check), 400 health_fix_mismatch, 404";
|
|
540
|
+
deals
|
|
541
|
+
.command("health-fix <id>")
|
|
542
|
+
.description(`Pipeline health QUICK FIX for a deal row (PATCH /deals/{id} with checkOrigin). Send the check the row came from, --expected-updated-at (the deal's updatedAt as the health row carried it, REQUIRED) and ONLY that check's fix: stale = --stage-id (+ --expected-stage-id), unassigned = --assigned, missing_close_date = --close-date, missing_contact = --contact. Anything else is refused 400 health_fix_mismatch and nothing is read or written. For high_value_idle / needs_action use \`deals health-next-action\`. ${HEALTH_FIX_REFUSALS}; an uncoded 409 is a stage move by another writer, a stale --expected-stage-id or an in-flight publication`)
|
|
543
|
+
.requiredOption("--check <check>", "stale | unassigned | missing_close_date | missing_contact")
|
|
544
|
+
.requiredOption("--expected-updated-at <iso>", "the deal's updatedAt as the health row carried it (PipelineHealthDeal.updatedAt)")
|
|
545
|
+
.option("--stage-id <uuid>", "stale: the stage UUID to move the deal to")
|
|
546
|
+
.option("--expected-stage-id <uuid>", "stale: the stage the move was chosen against; 409 (nothing written) when the deal is no longer in it")
|
|
547
|
+
.option("--assigned <uuid>", "unassigned: the user (owner) UUID to assign")
|
|
548
|
+
.option("--close-date <date>", "missing_close_date: the expected close date (ISO 8601)")
|
|
549
|
+
.option("--contact <uuid>", "missing_contact: the contact UUID to link")
|
|
550
|
+
.action(run(async (client, id, opts) => {
|
|
551
|
+
const dealId = uuidOf(id, "deal id");
|
|
552
|
+
if (!["stale", "unassigned", "missing_close_date", "missing_contact"].includes(opts.check))
|
|
553
|
+
throw new Error("--check must be stale, unassigned, missing_close_date or missing_contact (for high_value_idle / needs_action use `deals health-next-action`).");
|
|
554
|
+
rejectBlankFlags([["--close-date", opts.closeDate]]);
|
|
555
|
+
const body = { checkOrigin: opts.check, expectedUpdatedAt: isoDate(opts.expectedUpdatedAt, "--expected-updated-at") };
|
|
556
|
+
if (opts.stageId !== undefined)
|
|
557
|
+
body.stageId = uuidOf(opts.stageId, "--stage-id");
|
|
558
|
+
if (opts.expectedStageId !== undefined)
|
|
559
|
+
body.expectedStageId = uuidOf(opts.expectedStageId, "--expected-stage-id");
|
|
560
|
+
if (opts.assigned !== undefined)
|
|
561
|
+
body.assignedTo = uuidOf(opts.assigned, "--assigned");
|
|
562
|
+
if (opts.closeDate !== undefined)
|
|
563
|
+
body.expectedCloseDate = opts.closeDate;
|
|
564
|
+
if (opts.contact !== undefined)
|
|
565
|
+
body.contactId = uuidOf(opts.contact, "--contact");
|
|
566
|
+
return client.patch(`/api/v1/deals/${encodeURIComponent(dealId)}`, body);
|
|
567
|
+
}));
|
|
568
|
+
deals
|
|
569
|
+
.command("health-next-action <id>")
|
|
570
|
+
.description(`Pipeline health QUICK FIX "Set next action" (POST /tasks with checkOrigin): creates a follow-up task on deal <id> from a High-value idle or Needs next action row. The task is created only while, under the deal's next-action lock, the deal is still at --expected-updated-at, still has no next action and still fails the check. Pass --idempotency-key (1-255 printable ASCII) so a retry after a lost response replays the first task; reuse it ONLY for the same request. The database allows at most one non-done quick-fix task per deal (a second is refused 409 deal_has_next_action); ordinary tasks (\`tasks create\`) are unconstrained and may coexist with or race a quick fix. ${HEALTH_FIX_REFUSALS.replace("409 deal_check_cleared", "409 deal_has_next_action (This deal already has a next action booked.), 409 deal_check_cleared")}; also an uncoded 409 (the contact was merged or deleted mid-request: retry against the surviving record) and the idempotency 409 idempotency_key_spent / 503 idempotency_claim_held / idempotency_outcome_unknown (verify the outcome before using a new key)`)
|
|
571
|
+
.requiredOption("--check <check>", "high_value_idle | needs_action")
|
|
572
|
+
.requiredOption("--expected-updated-at <iso>", "the deal's updatedAt as the health row carried it (sent as expectedDealUpdatedAt)")
|
|
573
|
+
.requiredOption("--title <title>", "task title (1-500 chars)")
|
|
574
|
+
.option("--description <text>", "description (max 5000)")
|
|
575
|
+
.option("--due <date>", "due date: YYYY-MM-DD is stored as noon UTC that day; a date-time as given")
|
|
576
|
+
.option("--priority <priority>", "low|medium|high|urgent (default medium)")
|
|
577
|
+
.option("--status <status>", "todo|in_progress|done (default todo)")
|
|
578
|
+
.option("--assigned <id>", "assignee user UUID, or @me / @owner (assigning to someone else needs tasks.assign)")
|
|
579
|
+
.option("--contact <uuid>", "link to contact UUID")
|
|
580
|
+
.option("--idempotency-key <key>", "sent as the Idempotency-Key header")
|
|
581
|
+
.action(run(async (client, id, opts) => {
|
|
582
|
+
const dealId = uuidOf(id, "deal id");
|
|
583
|
+
if (!["high_value_idle", "needs_action"].includes(opts.check))
|
|
584
|
+
throw new Error("--check must be high_value_idle or needs_action (for the other checks use `deals health-fix`).");
|
|
585
|
+
if (opts.title.trim() === "" || opts.title.length > 500)
|
|
586
|
+
throw new Error("--title must be 1 to 500 characters.");
|
|
587
|
+
if (opts.description !== undefined && opts.description.length > 5000)
|
|
588
|
+
throw new Error("--description must be at most 5000 characters.");
|
|
589
|
+
if (opts.idempotencyKey !== undefined && !/^[!-~](?:[ -~]{0,253}[!-~])?$/.test(opts.idempotencyKey))
|
|
590
|
+
throw new Error("--idempotency-key must be 1-255 printable ASCII characters, not blank.");
|
|
591
|
+
const body = { checkOrigin: opts.check, dealId, expectedDealUpdatedAt: isoDate(opts.expectedUpdatedAt, "--expected-updated-at"), title: opts.title };
|
|
592
|
+
if (opts.description !== undefined)
|
|
593
|
+
body.description = opts.description;
|
|
594
|
+
if (opts.due !== undefined)
|
|
595
|
+
body.dueDate = opts.due;
|
|
596
|
+
if (opts.priority !== undefined)
|
|
597
|
+
body.priority = opts.priority;
|
|
598
|
+
if (opts.status !== undefined)
|
|
599
|
+
body.status = opts.status;
|
|
600
|
+
if (opts.assigned !== undefined)
|
|
601
|
+
body.assignedTo = opts.assigned;
|
|
602
|
+
if (opts.contact !== undefined)
|
|
603
|
+
body.contactId = uuidOf(opts.contact, "--contact");
|
|
604
|
+
return client.post("/api/v1/tasks", body, opts.idempotencyKey !== undefined ? { headers: { "Idempotency-Key": opts.idempotencyKey } } : undefined);
|
|
605
|
+
}));
|
|
539
606
|
// (BDA board 10-08) settings.dealCardFields: the fields on each kanban deal card, in order
|
|
540
607
|
const cardFields = deals
|
|
541
608
|
.command("card-fields")
|
|
@@ -1413,7 +1480,7 @@ tasks
|
|
|
1413
1480
|
}));
|
|
1414
1481
|
tasks
|
|
1415
1482
|
.command("create")
|
|
1416
|
-
.description("Create a task (--assigned accepts a user UUID, or the magic strings @me / @owner)")
|
|
1483
|
+
.description("Create a task (--assigned accepts a user UUID, or the magic strings @me / @owner). This is the ORDINARY task route: ordinary tasks are unconstrained and may coexist with or race a Pipeline health quick-fix task; the database allows at most one non-done quick-fix task per deal (a second is refused 409 deal_has_next_action). Create the quick fix itself with `deals health-next-action`. A 409 without a code means the contact was merged or deleted mid-request: retry against the surviving record")
|
|
1417
1484
|
.option("--title <title>", "task title (required)")
|
|
1418
1485
|
.option("--description <text>", "description")
|
|
1419
1486
|
.option("--status <status>", "todo|in_progress|done (default todo)")
|
|
@@ -1447,7 +1514,7 @@ tasks
|
|
|
1447
1514
|
}));
|
|
1448
1515
|
tasks
|
|
1449
1516
|
.command("update <id>")
|
|
1450
|
-
.description("Update a task (PATCH — only the fields you pass change)")
|
|
1517
|
+
.description("Update a task (PATCH — only the fields you pass change). A 409 means nothing was written, printed verbatim with exit 1 and never retried: code deal_has_next_action (\"This deal already has a next action booked.\": re-opening or re-linking a quick-fix task would give the deal a second open one), or no code (the task changed while saving: read it again and retry)")
|
|
1451
1518
|
.option("--title <title>", "task title")
|
|
1452
1519
|
.option("--description <text>", "description")
|
|
1453
1520
|
.option("--status <status>", "todo|in_progress|done")
|
|
@@ -3651,6 +3718,28 @@ replyCapture
|
|
|
3651
3718
|
}));
|
|
3652
3719
|
// ---- lifecycle (master lead status + intake deals) ----
|
|
3653
3720
|
// ---- general settings writes (09-15) ----
|
|
3721
|
+
/** Split an error message into its prefix and its trailing details object (the grammar's final token): try each `{` from the
|
|
3722
|
+
* left until the rest parses as a JSON object. */
|
|
3723
|
+
function splitErrorDetails(message) {
|
|
3724
|
+
for (let i = message.indexOf("{"); i !== -1; i = message.indexOf("{", i + 1)) {
|
|
3725
|
+
try {
|
|
3726
|
+
const v = JSON.parse(message.slice(i));
|
|
3727
|
+
if (v && typeof v === "object" && !Array.isArray(v))
|
|
3728
|
+
return { prefix: message.slice(0, i).trimEnd(), details: v };
|
|
3729
|
+
}
|
|
3730
|
+
catch { /* next brace */ }
|
|
3731
|
+
}
|
|
3732
|
+
return { prefix: message, details: {} };
|
|
3733
|
+
}
|
|
3734
|
+
/** A settings conflict re-raised with a hint and the CURRENT settings: the original details object and the extra state are merged
|
|
3735
|
+
* into ONE final JSON object `{...details, hint, current}`, so nothing follows it. `current` is kept whole up to 20000 characters;
|
|
3736
|
+
* past that it is left out and `currentOmitted` says so (re-read with `settings get`). */
|
|
3737
|
+
function settingsConflictError(err, hint, current) {
|
|
3738
|
+
const { prefix, details } = splitErrorDetails(err.message);
|
|
3739
|
+
const whole = JSON.stringify(current ?? null);
|
|
3740
|
+
const extra = whole.length <= 20000 ? { hint, current: current ?? null } : { hint, currentOmitted: `${whole.length} characters; re-read the settings` };
|
|
3741
|
+
return new Error(`${prefix} ${JSON.stringify({ ...details, ...extra })}`);
|
|
3742
|
+
}
|
|
3654
3743
|
// PATCH /api/v1/settings refuses a general-settings change without the settingsRevision the editor loaded
|
|
3655
3744
|
// (400 expected_settings_revision_required) and answers 409 when another save landed first. Every general
|
|
3656
3745
|
// write below reads the revision first and sends it; a 409 is re-raised with the current settings so the
|
|
@@ -3670,7 +3759,7 @@ async function patchGeneralSettings(client, body) {
|
|
|
3670
3759
|
catch (err) {
|
|
3671
3760
|
if (err instanceof Error && /^Conduyt API 409/.test(err.message)) {
|
|
3672
3761
|
const current = await read();
|
|
3673
|
-
throw
|
|
3762
|
+
throw settingsConflictError(err, `the account settings changed since revision ${expectedSettingsRevision ?? "?"} was read; re-run the command`, current?.data);
|
|
3674
3763
|
}
|
|
3675
3764
|
throw err;
|
|
3676
3765
|
}
|
|
@@ -3694,7 +3783,7 @@ async function patchGeneralSettingsMerged(client, build) {
|
|
|
3694
3783
|
throw err;
|
|
3695
3784
|
if (attempt === 1) {
|
|
3696
3785
|
const current = await read();
|
|
3697
|
-
throw
|
|
3786
|
+
throw settingsConflictError(err, `the account settings changed twice while this was being saved (last revision read ${revision ?? "?"}); re-run the command`, current?.data);
|
|
3698
3787
|
}
|
|
3699
3788
|
}
|
|
3700
3789
|
}
|
package/package.json
CHANGED