@exayard/cli 0.1.0 → 0.5.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/dist/index.js CHANGED
@@ -30,12 +30,2505 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
30
30
  ));
31
31
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
32
32
 
33
+ // ../sdk/dist/_generated/operations.js
34
+ var require_operations = __commonJS({
35
+ "../sdk/dist/_generated/operations.js"(exports2) {
36
+ "use strict";
37
+ Object.defineProperty(exports2, "__esModule", { value: true });
38
+ exports2.OPERATION_METHODS = exports2.OPERATIONS = void 0;
39
+ exports2.OPERATIONS = {
40
+ add_symbol_line_item: {
41
+ method: "POST",
42
+ path: "/symbols/{id}/line-items",
43
+ tag: "Takeoff",
44
+ summary: "Add a line item to a symbol",
45
+ body: true,
46
+ response: "json",
47
+ params: [
48
+ { name: "id", in: "path", required: true, kind: "string", description: "Convex takeoff symbol ID" },
49
+ { name: "projectId", in: "body", required: true, kind: "string", description: "Convex project ID" },
50
+ { name: "lineItem", in: "body", required: true, kind: "object" }
51
+ ]
52
+ },
53
+ archive_conversation: {
54
+ method: "DELETE",
55
+ path: "/conversations/{id}",
56
+ tag: "Conversations",
57
+ summary: "Archive a conversation",
58
+ body: false,
59
+ response: "empty",
60
+ params: [
61
+ { name: "id", in: "path", required: true, kind: "string", description: "Convex conversation ID" },
62
+ { name: "organizationId", in: "query", required: false, kind: "string", description: "Convex organization ID \u2014 get it from GET /v1/me or the get_me tool (memberships[].orgId), not the organization name. Optional with a company key or an app's key: the key's company is used." }
63
+ ]
64
+ },
65
+ archive_labor_crew: {
66
+ method: "DELETE",
67
+ path: "/labor/crews/{id}",
68
+ tag: "Labor",
69
+ summary: "Archive a labor crew",
70
+ body: false,
71
+ response: "empty",
72
+ params: [
73
+ { name: "id", in: "path", required: true, kind: "string" },
74
+ { name: "organizationId", in: "query", required: false, kind: "string", description: "Convex organization ID \u2014 get it from GET /v1/me or the get_me tool (memberships[].orgId), not the organization name. Optional with a company key or an app's key: the key's company is used." }
75
+ ]
76
+ },
77
+ archive_labor_equipment: {
78
+ method: "DELETE",
79
+ path: "/labor/equipment/{id}",
80
+ tag: "Labor",
81
+ summary: "Archive a labor equipment entry",
82
+ body: false,
83
+ response: "empty",
84
+ params: [
85
+ { name: "id", in: "path", required: true, kind: "string" },
86
+ { name: "organizationId", in: "query", required: false, kind: "string", description: "Convex organization ID \u2014 get it from GET /v1/me or the get_me tool (memberships[].orgId), not the organization name. Optional with a company key or an app's key: the key's company is used." }
87
+ ]
88
+ },
89
+ archive_labor_rate: {
90
+ method: "DELETE",
91
+ path: "/labor/rates/{id}",
92
+ tag: "Labor",
93
+ summary: "Archive a labor rate",
94
+ body: false,
95
+ response: "empty",
96
+ params: [
97
+ { name: "id", in: "path", required: true, kind: "string" },
98
+ { name: "organizationId", in: "query", required: false, kind: "string", description: "Convex organization ID \u2014 get it from GET /v1/me or the get_me tool (memberships[].orgId), not the organization name. Optional with a company key or an app's key: the key's company is used." }
99
+ ]
100
+ },
101
+ archive_list: {
102
+ method: "DELETE",
103
+ path: "/lists/{id}",
104
+ tag: "Lists",
105
+ summary: "Archive (soft-delete) a list",
106
+ body: false,
107
+ response: "empty",
108
+ params: [
109
+ { name: "id", in: "path", required: true, kind: "string", description: "Convex product list ID" },
110
+ { name: "organizationId", in: "query", required: false, kind: "string", description: "Convex organization ID \u2014 get it from GET /v1/me or the get_me tool (memberships[].orgId), not the organization name. Optional with a company key or an app's key: the key's company is used." }
111
+ ]
112
+ },
113
+ archive_page: {
114
+ method: "DELETE",
115
+ path: "/pages/{id}",
116
+ tag: "Pages",
117
+ summary: "Archive (soft-delete) a page",
118
+ body: false,
119
+ response: "empty",
120
+ params: [
121
+ { name: "id", in: "path", required: true, kind: "string", description: "Convex page ID" }
122
+ ]
123
+ },
124
+ archive_product: {
125
+ method: "DELETE",
126
+ path: "/products/{id}",
127
+ tag: "Products",
128
+ summary: "Archive (soft-delete) a product",
129
+ body: false,
130
+ response: "empty",
131
+ params: [
132
+ { name: "id", in: "path", required: true, kind: "string", description: "Convex product ID" },
133
+ { name: "organizationId", in: "query", required: false, kind: "string", description: "Convex organization ID \u2014 get it from GET /v1/me or the get_me tool (memberships[].orgId), not the organization name. Optional with a company key or an app's key: the key's company is used." }
134
+ ]
135
+ },
136
+ archive_project: {
137
+ method: "DELETE",
138
+ path: "/projects/{id}",
139
+ tag: "Projects",
140
+ summary: "Archive a project",
141
+ body: false,
142
+ response: "empty",
143
+ params: [
144
+ { name: "id", in: "path", required: true, kind: "string", description: "Convex project ID" },
145
+ { name: "organizationId", in: "query", required: false, kind: "string", description: "Convex organization ID \u2014 get it from GET /v1/me or the get_me tool (memberships[].orgId), not the organization name. Optional with a company key or an app's key: the key's company is used." }
146
+ ]
147
+ },
148
+ archive_vendor: {
149
+ method: "DELETE",
150
+ path: "/vendors/{id}",
151
+ tag: "Vendors",
152
+ summary: "Archive (soft-delete) a vendor",
153
+ body: false,
154
+ response: "empty",
155
+ params: [
156
+ { name: "id", in: "path", required: true, kind: "string", description: "Convex vendor ID" },
157
+ { name: "organizationId", in: "query", required: false, kind: "string", description: "Convex organization ID \u2014 get it from GET /v1/me or the get_me tool (memberships[].orgId), not the organization name. Optional with a company key or an app's key: the key's company is used." }
158
+ ]
159
+ },
160
+ attach_file_to_project: {
161
+ method: "POST",
162
+ path: "/projects/{id}/files/{fileId}/attach",
163
+ tag: "Files",
164
+ summary: "Attach an existing file to a project",
165
+ body: true,
166
+ response: "empty",
167
+ params: [
168
+ { name: "id", in: "path", required: true, kind: "string", description: "Convex project ID" },
169
+ { name: "fileId", in: "path", required: true, kind: "string", description: "Convex file ID" },
170
+ { name: "organizationId", in: "body", required: false, kind: "string", description: "Convex organization ID \u2014 get it from GET /v1/me or the get_me tool (memberships[].orgId), not the organization name. Optional with a company key or an app's key: the key's company is used." }
171
+ ]
172
+ },
173
+ batch_create_products: {
174
+ method: "POST",
175
+ path: "/products/batch",
176
+ tag: "Products",
177
+ summary: "Batch-create products",
178
+ body: true,
179
+ response: "json",
180
+ params: [
181
+ { name: "organizationId", in: "body", required: false, kind: "string", description: "Convex organization ID \u2014 get it from GET /v1/me or the get_me tool (memberships[].orgId), not the organization name. Optional with a company key or an app's key: the key's company is used." },
182
+ { name: "listId", in: "body", required: true, kind: "string", description: "Convex product list ID" },
183
+ { name: "products", in: "body", required: true, kind: "array" }
184
+ ]
185
+ },
186
+ cancel_assessment: {
187
+ method: "POST",
188
+ path: "/assessments/{id}/cancel",
189
+ tag: "Takeoff",
190
+ summary: "Cancel a running assessment",
191
+ body: true,
192
+ response: "empty",
193
+ params: [
194
+ { name: "id", in: "path", required: true, kind: "string", description: "Convex takeoff assessment ID" },
195
+ { name: "organizationId", in: "body", required: false, kind: "string", description: "Convex organization ID \u2014 get it from GET /v1/me or the get_me tool (memberships[].orgId), not the organization name. Optional with a company key or an app's key: the key's company is used." }
196
+ ]
197
+ },
198
+ confirm_file_upload: {
199
+ method: "POST",
200
+ path: "/files/{id}/confirm",
201
+ tag: "Files",
202
+ summary: "Confirm a file upload after the client PUT succeeds",
203
+ confirm: "This operation uses AI usage.",
204
+ body: true,
205
+ response: "json",
206
+ params: [
207
+ { name: "id", in: "path", required: true, kind: "string", description: "Convex file ID" },
208
+ { name: "r2Key", in: "body", required: true, kind: "string" }
209
+ ]
210
+ },
211
+ create_agent_session: {
212
+ method: "POST",
213
+ path: "/ai-sessions",
214
+ tag: "Account",
215
+ summary: "Start a computer-use agent session on a project page",
216
+ confirm: "This operation uses AI usage.",
217
+ body: true,
218
+ response: "json",
219
+ params: [
220
+ { name: "projectId", in: "body", required: true, kind: "string", description: "Convex project ID" },
221
+ { name: "pageId", in: "body", required: true, kind: "string", description: "Convex page ID the agent will operate on." },
222
+ { name: "vendor", in: "body", required: true, kind: "string", enum: ["claude", "openai", "gemini"] }
223
+ ]
224
+ },
225
+ create_assessment: {
226
+ method: "POST",
227
+ path: "/projects/{id}/assessments",
228
+ tag: "Takeoff",
229
+ summary: "Start a new AI assessment",
230
+ confirm: "This operation uses AI usage.",
231
+ body: true,
232
+ response: "json",
233
+ params: [
234
+ { name: "id", in: "path", required: true, kind: "string", description: "Convex project ID" },
235
+ { name: "organizationId", in: "body", required: false, kind: "string", description: "Convex organization ID \u2014 get it from GET /v1/me or the get_me tool (memberships[].orgId), not the organization name. Optional with a company key or an app's key: the key's company is used." },
236
+ { name: "conversationId", in: "body", required: false, kind: "string" },
237
+ { name: "fileIds", in: "body", required: false, kind: "array" },
238
+ { name: "pageIds", in: "body", required: false, kind: "array" },
239
+ { name: "imageUrls", in: "body", required: false, kind: "array" },
240
+ { name: "types", in: "body", required: false, kind: "array" }
241
+ ]
242
+ },
243
+ create_bid: {
244
+ method: "POST",
245
+ path: "/projects/{id}/bids",
246
+ tag: "Estimates",
247
+ summary: "Generate a project-scoped bid document (blocking)",
248
+ confirm: "This operation uses AI usage.",
249
+ body: true,
250
+ response: "json",
251
+ params: [
252
+ { name: "id", in: "path", required: true, kind: "string", description: "Convex project ID" },
253
+ { name: "organizationId", in: "body", required: false, kind: "string", description: "Convex organization ID \u2014 get it from GET /v1/me or the get_me tool (memberships[].orgId), not the organization name. Optional with a company key or an app's key: the key's company is used." },
254
+ { name: "notes", in: "body", required: false, kind: "string", description: "Additional context appended to the prompt" },
255
+ { name: "measurementContext", in: "body", required: false, kind: "string", description: "Pre-serialized takeoff measurements + line items (markdown). If omitted, the endpoint auto-loads the project takeoff from Convex and serializes it server-side." },
256
+ { name: "ignoreMissingVariables", in: "body", required: false, kind: "boolean", description: "When auto-loading takeoff data, skip line items with missing formula variables instead of failing with 422. Ignored if measurementContext is provided." },
257
+ { name: "share", in: "body", required: false, kind: "boolean", description: "Publish the saved bid on a public link that anyone holding the URL can read. Defaults to false \u2014 the document stays private to your organization and documentUrl points at the signed-in path. Set true only when the customer asked for a shareable link; you can also share or unshare later via PATCH /v1/documents/{id}/share." }
258
+ ]
259
+ },
260
+ create_conversation: {
261
+ method: "POST",
262
+ path: "/conversations",
263
+ tag: "Conversations",
264
+ summary: "Create a conversation",
265
+ body: true,
266
+ response: "json",
267
+ params: [
268
+ { name: "organizationId", in: "body", required: false, kind: "string", description: "Convex organization ID \u2014 get it from GET /v1/me or the get_me tool (memberships[].orgId), not the organization name. Optional with a company key or an app's key: the key's company is used." },
269
+ { name: "projectId", in: "body", required: false, kind: "string", description: "Convex project ID" },
270
+ { name: "title", in: "body", required: false, kind: "string" },
271
+ { name: "type", in: "body", required: false, kind: "string", enum: ["ai_chat", "project_activity"] },
272
+ { name: "model", in: "body", required: false, kind: "string" },
273
+ { name: "visibility", in: "body", required: false, kind: "string", enum: ["private", "shared"] }
274
+ ]
275
+ },
276
+ create_custom_property: {
277
+ method: "POST",
278
+ path: "/custom-properties",
279
+ tag: "Custom properties",
280
+ summary: "Create a custom property",
281
+ body: true,
282
+ response: "json",
283
+ params: [
284
+ { name: "organizationId", in: "body", required: false, kind: "string", description: "Convex organization ID \u2014 get it from GET /v1/me or the get_me tool (memberships[].orgId), not the organization name. Optional with a company key or an app's key: the key's company is used." },
285
+ { name: "name", in: "body", required: true, kind: "string" },
286
+ { name: "type", in: "body", required: true, kind: "string", enum: ["text", "textarea", "number", "date", "checkbox", "multiCheckbox", "select", "url", "email", "phone", "currency", "percentage"] },
287
+ { name: "recordTypes", in: "body", required: true, kind: "array" },
288
+ { name: "options", in: "body", required: false, kind: "array" },
289
+ { name: "presetId", in: "body", required: false, kind: "string" }
290
+ ]
291
+ },
292
+ create_estimate: {
293
+ method: "POST",
294
+ path: "/projects/{id}/estimates",
295
+ tag: "Estimates",
296
+ summary: "Generate an estimate and persist it as a document (blocking)",
297
+ confirm: "This operation uses AI usage.",
298
+ body: true,
299
+ response: "json",
300
+ params: [
301
+ { name: "id", in: "path", required: true, kind: "string", description: "Convex project ID" },
302
+ { name: "organizationId", in: "body", required: false, kind: "string", description: "Convex organization ID \u2014 get it from GET /v1/me or the get_me tool (memberships[].orgId), not the organization name. Optional with a company key or an app's key: the key's company is used." },
303
+ { name: "query", in: "body", required: true, kind: "string", description: "Natural-language prompt describing the scope of the estimate" },
304
+ { name: "notes", in: "body", required: false, kind: "string", description: "Additional notes appended to the prompt" },
305
+ { name: "measurementContext", in: "body", required: false, kind: "string", description: "Pre-serialized takeoff data to ground the estimator (see GET /v1/projects/{id}/takeoff-summary)" },
306
+ { name: "share", in: "body", required: false, kind: "boolean", description: "Publish the saved estimate on a public link that anyone holding the URL can read. Defaults to false \u2014 the document stays private to your organization and documentUrl points at the signed-in path. Set true only when the customer asked for a shareable link; you can also share or unshare later via PATCH /v1/documents/{id}/share." }
307
+ ]
308
+ },
309
+ create_folder: {
310
+ method: "POST",
311
+ path: "/folders",
312
+ tag: "Folders",
313
+ summary: "Create a folder",
314
+ body: true,
315
+ response: "json",
316
+ params: [
317
+ { name: "projectId", in: "body", required: true, kind: "string", description: "Convex project ID" },
318
+ { name: "name", in: "body", required: true, kind: "string" },
319
+ { name: "parentId", in: "body", required: true, kind: "string", description: "Convex folder ID" }
320
+ ]
321
+ },
322
+ create_group: {
323
+ method: "POST",
324
+ path: "/projects/{id}/groups",
325
+ tag: "Takeoff",
326
+ summary: "Create a takeoff group",
327
+ body: true,
328
+ response: "json",
329
+ params: [
330
+ { name: "id", in: "path", required: true, kind: "string", description: "Convex project ID" },
331
+ { name: "name", in: "body", required: true, kind: "string" },
332
+ { name: "parentId", in: "body", required: false, kind: "string", description: "Convex takeoff group ID" }
333
+ ]
334
+ },
335
+ create_labor_crew: {
336
+ method: "POST",
337
+ path: "/labor/crews",
338
+ tag: "Labor",
339
+ summary: "Create a labor crew",
340
+ body: true,
341
+ response: "json",
342
+ params: [
343
+ { name: "organizationId", in: "body", required: false, kind: "string", description: "Convex organization ID \u2014 get it from GET /v1/me or the get_me tool (memberships[].orgId), not the organization name. Optional with a company key or an app's key: the key's company is used." },
344
+ { name: "name", in: "body", required: true, kind: "string" },
345
+ { name: "description", in: "body", required: false, kind: "string" },
346
+ { name: "members", in: "body", required: true, kind: "array" },
347
+ { name: "equipmentIds", in: "body", required: false, kind: "array" },
348
+ { name: "dailyCapacity", in: "body", required: false, kind: "object" }
349
+ ]
350
+ },
351
+ create_labor_equipment: {
352
+ method: "POST",
353
+ path: "/labor/equipment",
354
+ tag: "Labor",
355
+ summary: "Create a labor equipment entry",
356
+ body: true,
357
+ response: "json",
358
+ params: [
359
+ { name: "organizationId", in: "body", required: false, kind: "string", description: "Convex organization ID \u2014 get it from GET /v1/me or the get_me tool (memberships[].orgId), not the organization name. Optional with a company key or an app's key: the key's company is used." },
360
+ { name: "name", in: "body", required: true, kind: "string" },
361
+ { name: "description", in: "body", required: false, kind: "string" },
362
+ { name: "category", in: "body", required: true, kind: "string" },
363
+ { name: "dailyRate", in: "body", required: true, kind: "number" },
364
+ { name: "hourlyRate", in: "body", required: false, kind: "number" },
365
+ { name: "currency", in: "body", required: false, kind: "string", enum: ["USD", "CAD", "MXN", "BRL", "ARS", "CLP", "COP", "PEN", "UYU", "PYG", "BOB", "VES", "GYD", "SRD", "GTQ", "HNL", "NIO", "CRC", "PAB", "BZD", "JMD", "HTG", "DOP", "CUP", "BSD", "BBD", "TTD", "XCD", "AWG", "ANG", "KYD", "BMD", "FKP", "EUR", "GBP", "CHF", "NOK", "SEK", "DKK", "PLN", "CZK", "HUF", "RON", "BGN", "HRK", "RSD", "UAH", "RUB", "TRY", "ISK", "BAM", "MKD", "ALL", "MDL", "BYN", "GEL", "AMD", "AZN", "GIP", "ILS", "AED", "SAR", "QAR", "KWD", "BHD", "OMR", "JOD", "LBP", "EGP", "IQD", "IRR", "SYP", "YER", "ZAR", "NAD", "LSL", "SZL", "BWP", "NGN", "KES", "GHS", "MAD", "TND", "DZD", "LYD", "ETB", "ERN", "DJF", "SOS", "SDG", "SSP", "UGX", "TZS", "RWF", "BIF", "MWK", "ZMW", "ZWG", "MZN", "AOA", "CDF", "MGA", "MUR", "SCR", "KMF", "CVE", "GMD", "GNF", "LRD", "SLE", "MRU", "STN", "XOF", "XAF", "SHP", "JPY", "CNY", "HKD", "MOP", "TWD", "KRW", "KPW", "MNT", "SGD", "INR", "BTN", "PKR", "BDT", "LKR", "NPR", "MVR", "AFN", "IDR", "MYR", "PHP", "THB", "VND", "MMK", "KHR", "LAK", "BND", "KZT", "UZS", "KGS", "TJS", "TMT", "AUD", "NZD", "FJD", "PGK", "SBD", "VUV", "WST", "TOP", "XPF"], description: "ISO 4217 currency code" },
366
+ { name: "isOwned", in: "body", required: false, kind: "boolean" }
367
+ ]
368
+ },
369
+ create_labor_rate: {
370
+ method: "POST",
371
+ path: "/labor/rates",
372
+ tag: "Labor",
373
+ summary: "Create a labor rate",
374
+ body: true,
375
+ response: "json",
376
+ params: [
377
+ { name: "organizationId", in: "body", required: false, kind: "string", description: "Convex organization ID \u2014 get it from GET /v1/me or the get_me tool (memberships[].orgId), not the organization name. Optional with a company key or an app's key: the key's company is used." },
378
+ { name: "name", in: "body", required: true, kind: "string" },
379
+ { name: "description", in: "body", required: false, kind: "string" },
380
+ { name: "hourlyRate", in: "body", required: true, kind: "number" },
381
+ { name: "burdenRate", in: "body", required: false, kind: "number" },
382
+ { name: "currency", in: "body", required: false, kind: "string", enum: ["USD", "CAD", "MXN", "BRL", "ARS", "CLP", "COP", "PEN", "UYU", "PYG", "BOB", "VES", "GYD", "SRD", "GTQ", "HNL", "NIO", "CRC", "PAB", "BZD", "JMD", "HTG", "DOP", "CUP", "BSD", "BBD", "TTD", "XCD", "AWG", "ANG", "KYD", "BMD", "FKP", "EUR", "GBP", "CHF", "NOK", "SEK", "DKK", "PLN", "CZK", "HUF", "RON", "BGN", "HRK", "RSD", "UAH", "RUB", "TRY", "ISK", "BAM", "MKD", "ALL", "MDL", "BYN", "GEL", "AMD", "AZN", "GIP", "ILS", "AED", "SAR", "QAR", "KWD", "BHD", "OMR", "JOD", "LBP", "EGP", "IQD", "IRR", "SYP", "YER", "ZAR", "NAD", "LSL", "SZL", "BWP", "NGN", "KES", "GHS", "MAD", "TND", "DZD", "LYD", "ETB", "ERN", "DJF", "SOS", "SDG", "SSP", "UGX", "TZS", "RWF", "BIF", "MWK", "ZMW", "ZWG", "MZN", "AOA", "CDF", "MGA", "MUR", "SCR", "KMF", "CVE", "GMD", "GNF", "LRD", "SLE", "MRU", "STN", "XOF", "XAF", "SHP", "JPY", "CNY", "HKD", "MOP", "TWD", "KRW", "KPW", "MNT", "SGD", "INR", "BTN", "PKR", "BDT", "LKR", "NPR", "MVR", "AFN", "IDR", "MYR", "PHP", "THB", "VND", "MMK", "KHR", "LAK", "BND", "KZT", "UZS", "KGS", "TJS", "TMT", "AUD", "NZD", "FJD", "PGK", "SBD", "VUV", "WST", "TOP", "XPF"], description: "ISO 4217 currency code" },
383
+ { name: "effectiveDate", in: "body", required: false, kind: "number" },
384
+ { name: "isDefault", in: "body", required: false, kind: "boolean" }
385
+ ]
386
+ },
387
+ create_layer: {
388
+ method: "POST",
389
+ path: "/projects/{id}/layers",
390
+ tag: "Takeoff",
391
+ summary: "Create a takeoff layer",
392
+ body: true,
393
+ response: "json",
394
+ params: [
395
+ { name: "id", in: "path", required: true, kind: "string", description: "Convex project ID" },
396
+ { name: "name", in: "body", required: true, kind: "string" }
397
+ ]
398
+ },
399
+ create_list: {
400
+ method: "POST",
401
+ path: "/lists",
402
+ tag: "Lists",
403
+ summary: "Create a list",
404
+ body: true,
405
+ response: "json",
406
+ params: [
407
+ { name: "organizationId", in: "body", required: false, kind: "string", description: "Convex organization ID \u2014 get it from GET /v1/me or the get_me tool (memberships[].orgId), not the organization name. Optional with a company key or an app's key: the key's company is used." },
408
+ { name: "name", in: "body", required: true, kind: "string" },
409
+ { name: "description", in: "body", required: false, kind: "string" },
410
+ { name: "type", in: "body", required: true, kind: "string", enum: ["product", "takeoffTemplate"] },
411
+ { name: "privacy", in: "body", required: false, kind: "string", enum: ["private", "organization", "public", "system"] },
412
+ { name: "category", in: "body", required: false, kind: "string" },
413
+ { name: "locale", in: "body", required: true, kind: "string", enum: ["af-ZA", "id-ID", "ms-MY", "ca-ES", "cs-CZ", "da-DK", "de-DE", "en-US", "es-ES", "es-419", "fr-CA", "fr-FR", "hr-HR", "it-IT", "hu-HU", "nl-NL", "no-NO", "pl-PL", "pt-BR", "pt-PT", "ro-RO", "sk-SK", "fi-FI", "sv-SE", "vi-VN", "tr-TR", "el-GR", "ru-RU", "sr-SP", "uk-UA", "hy-AM", "he-IL", "ur-PK", "ar-SA", "hi-IN", "th-TH", "ko-KR", "ja-JP", "zh-CN", "zh-TW", null], description: "Supported BCP-47 locale code" }
414
+ ]
415
+ },
416
+ create_measurement: {
417
+ method: "POST",
418
+ path: "/pages/{id}/measurements",
419
+ tag: "Takeoff",
420
+ summary: "Create a measurement on a page",
421
+ body: true,
422
+ response: "json",
423
+ params: [
424
+ { name: "id", in: "path", required: true, kind: "string", description: "Convex page ID" },
425
+ { name: "symbolId", in: "body", required: true, kind: "string", description: "Convex takeoff symbol ID" },
426
+ { name: "shapeId", in: "body", required: true, kind: "string", description: "Canvas shape ID" },
427
+ { name: "value", in: "body", required: true, kind: "number", description: "The measured quantity in RAW PAGE PIXELS \u2014 the same coordinate space as the geometry points, NEVER feet, metres, or any other real-world unit. Linear measurements are a length in px, areas are px\xB2, counts are a tally. Exayard converts to real units at display time using the page scale, so a real-world number sent here is converted a second time and reads wrong. When geometry is supplied the server measures it and rejects a value that disagrees by 10\xD7 or more, naming both numbers. To send a real-world quantity, convert it to page pixels first (multiply by the page's pixels-per-unit; square that factor for an area)." },
428
+ { name: "geometry", in: "body", required: false, kind: "object", description: 'Measurement geometry: { "points": [{ "x": number, "y": number, "bulge"?: number }, ...], "isClosed": boolean, "holes"?: Point[][], "pathD"?: string }. When geometry is provided, "points" and "isClosed" are required; other keys (e.g. { "length": 280.9 }) are rejected.' },
429
+ { name: "notes", in: "body", required: false, kind: "string" },
430
+ { name: "layerId", in: "body", required: false, kind: "string", description: "Convex takeoff layer ID" }
431
+ ]
432
+ },
433
+ create_message: {
434
+ method: "POST",
435
+ path: "/projects/{id}/messages",
436
+ tag: "Messages",
437
+ summary: "Post a message to a project",
438
+ body: true,
439
+ response: "json",
440
+ params: [
441
+ { name: "id", in: "path", required: true, kind: "string", description: "Convex project ID" },
442
+ { name: "content", in: "body", required: true, kind: "string" },
443
+ { name: "parentMessageId", in: "body", required: false, kind: "string", description: "Convex chat message ID" },
444
+ { name: "pageId", in: "body", required: false, kind: "string", description: "Convex page ID" },
445
+ { name: "location", in: "body", required: false, kind: "object" },
446
+ { name: "mentions", in: "body", required: false, kind: "array" }
447
+ ]
448
+ },
449
+ create_organization: {
450
+ method: "POST",
451
+ path: "/organizations",
452
+ tag: "Account",
453
+ summary: "Create a company your app's company owns",
454
+ body: true,
455
+ response: "json",
456
+ params: [
457
+ { name: "name", in: "body", required: true, kind: "string", description: "The company name, 1 to 200 characters." },
458
+ { name: "country", in: "body", required: false, kind: "string", description: "ISO 3166-1 alpha-2 country code. It sets the company's starting tax, units and currency." },
459
+ { name: "currency", in: "body", required: false, kind: "string", enum: ["USD", "CAD", "MXN", "BRL", "ARS", "CLP", "COP", "PEN", "UYU", "PYG", "BOB", "VES", "GYD", "SRD", "GTQ", "HNL", "NIO", "CRC", "PAB", "BZD", "JMD", "HTG", "DOP", "CUP", "BSD", "BBD", "TTD", "XCD", "AWG", "ANG", "KYD", "BMD", "FKP", "EUR", "GBP", "CHF", "NOK", "SEK", "DKK", "PLN", "CZK", "HUF", "RON", "BGN", "HRK", "RSD", "UAH", "RUB", "TRY", "ISK", "BAM", "MKD", "ALL", "MDL", "BYN", "GEL", "AMD", "AZN", "GIP", "ILS", "AED", "SAR", "QAR", "KWD", "BHD", "OMR", "JOD", "LBP", "EGP", "IQD", "IRR", "SYP", "YER", "ZAR", "NAD", "LSL", "SZL", "BWP", "NGN", "KES", "GHS", "MAD", "TND", "DZD", "LYD", "ETB", "ERN", "DJF", "SOS", "SDG", "SSP", "UGX", "TZS", "RWF", "BIF", "MWK", "ZMW", "ZWG", "MZN", "AOA", "CDF", "MGA", "MUR", "SCR", "KMF", "CVE", "GMD", "GNF", "LRD", "SLE", "MRU", "STN", "XOF", "XAF", "SHP", "JPY", "CNY", "HKD", "MOP", "TWD", "KRW", "KPW", "MNT", "SGD", "INR", "BTN", "PKR", "BDT", "LKR", "NPR", "MVR", "AFN", "IDR", "MYR", "PHP", "THB", "VND", "MMK", "KHR", "LAK", "BND", "KZT", "UZS", "KGS", "TJS", "TMT", "AUD", "NZD", "FJD", "PGK", "SBD", "VUV", "WST", "TOP", "XPF"], description: "ISO 4217 currency code" },
460
+ { name: "measurementSystem", in: "body", required: false, kind: "string", enum: ["imperial", "metric"] }
461
+ ]
462
+ },
463
+ create_product: {
464
+ method: "POST",
465
+ path: "/products",
466
+ tag: "Products",
467
+ summary: "Create a product",
468
+ body: true,
469
+ response: "json",
470
+ params: [
471
+ { name: "organizationId", in: "body", required: false, kind: "string", description: "Convex organization ID \u2014 get it from GET /v1/me or the get_me tool (memberships[].orgId), not the organization name. Optional with a company key or an app's key: the key's company is used." },
472
+ { name: "listId", in: "body", required: true, kind: "string", description: "Convex product list ID" },
473
+ { name: "name", in: "body", required: false, kind: "string" },
474
+ { name: "sku", in: "body", required: false, kind: "string" },
475
+ { name: "unitOfMeasure", in: "body", required: false, kind: "string" },
476
+ { name: "costPerUnit", in: "body", required: false, kind: "number" },
477
+ { name: "currency", in: "body", required: false, kind: "string", enum: ["USD", "CAD", "MXN", "BRL", "ARS", "CLP", "COP", "PEN", "UYU", "PYG", "BOB", "VES", "GYD", "SRD", "GTQ", "HNL", "NIO", "CRC", "PAB", "BZD", "JMD", "HTG", "DOP", "CUP", "BSD", "BBD", "TTD", "XCD", "AWG", "ANG", "KYD", "BMD", "FKP", "EUR", "GBP", "CHF", "NOK", "SEK", "DKK", "PLN", "CZK", "HUF", "RON", "BGN", "HRK", "RSD", "UAH", "RUB", "TRY", "ISK", "BAM", "MKD", "ALL", "MDL", "BYN", "GEL", "AMD", "AZN", "GIP", "ILS", "AED", "SAR", "QAR", "KWD", "BHD", "OMR", "JOD", "LBP", "EGP", "IQD", "IRR", "SYP", "YER", "ZAR", "NAD", "LSL", "SZL", "BWP", "NGN", "KES", "GHS", "MAD", "TND", "DZD", "LYD", "ETB", "ERN", "DJF", "SOS", "SDG", "SSP", "UGX", "TZS", "RWF", "BIF", "MWK", "ZMW", "ZWG", "MZN", "AOA", "CDF", "MGA", "MUR", "SCR", "KMF", "CVE", "GMD", "GNF", "LRD", "SLE", "MRU", "STN", "XOF", "XAF", "SHP", "JPY", "CNY", "HKD", "MOP", "TWD", "KRW", "KPW", "MNT", "SGD", "INR", "BTN", "PKR", "BDT", "LKR", "NPR", "MVR", "AFN", "IDR", "MYR", "PHP", "THB", "VND", "MMK", "KHR", "LAK", "BND", "KZT", "UZS", "KGS", "TJS", "TMT", "AUD", "NZD", "FJD", "PGK", "SBD", "VUV", "WST", "TOP", "XPF"], description: "ISO 4217 currency code" },
478
+ { name: "markupPercent", in: "body", required: false, kind: "number" },
479
+ { name: "customProperties", in: "body", required: false, kind: "object" },
480
+ { name: "position", in: "body", required: false, kind: "integer" }
481
+ ]
482
+ },
483
+ create_project: {
484
+ method: "POST",
485
+ path: "/projects",
486
+ tag: "Projects",
487
+ summary: "Create a project",
488
+ body: true,
489
+ response: "json",
490
+ params: [
491
+ { name: "organizationId", in: "body", required: false, kind: "string", description: "Convex organization ID \u2014 get it from GET /v1/me or the get_me tool (memberships[].orgId), not the organization name. Optional with a company key or an app's key: the key's company is used." },
492
+ { name: "name", in: "body", required: true, kind: "string" },
493
+ { name: "customProperties", in: "body", required: false, kind: "object" }
494
+ ]
495
+ },
496
+ create_symbol: {
497
+ method: "POST",
498
+ path: "/projects/{id}/symbols",
499
+ tag: "Takeoff",
500
+ summary: "Create a takeoff symbol",
501
+ body: true,
502
+ response: "json",
503
+ params: [
504
+ { name: "id", in: "path", required: true, kind: "string", description: "Convex project ID" },
505
+ { name: "isTemplate", in: "body", required: false, kind: "boolean" },
506
+ { name: "listId", in: "body", required: false, kind: "string", description: "Convex product list ID" },
507
+ { name: "name", in: "body", required: true, kind: "string" },
508
+ { name: "type", in: "body", required: true, kind: "string", enum: ["count", "linear", "area"] },
509
+ { name: "color", in: "body", required: true, kind: "string" },
510
+ { name: "category", in: "body", required: false, kind: "string" },
511
+ { name: "subcategory", in: "body", required: false, kind: "string" },
512
+ { name: "groupId", in: "body", required: false, kind: "string", description: "Convex takeoff group ID" },
513
+ { name: "customProperties", in: "body", required: false, kind: "object" }
514
+ ]
515
+ },
516
+ create_vendor: {
517
+ method: "POST",
518
+ path: "/vendors",
519
+ tag: "Vendors",
520
+ summary: "Create a vendor",
521
+ body: true,
522
+ response: "json",
523
+ params: [
524
+ { name: "organizationId", in: "body", required: false, kind: "string", description: "Convex organization ID \u2014 get it from GET /v1/me or the get_me tool (memberships[].orgId), not the organization name. Optional with a company key or an app's key: the key's company is used." },
525
+ { name: "name", in: "body", required: true, kind: "string" },
526
+ { name: "type", in: "body", required: true, kind: "string", enum: ["supplier", "subcontractor", "rental"] },
527
+ { name: "kind", in: "body", required: false, kind: "string", enum: ["sub", "supplier", "surety", "insurer", "agency", "owner", "architect", "engineer", "other", null] },
528
+ { name: "contactName", in: "body", required: false, kind: "string" },
529
+ { name: "email", in: "body", required: false, kind: "string" },
530
+ { name: "phone", in: "body", required: false, kind: "string" },
531
+ { name: "website", in: "body", required: false, kind: "string" },
532
+ { name: "address", in: "body", required: false, kind: "object" },
533
+ { name: "paymentTerms", in: "body", required: false, kind: "string" },
534
+ { name: "accountNumber", in: "body", required: false, kind: "string" },
535
+ { name: "notes", in: "body", required: false, kind: "string" },
536
+ { name: "tags", in: "body", required: false, kind: "array" },
537
+ { name: "isPreferred", in: "body", required: false, kind: "boolean" }
538
+ ]
539
+ },
540
+ create_vendor_quote: {
541
+ method: "POST",
542
+ path: "/projects/{id}/quotes",
543
+ tag: "Vendor quotes",
544
+ summary: "Create a vendor quote request",
545
+ body: true,
546
+ response: "json",
547
+ params: [
548
+ { name: "id", in: "path", required: true, kind: "string", description: "Convex project ID" },
549
+ { name: "organizationId", in: "body", required: false, kind: "string", description: "Convex organization ID \u2014 get it from GET /v1/me or the get_me tool (memberships[].orgId), not the organization name. Optional with a company key or an app's key: the key's company is used." },
550
+ { name: "vendorId", in: "body", required: true, kind: "string" },
551
+ { name: "quoteNumber", in: "body", required: true, kind: "string" },
552
+ { name: "lineItems", in: "body", required: false, kind: "array" },
553
+ { name: "subtotal", in: "body", required: false, kind: "number", description: "Sum of the line items' totalPrice when lineItems are sent; absent counts as 0" },
554
+ { name: "tax", in: "body", required: false, kind: "number" },
555
+ { name: "shipping", in: "body", required: false, kind: "number" },
556
+ { name: "total", in: "body", required: false, kind: "number", description: "Must equal subtotal + tax + shipping (an absent amount counts as 0); absent counts as 0" },
557
+ { name: "validUntil", in: "body", required: false, kind: "number", description: "Quote expiry as epoch milliseconds" },
558
+ { name: "notes", in: "body", required: false, kind: "string" }
559
+ ]
560
+ },
561
+ create_webhook_endpoint: {
562
+ method: "POST",
563
+ path: "/webhook_endpoints",
564
+ tag: "Webhooks",
565
+ summary: "Create a webhook endpoint",
566
+ body: true,
567
+ response: "json",
568
+ params: [
569
+ { name: "organizationId", in: "body", required: false, kind: "string", description: "Convex organization ID \u2014 get it from GET /v1/me or the get_me tool (memberships[].orgId), not the organization name. Optional with a company key or an app's key: the key's company is used." },
570
+ { name: "url", in: "body", required: true, kind: "string" },
571
+ { name: "events", in: "body", required: true, kind: "array" },
572
+ { name: "description", in: "body", required: false, kind: "string" }
573
+ ]
574
+ },
575
+ create_widget: {
576
+ method: "POST",
577
+ path: "/widgets",
578
+ tag: "Widgets",
579
+ summary: "Create a widget",
580
+ body: true,
581
+ response: "json",
582
+ params: [
583
+ { name: "organizationId", in: "body", required: false, kind: "string", description: "Convex organization ID \u2014 get it from GET /v1/me or the get_me tool (memberships[].orgId), not the organization name. Optional with a company key or an app's key: the key's company is used." },
584
+ { name: "name", in: "body", required: true, kind: "string" },
585
+ { name: "config", in: "body", required: false, kind: "object" }
586
+ ]
587
+ },
588
+ delete_organization: {
589
+ method: "DELETE",
590
+ path: "/organizations/{id}",
591
+ tag: "Account",
592
+ summary: "Release a company your app's company owns",
593
+ body: false,
594
+ response: "json",
595
+ params: [
596
+ { name: "id", in: "path", required: true, kind: "string", description: "The id of a company your app's company owns, from GET /v1/organizations or its creation." }
597
+ ]
598
+ },
599
+ delete_webhook_endpoint: {
600
+ method: "DELETE",
601
+ path: "/webhook_endpoints/{id}",
602
+ tag: "Webhooks",
603
+ summary: "Delete a webhook endpoint",
604
+ body: false,
605
+ response: "empty",
606
+ params: [
607
+ { name: "id", in: "path", required: true, kind: "string" },
608
+ { name: "organizationId", in: "query", required: false, kind: "string", description: "Convex organization ID \u2014 get it from GET /v1/me or the get_me tool (memberships[].orgId), not the organization name. Optional with a company key or an app's key: the key's company is used." }
609
+ ]
610
+ },
611
+ disconnect_integration: {
612
+ method: "DELETE",
613
+ path: "/integrations/{id}",
614
+ tag: "Integrations",
615
+ summary: "Disconnect an integration",
616
+ body: false,
617
+ response: "empty",
618
+ params: [
619
+ { name: "id", in: "path", required: true, kind: "string" },
620
+ { name: "organizationId", in: "query", required: false, kind: "string", description: "Convex organization ID \u2014 get it from GET /v1/me or the get_me tool (memberships[].orgId), not the organization name. Optional with a company key or an app's key: the key's company is used." }
621
+ ]
622
+ },
623
+ export_project: {
624
+ method: "GET",
625
+ path: "/projects/{id}/export",
626
+ tag: "Projects",
627
+ summary: "Export a project as structured JSON",
628
+ body: false,
629
+ response: "json",
630
+ params: [
631
+ { name: "id", in: "path", required: true, kind: "string", description: "Convex project ID" },
632
+ { name: "organizationId", in: "query", required: false, kind: "string", description: "Convex organization ID \u2014 get it from GET /v1/me or the get_me tool (memberships[].orgId), not the organization name. Optional with a company key or an app's key: the key's company is used." }
633
+ ]
634
+ },
635
+ find_symbol_elements: {
636
+ method: "POST",
637
+ path: "/pages/{id}/elements/find",
638
+ tag: "Takeoff",
639
+ summary: "Detect instances of an element on a page via Claude Vision",
640
+ body: true,
641
+ response: "json",
642
+ params: [
643
+ { name: "id", in: "path", required: true, kind: "string", description: "Convex page ID" },
644
+ { name: "organizationId", in: "body", required: false, kind: "string", description: "Convex organization ID \u2014 get it from GET /v1/me or the get_me tool (memberships[].orgId), not the organization name. Optional with a company key or an app's key: the key's company is used." },
645
+ { name: "projectId", in: "body", required: true, kind: "string", description: "Convex project ID" },
646
+ { name: "elementDescription", in: "body", required: true, kind: "string", description: 'Natural-language description of what to find (e.g. "electrical outlets", "doors")' },
647
+ { name: "measurementType", in: "body", required: true, kind: "string", enum: ["count", "linear", "area"], description: "How detections should be shaped (count marker / linear line / area rectangle)" },
648
+ { name: "persist", in: "body", required: false, kind: "boolean", description: "When true, writes the detected shapes to the realtime canvas via the sync-worker. Returns persistedShapeIds on success." }
649
+ ]
650
+ },
651
+ generate_estimate: {
652
+ method: "POST",
653
+ path: "/projects/{id}/estimates/generate",
654
+ tag: "Estimates",
655
+ summary: "Stream an AI-generated cost estimate",
656
+ body: true,
657
+ response: "stream",
658
+ params: [
659
+ { name: "id", in: "path", required: true, kind: "string", description: "Convex project ID" },
660
+ { name: "organizationId", in: "body", required: false, kind: "string", description: "Convex organization ID \u2014 get it from GET /v1/me or the get_me tool (memberships[].orgId), not the organization name. Optional with a company key or an app's key: the key's company is used." },
661
+ { name: "query", in: "body", required: true, kind: "string", description: "Natural-language prompt describing the scope of the estimate" },
662
+ { name: "notes", in: "body", required: false, kind: "string", description: "Additional notes appended to the prompt" },
663
+ { name: "measurementContext", in: "body", required: false, kind: "string", description: "Pre-serialized takeoff data to ground the estimator (see GET /v1/projects/{id}/takeoff-summary)" }
664
+ ]
665
+ },
666
+ get_assessment: {
667
+ method: "GET",
668
+ path: "/assessments/{id}",
669
+ tag: "Takeoff",
670
+ summary: "Get an assessment by ID",
671
+ body: false,
672
+ response: "json",
673
+ params: [
674
+ { name: "id", in: "path", required: true, kind: "string", description: "Convex takeoff assessment ID" }
675
+ ]
676
+ },
677
+ get_best_price_for_product: {
678
+ method: "GET",
679
+ path: "/products/{id}/best-price",
680
+ tag: "Vendor prices",
681
+ summary: "Best vendor price for a product",
682
+ body: false,
683
+ response: "json",
684
+ params: [
685
+ { name: "id", in: "path", required: true, kind: "string", description: "Convex product ID" },
686
+ { name: "organizationId", in: "query", required: false, kind: "string", description: "Convex organization ID \u2014 get it from GET /v1/me or the get_me tool (memberships[].orgId), not the organization name. Optional with a company key or an app's key: the key's company is used." },
687
+ { name: "quantity", in: "query", required: false, kind: "number" }
688
+ ]
689
+ },
690
+ get_contact: {
691
+ method: "GET",
692
+ path: "/contacts/{id}",
693
+ tag: "Contacts",
694
+ summary: "Get a contact by ID",
695
+ body: false,
696
+ response: "json",
697
+ params: [
698
+ { name: "id", in: "path", required: true, kind: "string", description: "Convex contact ID" },
699
+ { name: "organizationId", in: "query", required: false, kind: "string", description: "Convex organization ID \u2014 get it from GET /v1/me or the get_me tool (memberships[].orgId), not the organization name. Optional with a company key or an app's key: the key's company is used." }
700
+ ]
701
+ },
702
+ get_conversation: {
703
+ method: "GET",
704
+ path: "/conversations/{id}",
705
+ tag: "Conversations",
706
+ summary: "Get a conversation by ID",
707
+ body: false,
708
+ response: "json",
709
+ params: [
710
+ { name: "id", in: "path", required: true, kind: "string", description: "Convex conversation ID" },
711
+ { name: "organizationId", in: "query", required: false, kind: "string", description: "Convex organization ID \u2014 get it from GET /v1/me or the get_me tool (memberships[].orgId), not the organization name. Optional with a company key or an app's key: the key's company is used." }
712
+ ]
713
+ },
714
+ get_credit_balance: {
715
+ method: "GET",
716
+ path: "/credits/balance",
717
+ tag: "Credits",
718
+ summary: "Get credit balance for an organization",
719
+ body: false,
720
+ response: "json",
721
+ params: [
722
+ { name: "organizationId", in: "query", required: false, kind: "string", description: "Convex organization ID \u2014 get it from GET /v1/me or the get_me tool (memberships[].orgId), not the organization name. Optional with a company key or an app's key: the key's company is used." }
723
+ ]
724
+ },
725
+ get_credit_usage_history: {
726
+ method: "GET",
727
+ path: "/credits/usage",
728
+ tag: "Credits",
729
+ summary: "Get credit usage history",
730
+ body: false,
731
+ response: "json",
732
+ params: [
733
+ { name: "organizationId", in: "query", required: false, kind: "string", description: "Convex organization ID \u2014 get it from GET /v1/me or the get_me tool (memberships[].orgId), not the organization name. Optional with a company key or an app's key: the key's company is used." },
734
+ { name: "limit", in: "query", required: false, kind: "integer" },
735
+ { name: "cursor", in: "query", required: false, kind: "number" },
736
+ { name: "dateFrom", in: "query", required: false, kind: "number" },
737
+ { name: "dateTo", in: "query", required: false, kind: "number" },
738
+ { name: "type", in: "query", required: false, kind: "string", enum: ["issuance", "consumption"] },
739
+ { name: "kind", in: "query", required: false, kind: "string", enum: ["monthly", "gifted", "purchased"] }
740
+ ]
741
+ },
742
+ get_credit_usage_stats: {
743
+ method: "GET",
744
+ path: "/credits/stats",
745
+ tag: "Credits",
746
+ summary: "Get credit usage stats",
747
+ body: false,
748
+ response: "json",
749
+ params: [
750
+ { name: "organizationId", in: "query", required: false, kind: "string", description: "Convex organization ID \u2014 get it from GET /v1/me or the get_me tool (memberships[].orgId), not the organization name. Optional with a company key or an app's key: the key's company is used." },
751
+ { name: "dateFrom", in: "query", required: false, kind: "number" },
752
+ { name: "dateTo", in: "query", required: false, kind: "number" }
753
+ ]
754
+ },
755
+ get_custom_property: {
756
+ method: "GET",
757
+ path: "/custom-properties/{id}",
758
+ tag: "Custom properties",
759
+ summary: "Get a custom property",
760
+ body: false,
761
+ response: "json",
762
+ params: [
763
+ { name: "id", in: "path", required: true, kind: "string", description: "Convex custom property ID" },
764
+ { name: "organizationId", in: "query", required: false, kind: "string", description: "Convex organization ID \u2014 get it from GET /v1/me or the get_me tool (memberships[].orgId), not the organization name. Optional with a company key or an app's key: the key's company is used." }
765
+ ]
766
+ },
767
+ get_document: {
768
+ method: "GET",
769
+ path: "/documents/{id}",
770
+ tag: "Documents",
771
+ summary: "Get a single document",
772
+ body: false,
773
+ response: "json",
774
+ params: [
775
+ { name: "id", in: "path", required: true, kind: "string", description: "Convex document ID" },
776
+ { name: "organizationId", in: "query", required: false, kind: "string", description: "Convex organization ID \u2014 get it from GET /v1/me or the get_me tool (memberships[].orgId), not the organization name. Optional with a company key or an app's key: the key's company is used." }
777
+ ]
778
+ },
779
+ get_file: {
780
+ method: "GET",
781
+ path: "/files/{id}",
782
+ tag: "Files",
783
+ summary: "Get a file with its pages",
784
+ body: false,
785
+ response: "json",
786
+ params: [
787
+ { name: "id", in: "path", required: true, kind: "string", description: "Convex file ID" },
788
+ { name: "organizationId", in: "query", required: false, kind: "string", description: "Convex organization ID \u2014 get it from GET /v1/me or the get_me tool (memberships[].orgId), not the organization name. Optional with a company key or an app's key: the key's company is used." }
789
+ ]
790
+ },
791
+ get_file_url: {
792
+ method: "GET",
793
+ path: "/files/{id}/url",
794
+ tag: "Files",
795
+ summary: "Get a presigned download URL for a file",
796
+ body: false,
797
+ response: "json",
798
+ params: [
799
+ { name: "id", in: "path", required: true, kind: "string", description: "Convex file ID" },
800
+ { name: "organizationId", in: "query", required: false, kind: "string", description: "Convex organization ID \u2014 get it from GET /v1/me or the get_me tool (memberships[].orgId), not the organization name. Optional with a company key or an app's key: the key's company is used." }
801
+ ]
802
+ },
803
+ get_folder: {
804
+ method: "GET",
805
+ path: "/folders/{id}",
806
+ tag: "Folders",
807
+ summary: "Get a folder by ID",
808
+ body: false,
809
+ response: "json",
810
+ params: [
811
+ { name: "id", in: "path", required: true, kind: "string", description: "Convex folder ID" }
812
+ ]
813
+ },
814
+ get_labor_crew: {
815
+ method: "GET",
816
+ path: "/labor/crews/{id}",
817
+ tag: "Labor",
818
+ summary: "Get a labor crew",
819
+ body: false,
820
+ response: "json",
821
+ params: [
822
+ { name: "id", in: "path", required: true, kind: "string" },
823
+ { name: "organizationId", in: "query", required: false, kind: "string", description: "Convex organization ID \u2014 get it from GET /v1/me or the get_me tool (memberships[].orgId), not the organization name. Optional with a company key or an app's key: the key's company is used." }
824
+ ]
825
+ },
826
+ get_labor_equipment: {
827
+ method: "GET",
828
+ path: "/labor/equipment/{id}",
829
+ tag: "Labor",
830
+ summary: "Get a labor equipment entry",
831
+ body: false,
832
+ response: "json",
833
+ params: [
834
+ { name: "id", in: "path", required: true, kind: "string" },
835
+ { name: "organizationId", in: "query", required: false, kind: "string", description: "Convex organization ID \u2014 get it from GET /v1/me or the get_me tool (memberships[].orgId), not the organization name. Optional with a company key or an app's key: the key's company is used." }
836
+ ]
837
+ },
838
+ get_labor_rate: {
839
+ method: "GET",
840
+ path: "/labor/rates/{id}",
841
+ tag: "Labor",
842
+ summary: "Get a labor rate",
843
+ body: false,
844
+ response: "json",
845
+ params: [
846
+ { name: "id", in: "path", required: true, kind: "string" },
847
+ { name: "organizationId", in: "query", required: false, kind: "string", description: "Convex organization ID \u2014 get it from GET /v1/me or the get_me tool (memberships[].orgId), not the organization name. Optional with a company key or an app's key: the key's company is used." }
848
+ ]
849
+ },
850
+ get_latest_assessment: {
851
+ method: "GET",
852
+ path: "/projects/{id}/assessments/latest",
853
+ tag: "Takeoff",
854
+ summary: "Get the most recent assessment with elements",
855
+ body: false,
856
+ response: "json",
857
+ params: [
858
+ { name: "id", in: "path", required: true, kind: "string", description: "Convex project ID" }
859
+ ]
860
+ },
861
+ get_list: {
862
+ method: "GET",
863
+ path: "/lists/{id}",
864
+ tag: "Lists",
865
+ summary: "Get a single list",
866
+ body: false,
867
+ response: "json",
868
+ params: [
869
+ { name: "id", in: "path", required: true, kind: "string", description: "Convex product list ID" },
870
+ { name: "organizationId", in: "query", required: false, kind: "string", description: "Convex organization ID \u2014 get it from GET /v1/me or the get_me tool (memberships[].orgId), not the organization name. Optional with a company key or an app's key: the key's company is used." }
871
+ ]
872
+ },
873
+ get_me: {
874
+ method: "GET",
875
+ path: "/me",
876
+ tag: "Account",
877
+ summary: "Get the authenticated caller",
878
+ body: false,
879
+ response: "json",
880
+ params: []
881
+ },
882
+ get_measurement: {
883
+ method: "GET",
884
+ path: "/pages/{pageId}/measurements/{shapeId}",
885
+ tag: "Takeoff",
886
+ summary: "Get a measurement by shape ID",
887
+ body: false,
888
+ response: "json",
889
+ params: [
890
+ { name: "pageId", in: "path", required: true, kind: "string", description: "Convex page ID" },
891
+ { name: "shapeId", in: "path", required: true, kind: "string" }
892
+ ]
893
+ },
894
+ get_page: {
895
+ method: "GET",
896
+ path: "/pages/{id}",
897
+ tag: "Pages",
898
+ summary: "Get a single page",
899
+ body: false,
900
+ response: "json",
901
+ params: [
902
+ { name: "id", in: "path", required: true, kind: "string", description: "Convex page ID" }
903
+ ]
904
+ },
905
+ get_page_content: {
906
+ method: "GET",
907
+ path: "/pages/{id}/content",
908
+ tag: "Pages",
909
+ summary: "Extracted text and AI description for a page",
910
+ body: false,
911
+ response: "json",
912
+ params: [
913
+ { name: "id", in: "path", required: true, kind: "string", description: "Convex page ID" }
914
+ ]
915
+ },
916
+ get_prices_for_product: {
917
+ method: "GET",
918
+ path: "/products/{id}/prices",
919
+ tag: "Vendor prices",
920
+ summary: "List vendor prices for a product",
921
+ body: false,
922
+ response: "json",
923
+ params: [
924
+ { name: "id", in: "path", required: true, kind: "string", description: "Convex product ID" },
925
+ { name: "organizationId", in: "query", required: false, kind: "string", description: "Convex organization ID \u2014 get it from GET /v1/me or the get_me tool (memberships[].orgId), not the organization name. Optional with a company key or an app's key: the key's company is used." }
926
+ ]
927
+ },
928
+ get_product: {
929
+ method: "GET",
930
+ path: "/products/{id}",
931
+ tag: "Products",
932
+ summary: "Get a single product",
933
+ body: false,
934
+ response: "json",
935
+ params: [
936
+ { name: "id", in: "path", required: true, kind: "string", description: "Convex product ID" },
937
+ { name: "organizationId", in: "query", required: false, kind: "string", description: "Convex organization ID \u2014 get it from GET /v1/me or the get_me tool (memberships[].orgId), not the organization name. Optional with a company key or an app's key: the key's company is used." }
938
+ ]
939
+ },
940
+ get_project: {
941
+ method: "GET",
942
+ path: "/projects/{id}",
943
+ tag: "Projects",
944
+ summary: "Get a single project",
945
+ body: false,
946
+ response: "json",
947
+ params: [
948
+ { name: "id", in: "path", required: true, kind: "string", description: "Convex project ID" },
949
+ { name: "organizationId", in: "query", required: false, kind: "string", description: "Convex organization ID \u2014 get it from GET /v1/me or the get_me tool (memberships[].orgId), not the organization name. Optional with a company key or an app's key: the key's company is used." }
950
+ ]
951
+ },
952
+ get_project_relations: {
953
+ method: "GET",
954
+ path: "/projects/{id}/relations",
955
+ tag: "Projects",
956
+ summary: "Get a project with page/estimate counts",
957
+ body: false,
958
+ response: "json",
959
+ params: [
960
+ { name: "id", in: "path", required: true, kind: "string", description: "Convex project ID" },
961
+ { name: "organizationId", in: "query", required: false, kind: "string", description: "Convex organization ID \u2014 get it from GET /v1/me or the get_me tool (memberships[].orgId), not the organization name. Optional with a company key or an app's key: the key's company is used." }
962
+ ]
963
+ },
964
+ get_spreadsheet: {
965
+ method: "GET",
966
+ path: "/spreadsheets/{id}",
967
+ tag: "Spreadsheets",
968
+ summary: "Get a single spreadsheet",
969
+ body: false,
970
+ response: "json",
971
+ params: [
972
+ { name: "id", in: "path", required: true, kind: "string", description: "Convex spreadsheet ID" },
973
+ { name: "organizationId", in: "query", required: false, kind: "string", description: "Convex organization ID \u2014 get it from GET /v1/me or the get_me tool (memberships[].orgId), not the organization name. Optional with a company key or an app's key: the key's company is used." }
974
+ ]
975
+ },
976
+ get_symbol: {
977
+ method: "GET",
978
+ path: "/symbols/{id}",
979
+ tag: "Takeoff",
980
+ summary: "Get a takeoff symbol",
981
+ body: false,
982
+ response: "json",
983
+ params: [
984
+ { name: "id", in: "path", required: true, kind: "string", description: "Convex takeoff symbol ID" },
985
+ { name: "projectId", in: "query", required: true, kind: "string" }
986
+ ]
987
+ },
988
+ get_symbol_line_item: {
989
+ method: "GET",
990
+ path: "/symbols/{symbolId}/line-items/{id}",
991
+ tag: "Takeoff",
992
+ summary: "Get a single line item on a symbol",
993
+ body: false,
994
+ response: "json",
995
+ params: [
996
+ { name: "symbolId", in: "path", required: true, kind: "string", description: "Convex takeoff symbol ID" },
997
+ { name: "id", in: "path", required: true, kind: "string" },
998
+ { name: "projectId", in: "query", required: true, kind: "string" }
999
+ ]
1000
+ },
1001
+ get_takeoff_summary: {
1002
+ method: "GET",
1003
+ path: "/projects/{id}/takeoff-summary",
1004
+ tag: "Takeoff",
1005
+ summary: "Aggregated totals across takeoff symbols in a project",
1006
+ body: false,
1007
+ response: "json",
1008
+ params: [
1009
+ { name: "id", in: "path", required: true, kind: "string", description: "Convex project ID" }
1010
+ ]
1011
+ },
1012
+ get_unread_notification_count: {
1013
+ method: "GET",
1014
+ path: "/notifications/unread-count",
1015
+ tag: "Notifications",
1016
+ summary: "Get unread notification count",
1017
+ body: false,
1018
+ response: "json",
1019
+ params: []
1020
+ },
1021
+ get_usage: {
1022
+ method: "GET",
1023
+ path: "/usage",
1024
+ tag: "Credits",
1025
+ summary: "Get AI usage by app, key, end user or operation",
1026
+ body: false,
1027
+ response: "json",
1028
+ params: [
1029
+ { name: "organizationId", in: "query", required: false, kind: "string", description: "Convex organization ID \u2014 get it from GET /v1/me or the get_me tool (memberships[].orgId), not the organization name. Optional with a company key or an app's key: the key's company is used." },
1030
+ { name: "groupBy", in: "query", required: true, kind: "string", enum: ["app", "key", "end_user", "operation"], description: "What to group by: app, key, end user (the Exayard-End-User header) or operation." },
1031
+ { name: "dateFrom", in: "query", required: false, kind: "number", description: "Start of the window, epoch ms, inclusive. Default: this billing month." },
1032
+ { name: "dateTo", in: "query", required: false, kind: "number", description: "End of the window, epoch ms, exclusive. Default: now when dateFrom is given, else the end of this billing month." },
1033
+ { name: "appId", in: "query", required: false, kind: "string", description: "Only the usage of this app." },
1034
+ { name: "appKeyId", in: "query", required: false, kind: "string", description: "Only the usage of this key." },
1035
+ { name: "endUser", in: "query", required: false, kind: "string", description: "Only the usage of this end user." }
1036
+ ]
1037
+ },
1038
+ get_vendor: {
1039
+ method: "GET",
1040
+ path: "/vendors/{id}",
1041
+ tag: "Vendors",
1042
+ summary: "Get a single vendor",
1043
+ body: false,
1044
+ response: "json",
1045
+ params: [
1046
+ { name: "id", in: "path", required: true, kind: "string", description: "Convex vendor ID" },
1047
+ { name: "organizationId", in: "query", required: false, kind: "string", description: "Convex organization ID \u2014 get it from GET /v1/me or the get_me tool (memberships[].orgId), not the organization name. Optional with a company key or an app's key: the key's company is used." }
1048
+ ]
1049
+ },
1050
+ get_vendor_quote: {
1051
+ method: "GET",
1052
+ path: "/quotes/{id}",
1053
+ tag: "Vendor quotes",
1054
+ summary: "Get a vendor quote",
1055
+ body: false,
1056
+ response: "json",
1057
+ params: [
1058
+ { name: "id", in: "path", required: true, kind: "string" },
1059
+ { name: "organizationId", in: "query", required: false, kind: "string", description: "Convex organization ID \u2014 get it from GET /v1/me or the get_me tool (memberships[].orgId), not the organization name. Optional with a company key or an app's key: the key's company is used." }
1060
+ ]
1061
+ },
1062
+ get_widget: {
1063
+ method: "GET",
1064
+ path: "/widgets/{id}",
1065
+ tag: "Widgets",
1066
+ summary: "Get a widget",
1067
+ body: false,
1068
+ response: "json",
1069
+ params: [
1070
+ { name: "id", in: "path", required: true, kind: "string", description: "Convex widget ID" },
1071
+ { name: "organizationId", in: "query", required: false, kind: "string", description: "Convex organization ID \u2014 get it from GET /v1/me or the get_me tool (memberships[].orgId), not the organization name. Optional with a company key or an app's key: the key's company is used." }
1072
+ ]
1073
+ },
1074
+ list_assessments: {
1075
+ method: "GET",
1076
+ path: "/projects/{id}/assessments",
1077
+ tag: "Takeoff",
1078
+ summary: "List assessments for a project",
1079
+ body: false,
1080
+ response: "json",
1081
+ params: [
1082
+ { name: "id", in: "path", required: true, kind: "string", description: "Convex project ID" }
1083
+ ]
1084
+ },
1085
+ list_contacts: {
1086
+ method: "GET",
1087
+ path: "/contacts",
1088
+ tag: "Contacts",
1089
+ summary: "List contacts captured by widgets",
1090
+ body: false,
1091
+ response: "json",
1092
+ params: [
1093
+ { name: "organizationId", in: "query", required: false, kind: "string", description: "Convex organization ID \u2014 get it from GET /v1/me or the get_me tool (memberships[].orgId), not the organization name. Optional with a company key or an app's key: the key's company is used." },
1094
+ { name: "widgetId", in: "query", required: false, kind: "string" },
1095
+ { name: "search", in: "query", required: false, kind: "string" },
1096
+ { name: "limit", in: "query", required: false, kind: "integer" }
1097
+ ]
1098
+ },
1099
+ list_conversation_files: {
1100
+ method: "GET",
1101
+ path: "/conversations/{id}/files",
1102
+ tag: "Conversations",
1103
+ summary: "List files uploaded in a conversation",
1104
+ body: false,
1105
+ response: "json",
1106
+ params: [
1107
+ { name: "id", in: "path", required: true, kind: "string", description: "Convex conversation ID" },
1108
+ { name: "organizationId", in: "query", required: false, kind: "string", description: "Convex organization ID \u2014 get it from GET /v1/me or the get_me tool (memberships[].orgId), not the organization name. Optional with a company key or an app's key: the key's company is used." },
1109
+ { name: "imagesOnly", in: "query", required: false, kind: "boolean" }
1110
+ ]
1111
+ },
1112
+ list_conversations: {
1113
+ method: "GET",
1114
+ path: "/conversations",
1115
+ tag: "Conversations",
1116
+ summary: "List conversations for the caller",
1117
+ body: false,
1118
+ response: "json",
1119
+ params: [
1120
+ { name: "organizationId", in: "query", required: false, kind: "string", description: "Convex organization ID \u2014 get it from GET /v1/me or the get_me tool (memberships[].orgId), not the organization name. Optional with a company key or an app's key: the key's company is used." },
1121
+ { name: "type", in: "query", required: false, kind: "string", enum: ["ai_chat", "project_activity", "whatsapp_chat"] },
1122
+ { name: "channel", in: "query", required: false, kind: "string", enum: ["app", "email", "whatsapp"] },
1123
+ { name: "limit", in: "query", required: false, kind: "integer" },
1124
+ { name: "cursor", in: "query", required: false, kind: "number" }
1125
+ ]
1126
+ },
1127
+ list_custom_properties: {
1128
+ method: "GET",
1129
+ path: "/custom-properties",
1130
+ tag: "Custom properties",
1131
+ summary: "List custom properties in an organization",
1132
+ body: false,
1133
+ response: "json",
1134
+ params: [
1135
+ { name: "organizationId", in: "query", required: false, kind: "string", description: "Convex organization ID \u2014 get it from GET /v1/me or the get_me tool (memberships[].orgId), not the organization name. Optional with a company key or an app's key: the key's company is used." },
1136
+ { name: "recordType", in: "query", required: false, kind: "string", enum: ["project", "countTakeoff", "linearTakeoff", "areaTakeoff", "product"] }
1137
+ ]
1138
+ },
1139
+ list_documents: {
1140
+ method: "GET",
1141
+ path: "/projects/{id}/documents",
1142
+ tag: "Documents",
1143
+ summary: "List documents in a project",
1144
+ body: false,
1145
+ response: "json",
1146
+ params: [
1147
+ { name: "id", in: "path", required: true, kind: "string", description: "Convex project ID" },
1148
+ { name: "organizationId", in: "query", required: false, kind: "string", description: "Convex organization ID \u2014 get it from GET /v1/me or the get_me tool (memberships[].orgId), not the organization name. Optional with a company key or an app's key: the key's company is used." }
1149
+ ]
1150
+ },
1151
+ list_files: {
1152
+ method: "GET",
1153
+ path: "/files",
1154
+ tag: "Files",
1155
+ summary: "List files in a project",
1156
+ body: false,
1157
+ response: "json",
1158
+ params: [
1159
+ { name: "projectId", in: "query", required: true, kind: "string" },
1160
+ { name: "organizationId", in: "query", required: false, kind: "string", description: "Convex organization ID \u2014 get it from GET /v1/me or the get_me tool (memberships[].orgId), not the organization name. Optional with a company key or an app's key: the key's company is used." }
1161
+ ]
1162
+ },
1163
+ list_folder_contents: {
1164
+ method: "GET",
1165
+ path: "/folders/contents",
1166
+ tag: "Folders",
1167
+ summary: "List files and folders inside a folder",
1168
+ body: false,
1169
+ response: "json",
1170
+ params: [
1171
+ { name: "projectId", in: "query", required: true, kind: "string", description: "Convex project ID" },
1172
+ { name: "parentId", in: "query", required: false, kind: "string" }
1173
+ ]
1174
+ },
1175
+ list_folders: {
1176
+ method: "GET",
1177
+ path: "/folders",
1178
+ tag: "Folders",
1179
+ summary: "List folders in a project",
1180
+ body: false,
1181
+ response: "json",
1182
+ params: [
1183
+ { name: "projectId", in: "query", required: true, kind: "string", description: "Convex project ID" }
1184
+ ]
1185
+ },
1186
+ list_groups: {
1187
+ method: "GET",
1188
+ path: "/projects/{id}/groups",
1189
+ tag: "Takeoff",
1190
+ summary: "List takeoff groups in a project",
1191
+ body: false,
1192
+ response: "json",
1193
+ params: [
1194
+ { name: "id", in: "path", required: true, kind: "string", description: "Convex project ID" }
1195
+ ]
1196
+ },
1197
+ list_installations: {
1198
+ method: "GET",
1199
+ path: "/installations",
1200
+ tag: "Account",
1201
+ summary: "List the companies where the calling app is installed",
1202
+ body: false,
1203
+ response: "json",
1204
+ params: [
1205
+ { name: "limit", in: "query", required: false, kind: "integer", description: "Companies per page (1 to 100, default 50)." },
1206
+ { name: "cursor", in: "query", required: false, kind: "string", description: "`nextCursor` from the previous page." }
1207
+ ]
1208
+ },
1209
+ list_integrations: {
1210
+ method: "GET",
1211
+ path: "/integrations",
1212
+ tag: "Integrations",
1213
+ summary: "List integrations connected to an organization",
1214
+ body: false,
1215
+ response: "json",
1216
+ params: [
1217
+ { name: "organizationId", in: "query", required: false, kind: "string", description: "Convex organization ID \u2014 get it from GET /v1/me or the get_me tool (memberships[].orgId), not the organization name. Optional with a company key or an app's key: the key's company is used." }
1218
+ ]
1219
+ },
1220
+ list_labor_crews: {
1221
+ method: "GET",
1222
+ path: "/labor/crews",
1223
+ tag: "Labor",
1224
+ summary: "List labor crews",
1225
+ body: false,
1226
+ response: "json",
1227
+ params: [
1228
+ { name: "organizationId", in: "query", required: false, kind: "string", description: "Convex organization ID \u2014 get it from GET /v1/me or the get_me tool (memberships[].orgId), not the organization name. Optional with a company key or an app's key: the key's company is used." },
1229
+ { name: "includeInactive", in: "query", required: false, kind: "boolean" }
1230
+ ]
1231
+ },
1232
+ list_labor_equipment: {
1233
+ method: "GET",
1234
+ path: "/labor/equipment",
1235
+ tag: "Labor",
1236
+ summary: "List labor equipment",
1237
+ body: false,
1238
+ response: "json",
1239
+ params: [
1240
+ { name: "organizationId", in: "query", required: false, kind: "string", description: "Convex organization ID \u2014 get it from GET /v1/me or the get_me tool (memberships[].orgId), not the organization name. Optional with a company key or an app's key: the key's company is used." },
1241
+ { name: "category", in: "query", required: false, kind: "string" },
1242
+ { name: "includeInactive", in: "query", required: false, kind: "boolean" }
1243
+ ]
1244
+ },
1245
+ list_labor_rates: {
1246
+ method: "GET",
1247
+ path: "/labor/rates",
1248
+ tag: "Labor",
1249
+ summary: "List labor rates",
1250
+ body: false,
1251
+ response: "json",
1252
+ params: [
1253
+ { name: "organizationId", in: "query", required: false, kind: "string", description: "Convex organization ID \u2014 get it from GET /v1/me or the get_me tool (memberships[].orgId), not the organization name. Optional with a company key or an app's key: the key's company is used." },
1254
+ { name: "includeInactive", in: "query", required: false, kind: "boolean" }
1255
+ ]
1256
+ },
1257
+ list_layers: {
1258
+ method: "GET",
1259
+ path: "/projects/{id}/layers",
1260
+ tag: "Takeoff",
1261
+ summary: "List takeoff layers in a project",
1262
+ body: false,
1263
+ response: "json",
1264
+ params: [
1265
+ { name: "id", in: "path", required: true, kind: "string", description: "Convex project ID" },
1266
+ { name: "organizationId", in: "query", required: false, kind: "string", description: "Convex organization ID \u2014 get it from GET /v1/me or the get_me tool (memberships[].orgId), not the organization name. Optional with a company key or an app's key: the key's company is used." }
1267
+ ]
1268
+ },
1269
+ list_lists: {
1270
+ method: "GET",
1271
+ path: "/lists",
1272
+ tag: "Lists",
1273
+ summary: "List product lists and takeoff templates",
1274
+ body: false,
1275
+ response: "json",
1276
+ params: [
1277
+ { name: "organizationId", in: "query", required: false, kind: "string", description: "Convex organization ID \u2014 get it from GET /v1/me or the get_me tool (memberships[].orgId), not the organization name. Optional with a company key or an app's key: the key's company is used." },
1278
+ { name: "type", in: "query", required: false, kind: "string", enum: ["product", "takeoffTemplate"] },
1279
+ { name: "category", in: "query", required: false, kind: "string" },
1280
+ { name: "includeInactive", in: "query", required: false, kind: "boolean" }
1281
+ ]
1282
+ },
1283
+ list_measurements_by_page: {
1284
+ method: "GET",
1285
+ path: "/pages/{id}/measurements",
1286
+ tag: "Takeoff",
1287
+ summary: "List measurements on a page",
1288
+ body: false,
1289
+ response: "json",
1290
+ params: [
1291
+ { name: "id", in: "path", required: true, kind: "string", description: "Convex page ID" }
1292
+ ]
1293
+ },
1294
+ list_measurements_by_symbol: {
1295
+ method: "GET",
1296
+ path: "/symbols/{id}/measurements",
1297
+ tag: "Takeoff",
1298
+ summary: "List measurements for a symbol",
1299
+ body: false,
1300
+ response: "json",
1301
+ params: [
1302
+ { name: "id", in: "path", required: true, kind: "string", description: "Convex takeoff symbol ID" },
1303
+ { name: "projectId", in: "query", required: true, kind: "string" }
1304
+ ]
1305
+ },
1306
+ list_messages: {
1307
+ method: "GET",
1308
+ path: "/projects/{id}/messages",
1309
+ tag: "Messages",
1310
+ summary: "List project activity messages",
1311
+ body: false,
1312
+ response: "json",
1313
+ params: [
1314
+ { name: "id", in: "path", required: true, kind: "string", description: "Convex project ID" },
1315
+ { name: "pageId", in: "query", required: false, kind: "string" }
1316
+ ]
1317
+ },
1318
+ list_notifications: {
1319
+ method: "GET",
1320
+ path: "/notifications",
1321
+ tag: "Notifications",
1322
+ summary: "List notifications for the caller",
1323
+ body: false,
1324
+ response: "json",
1325
+ params: [
1326
+ { name: "limit", in: "query", required: false, kind: "integer" },
1327
+ { name: "cursor", in: "query", required: false, kind: "string" }
1328
+ ]
1329
+ },
1330
+ list_org_documents: {
1331
+ method: "GET",
1332
+ path: "/documents",
1333
+ tag: "Documents",
1334
+ summary: "List documents in the organization",
1335
+ body: false,
1336
+ response: "json",
1337
+ params: [
1338
+ { name: "organizationId", in: "query", required: false, kind: "string", description: "Convex organization ID \u2014 get it from GET /v1/me or the get_me tool (memberships[].orgId), not the organization name. Optional with a company key or an app's key: the key's company is used." },
1339
+ { name: "type", in: "query", required: false, kind: "string" },
1340
+ { name: "limit", in: "query", required: false, kind: "integer" }
1341
+ ]
1342
+ },
1343
+ list_organizations: {
1344
+ method: "GET",
1345
+ path: "/organizations",
1346
+ tag: "Account",
1347
+ summary: "List the companies your app's company owns",
1348
+ body: false,
1349
+ response: "json",
1350
+ params: [
1351
+ { name: "limit", in: "query", required: false, kind: "integer", description: "Companies per page (1 to 100, default 50)." },
1352
+ { name: "cursor", in: "query", required: false, kind: "string", description: "`nextCursor` from the previous page." }
1353
+ ]
1354
+ },
1355
+ list_pages: {
1356
+ method: "GET",
1357
+ path: "/projects/{id}/pages",
1358
+ tag: "Pages",
1359
+ summary: "List pages in a project, grouped by file",
1360
+ body: false,
1361
+ response: "json",
1362
+ params: [
1363
+ { name: "id", in: "path", required: true, kind: "string", description: "Convex project ID" },
1364
+ { name: "organizationId", in: "query", required: false, kind: "string", description: "Convex organization ID \u2014 get it from GET /v1/me or the get_me tool (memberships[].orgId), not the organization name. Optional with a company key or an app's key: the key's company is used." }
1365
+ ]
1366
+ },
1367
+ list_products: {
1368
+ method: "GET",
1369
+ path: "/products",
1370
+ tag: "Products",
1371
+ summary: "List products in an organization",
1372
+ body: false,
1373
+ response: "json",
1374
+ params: [
1375
+ { name: "organizationId", in: "query", required: false, kind: "string", description: "Convex organization ID \u2014 get it from GET /v1/me or the get_me tool (memberships[].orgId), not the organization name. Optional with a company key or an app's key: the key's company is used." },
1376
+ { name: "search", in: "query", required: false, kind: "string" },
1377
+ { name: "limit", in: "query", required: false, kind: "integer" }
1378
+ ]
1379
+ },
1380
+ list_products_in_list: {
1381
+ method: "GET",
1382
+ path: "/lists/{id}/products",
1383
+ tag: "Products",
1384
+ summary: "List products in a list",
1385
+ body: false,
1386
+ response: "json",
1387
+ params: [
1388
+ { name: "id", in: "path", required: true, kind: "string", description: "Convex product list ID" },
1389
+ { name: "organizationId", in: "query", required: false, kind: "string", description: "Convex organization ID \u2014 get it from GET /v1/me or the get_me tool (memberships[].orgId), not the organization name. Optional with a company key or an app's key: the key's company is used." }
1390
+ ]
1391
+ },
1392
+ list_project_conversations: {
1393
+ method: "GET",
1394
+ path: "/projects/{id}/conversations",
1395
+ tag: "Conversations",
1396
+ summary: "List conversations in a project",
1397
+ body: false,
1398
+ response: "json",
1399
+ params: [
1400
+ { name: "id", in: "path", required: true, kind: "string", description: "Convex project ID" },
1401
+ { name: "organizationId", in: "query", required: false, kind: "string", description: "Convex organization ID \u2014 get it from GET /v1/me or the get_me tool (memberships[].orgId), not the organization name. Optional with a company key or an app's key: the key's company is used." },
1402
+ { name: "type", in: "query", required: false, kind: "string", enum: ["ai_chat", "project_activity", "widget_chat"] }
1403
+ ]
1404
+ },
1405
+ list_projects: {
1406
+ method: "GET",
1407
+ path: "/projects",
1408
+ tag: "Projects",
1409
+ summary: "List projects in an organization",
1410
+ body: false,
1411
+ response: "json",
1412
+ params: [
1413
+ { name: "organizationId", in: "query", required: false, kind: "string", description: "Convex organization ID \u2014 get it from GET /v1/me or the get_me tool (memberships[].orgId), not the organization name. Optional with a company key or an app's key: the key's company is used." },
1414
+ { name: "status", in: "query", required: false, kind: "string", enum: ["lead", "contacted", "qualified", "active", "archived"] },
1415
+ { name: "search", in: "query", required: false, kind: "string" }
1416
+ ]
1417
+ },
1418
+ list_spreadsheets: {
1419
+ method: "GET",
1420
+ path: "/projects/{id}/spreadsheets",
1421
+ tag: "Spreadsheets",
1422
+ summary: "List spreadsheets in a project",
1423
+ body: false,
1424
+ response: "json",
1425
+ params: [
1426
+ { name: "id", in: "path", required: true, kind: "string", description: "Convex project ID" },
1427
+ { name: "organizationId", in: "query", required: false, kind: "string", description: "Convex organization ID \u2014 get it from GET /v1/me or the get_me tool (memberships[].orgId), not the organization name. Optional with a company key or an app's key: the key's company is used." }
1428
+ ]
1429
+ },
1430
+ list_symbol_line_items: {
1431
+ method: "GET",
1432
+ path: "/symbols/{id}/line-items",
1433
+ tag: "Takeoff",
1434
+ summary: "List all line items on a takeoff symbol",
1435
+ body: false,
1436
+ response: "json",
1437
+ params: [
1438
+ { name: "id", in: "path", required: true, kind: "string", description: "Convex takeoff symbol ID" },
1439
+ { name: "projectId", in: "query", required: true, kind: "string" }
1440
+ ]
1441
+ },
1442
+ list_symbols: {
1443
+ method: "GET",
1444
+ path: "/projects/{id}/symbols",
1445
+ tag: "Takeoff",
1446
+ summary: "List takeoff symbols in a project",
1447
+ body: false,
1448
+ response: "json",
1449
+ params: [
1450
+ { name: "id", in: "path", required: true, kind: "string", description: "Convex project ID" },
1451
+ { name: "type", in: "query", required: false, kind: "string", enum: ["count", "linear", "area"] },
1452
+ { name: "source", in: "query", required: false, kind: "string", enum: ["ai", "manual"] },
1453
+ { name: "category", in: "query", required: false, kind: "string" }
1454
+ ]
1455
+ },
1456
+ list_vendor_quotes: {
1457
+ method: "GET",
1458
+ path: "/projects/{id}/quotes",
1459
+ tag: "Vendor quotes",
1460
+ summary: "List vendor quotes on a project",
1461
+ body: false,
1462
+ response: "json",
1463
+ params: [
1464
+ { name: "id", in: "path", required: true, kind: "string", description: "Convex project ID" },
1465
+ { name: "organizationId", in: "query", required: false, kind: "string", description: "Convex organization ID \u2014 get it from GET /v1/me or the get_me tool (memberships[].orgId), not the organization name. Optional with a company key or an app's key: the key's company is used." },
1466
+ { name: "status", in: "query", required: false, kind: "string", enum: ["requested", "received", "accepted", "rejected", "expired"] }
1467
+ ]
1468
+ },
1469
+ list_vendors: {
1470
+ method: "GET",
1471
+ path: "/vendors",
1472
+ tag: "Vendors",
1473
+ summary: "List vendors in an organization",
1474
+ body: false,
1475
+ response: "json",
1476
+ params: [
1477
+ { name: "organizationId", in: "query", required: false, kind: "string", description: "Convex organization ID \u2014 get it from GET /v1/me or the get_me tool (memberships[].orgId), not the organization name. Optional with a company key or an app's key: the key's company is used." },
1478
+ { name: "type", in: "query", required: false, kind: "string", enum: ["supplier", "subcontractor", "rental"] },
1479
+ { name: "kind", in: "query", required: false, kind: "string", enum: ["sub", "supplier", "surety", "insurer", "agency", "owner", "architect", "engineer", "other"] },
1480
+ { name: "preferredOnly", in: "query", required: false, kind: "boolean" },
1481
+ { name: "search", in: "query", required: false, kind: "string" },
1482
+ { name: "limit", in: "query", required: false, kind: "integer" }
1483
+ ]
1484
+ },
1485
+ list_webhook_deliveries: {
1486
+ method: "GET",
1487
+ path: "/webhook_endpoints/{id}/deliveries",
1488
+ tag: "Webhooks",
1489
+ summary: "List deliveries for a webhook endpoint",
1490
+ body: false,
1491
+ response: "json",
1492
+ params: [
1493
+ { name: "id", in: "path", required: true, kind: "string" },
1494
+ { name: "organizationId", in: "query", required: false, kind: "string", description: "Convex organization ID \u2014 get it from GET /v1/me or the get_me tool (memberships[].orgId), not the organization name. Optional with a company key or an app's key: the key's company is used." },
1495
+ { name: "limit", in: "query", required: false, kind: "integer" }
1496
+ ]
1497
+ },
1498
+ list_webhook_endpoints: {
1499
+ method: "GET",
1500
+ path: "/webhook_endpoints",
1501
+ tag: "Webhooks",
1502
+ summary: "List webhook endpoints in an organization",
1503
+ body: false,
1504
+ response: "json",
1505
+ params: [
1506
+ { name: "organizationId", in: "query", required: false, kind: "string", description: "Convex organization ID \u2014 get it from GET /v1/me or the get_me tool (memberships[].orgId), not the organization name. Optional with a company key or an app's key: the key's company is used." }
1507
+ ]
1508
+ },
1509
+ list_widget_conversations: {
1510
+ method: "GET",
1511
+ path: "/widgets/{id}/conversations",
1512
+ tag: "Conversations",
1513
+ summary: "List a widget\u2019s inbox conversations",
1514
+ body: false,
1515
+ response: "json",
1516
+ params: [
1517
+ { name: "id", in: "path", required: true, kind: "string", description: "Convex widget ID" },
1518
+ { name: "organizationId", in: "query", required: false, kind: "string", description: "Convex organization ID \u2014 get it from GET /v1/me or the get_me tool (memberships[].orgId), not the organization name. Optional with a company key or an app's key: the key's company is used." },
1519
+ { name: "status", in: "query", required: false, kind: "string", enum: ["bot", "open", "snoozed", "closed"] },
1520
+ { name: "search", in: "query", required: false, kind: "string" },
1521
+ { name: "limit", in: "query", required: false, kind: "integer" }
1522
+ ]
1523
+ },
1524
+ list_widgets: {
1525
+ method: "GET",
1526
+ path: "/widgets",
1527
+ tag: "Widgets",
1528
+ summary: "List widgets in an organization",
1529
+ body: false,
1530
+ response: "json",
1531
+ params: [
1532
+ { name: "organizationId", in: "query", required: false, kind: "string", description: "Convex organization ID \u2014 get it from GET /v1/me or the get_me tool (memberships[].orgId), not the organization name. Optional with a company key or an app's key: the key's company is used." }
1533
+ ]
1534
+ },
1535
+ mark_all_notifications_read: {
1536
+ method: "POST",
1537
+ path: "/notifications/mark-all-read",
1538
+ tag: "Notifications",
1539
+ summary: "Mark all notifications as read",
1540
+ body: false,
1541
+ response: "empty",
1542
+ params: []
1543
+ },
1544
+ mark_notification_read: {
1545
+ method: "POST",
1546
+ path: "/notifications/{id}/read",
1547
+ tag: "Notifications",
1548
+ summary: "Mark a notification as read",
1549
+ body: false,
1550
+ response: "empty",
1551
+ params: [
1552
+ { name: "id", in: "path", required: true, kind: "string" }
1553
+ ]
1554
+ },
1555
+ patch_symbol_line_item: {
1556
+ method: "PATCH",
1557
+ path: "/symbols/{symbolId}/line-items/{id}",
1558
+ tag: "Takeoff",
1559
+ summary: "Update a line item on a symbol",
1560
+ body: true,
1561
+ response: "empty",
1562
+ params: [
1563
+ { name: "symbolId", in: "path", required: true, kind: "string", description: "Convex takeoff symbol ID" },
1564
+ { name: "id", in: "path", required: true, kind: "string" },
1565
+ { name: "projectId", in: "body", required: true, kind: "string", description: "Convex project ID" },
1566
+ { name: "patch", in: "body", required: true, kind: "object", description: "Only the fields to change" }
1567
+ ]
1568
+ },
1569
+ presign_file_upload: {
1570
+ method: "POST",
1571
+ path: "/files",
1572
+ tag: "Files",
1573
+ summary: "Start a file upload \u2014 returns a presigned R2 URL",
1574
+ body: true,
1575
+ response: "json",
1576
+ params: [
1577
+ { name: "projectId", in: "body", required: true, kind: "string", description: "Convex project ID" },
1578
+ { name: "filename", in: "body", required: true, kind: "string" },
1579
+ { name: "mimeType", in: "body", required: true, kind: "string" },
1580
+ { name: "fileSize", in: "body", required: true, kind: "integer", description: "File size in bytes (max 500 MB)" },
1581
+ { name: "folderId", in: "body", required: false, kind: "string", description: "Convex folder ID" },
1582
+ { name: "dimensions", in: "body", required: false, kind: "object" }
1583
+ ]
1584
+ },
1585
+ propose_analysis: {
1586
+ method: "POST",
1587
+ path: "/projects/{id}/analysis/propose",
1588
+ tag: "Takeoff",
1589
+ summary: "Propose an AI-generated analysis from a natural-language prompt",
1590
+ body: true,
1591
+ response: "json",
1592
+ params: [
1593
+ { name: "id", in: "path", required: true, kind: "string", description: "Convex project ID" },
1594
+ { name: "organizationId", in: "body", required: false, kind: "string", description: "Convex organization ID \u2014 get it from GET /v1/me or the get_me tool (memberships[].orgId), not the organization name. Optional with a company key or an app's key: the key's company is used." },
1595
+ { name: "prompt", in: "body", required: true, kind: "string", description: "Natural-language description of what to measure" },
1596
+ { name: "pageIds", in: "body", required: false, kind: "array", description: "Page IDs to propose against. Defaults to the project's first page." },
1597
+ { name: "fileIds", in: "body", required: false, kind: "array", description: "File IDs to scope the proposal to. When set, only pages belonging to these files are considered \u2014 pass the IDs from the [Attached files: ...] note in the user's message so a fresh upload doesn't pull in older project files. Wins over pageIds." }
1598
+ ]
1599
+ },
1600
+ provision_cma: {
1601
+ method: "POST",
1602
+ path: "/cma/provision",
1603
+ tag: "Account",
1604
+ summary: "Provision or rotate the caller\u2019s Managed Agents vault",
1605
+ body: false,
1606
+ response: "json",
1607
+ params: []
1608
+ },
1609
+ receive_vendor_quote: {
1610
+ method: "PATCH",
1611
+ path: "/quotes/{id}/receive",
1612
+ tag: "Vendor quotes",
1613
+ summary: "Record a vendor quote response",
1614
+ body: true,
1615
+ response: "json",
1616
+ params: [
1617
+ { name: "id", in: "path", required: true, kind: "string" },
1618
+ { name: "organizationId", in: "body", required: false, kind: "string", description: "Convex organization ID \u2014 get it from GET /v1/me or the get_me tool (memberships[].orgId), not the organization name. Optional with a company key or an app's key: the key's company is used." },
1619
+ { name: "lineItems", in: "body", required: true, kind: "array" },
1620
+ { name: "subtotal", in: "body", required: true, kind: "number", description: "Sum of the line items' totalPrice when lineItems are sent" },
1621
+ { name: "tax", in: "body", required: false, kind: "number" },
1622
+ { name: "shipping", in: "body", required: false, kind: "number" },
1623
+ { name: "total", in: "body", required: true, kind: "number", description: "Must equal subtotal + tax + shipping (an absent amount counts as 0)" },
1624
+ { name: "validUntil", in: "body", required: false, kind: "number" }
1625
+ ]
1626
+ },
1627
+ remove_custom_property: {
1628
+ method: "DELETE",
1629
+ path: "/custom-properties/{id}",
1630
+ tag: "Custom properties",
1631
+ summary: "Archive a custom property",
1632
+ body: false,
1633
+ response: "empty",
1634
+ params: [
1635
+ { name: "id", in: "path", required: true, kind: "string", description: "Convex custom property ID" },
1636
+ { name: "organizationId", in: "query", required: false, kind: "string", description: "Convex organization ID \u2014 get it from GET /v1/me or the get_me tool (memberships[].orgId), not the organization name. Optional with a company key or an app's key: the key's company is used." }
1637
+ ]
1638
+ },
1639
+ remove_file: {
1640
+ method: "DELETE",
1641
+ path: "/files/{id}",
1642
+ tag: "Files",
1643
+ summary: "Soft-delete a file",
1644
+ body: false,
1645
+ response: "empty",
1646
+ params: [
1647
+ { name: "id", in: "path", required: true, kind: "string", description: "Convex file ID" }
1648
+ ]
1649
+ },
1650
+ remove_folder: {
1651
+ method: "DELETE",
1652
+ path: "/folders/{id}",
1653
+ tag: "Folders",
1654
+ summary: "Delete a folder",
1655
+ body: false,
1656
+ response: "empty",
1657
+ params: [
1658
+ { name: "id", in: "path", required: true, kind: "string", description: "Convex folder ID" }
1659
+ ]
1660
+ },
1661
+ remove_group: {
1662
+ method: "DELETE",
1663
+ path: "/groups/{id}",
1664
+ tag: "Takeoff",
1665
+ summary: "Delete a takeoff group",
1666
+ body: false,
1667
+ response: "empty",
1668
+ params: [
1669
+ { name: "id", in: "path", required: true, kind: "string", description: "Convex takeoff group ID" },
1670
+ { name: "projectId", in: "query", required: true, kind: "string" }
1671
+ ]
1672
+ },
1673
+ remove_layer: {
1674
+ method: "DELETE",
1675
+ path: "/layers/{id}",
1676
+ tag: "Takeoff",
1677
+ summary: "Delete a takeoff layer",
1678
+ body: false,
1679
+ response: "empty",
1680
+ params: [
1681
+ { name: "id", in: "path", required: true, kind: "string", description: "Convex takeoff layer ID" },
1682
+ { name: "projectId", in: "query", required: true, kind: "string" }
1683
+ ]
1684
+ },
1685
+ remove_measurement: {
1686
+ method: "DELETE",
1687
+ path: "/pages/{pageId}/measurements/{shapeId}",
1688
+ tag: "Takeoff",
1689
+ summary: "Delete a measurement by shape ID",
1690
+ body: false,
1691
+ response: "empty",
1692
+ params: [
1693
+ { name: "pageId", in: "path", required: true, kind: "string", description: "Convex page ID" },
1694
+ { name: "shapeId", in: "path", required: true, kind: "string" }
1695
+ ]
1696
+ },
1697
+ remove_message: {
1698
+ method: "DELETE",
1699
+ path: "/messages/{id}",
1700
+ tag: "Messages",
1701
+ summary: "Delete a message",
1702
+ body: false,
1703
+ response: "empty",
1704
+ params: [
1705
+ { name: "id", in: "path", required: true, kind: "string", description: "Convex chat message ID" }
1706
+ ]
1707
+ },
1708
+ remove_symbol: {
1709
+ method: "DELETE",
1710
+ path: "/symbols/{id}",
1711
+ tag: "Takeoff",
1712
+ summary: "Delete a takeoff symbol",
1713
+ body: false,
1714
+ response: "empty",
1715
+ params: [
1716
+ { name: "id", in: "path", required: true, kind: "string", description: "Convex takeoff symbol ID" },
1717
+ { name: "projectId", in: "query", required: true, kind: "string" }
1718
+ ]
1719
+ },
1720
+ remove_symbol_line_item: {
1721
+ method: "DELETE",
1722
+ path: "/symbols/{symbolId}/line-items/{id}",
1723
+ tag: "Takeoff",
1724
+ summary: "Remove a line item from a symbol",
1725
+ body: false,
1726
+ response: "empty",
1727
+ params: [
1728
+ { name: "symbolId", in: "path", required: true, kind: "string", description: "Convex takeoff symbol ID" },
1729
+ { name: "id", in: "path", required: true, kind: "string" },
1730
+ { name: "projectId", in: "query", required: true, kind: "string" }
1731
+ ]
1732
+ },
1733
+ remove_vendor_price: {
1734
+ method: "DELETE",
1735
+ path: "/products/{productId}/prices/{vendorId}",
1736
+ tag: "Vendor prices",
1737
+ summary: "Remove a vendor price",
1738
+ body: false,
1739
+ response: "empty",
1740
+ params: [
1741
+ { name: "productId", in: "path", required: true, kind: "string", description: "Convex product ID" },
1742
+ { name: "vendorId", in: "path", required: true, kind: "string", description: "Convex vendor ID" },
1743
+ { name: "organizationId", in: "query", required: false, kind: "string", description: "Convex organization ID \u2014 get it from GET /v1/me or the get_me tool (memberships[].orgId), not the organization name. Optional with a company key or an app's key: the key's company is used." }
1744
+ ]
1745
+ },
1746
+ remove_widget: {
1747
+ method: "DELETE",
1748
+ path: "/widgets/{id}",
1749
+ tag: "Widgets",
1750
+ summary: "Delete a widget",
1751
+ body: false,
1752
+ response: "empty",
1753
+ params: [
1754
+ { name: "id", in: "path", required: true, kind: "string", description: "Convex widget ID" },
1755
+ { name: "organizationId", in: "query", required: false, kind: "string", description: "Convex organization ID \u2014 get it from GET /v1/me or the get_me tool (memberships[].orgId), not the organization name. Optional with a company key or an app's key: the key's company is used." }
1756
+ ]
1757
+ },
1758
+ rename_file: {
1759
+ method: "PATCH",
1760
+ path: "/files/{id}",
1761
+ tag: "Files",
1762
+ summary: "Rename or move a file",
1763
+ body: true,
1764
+ response: "empty",
1765
+ params: [
1766
+ { name: "id", in: "path", required: true, kind: "string", description: "Convex file ID" },
1767
+ { name: "filename", in: "body", required: false, kind: "string" },
1768
+ { name: "folderId", in: "body", required: false, kind: "string", description: "Convex folder ID" }
1769
+ ]
1770
+ },
1771
+ rename_folder: {
1772
+ method: "PATCH",
1773
+ path: "/folders/{id}",
1774
+ tag: "Folders",
1775
+ summary: "Rename or move a folder",
1776
+ body: true,
1777
+ response: "empty",
1778
+ params: [
1779
+ { name: "id", in: "path", required: true, kind: "string", description: "Convex folder ID" },
1780
+ { name: "name", in: "body", required: false, kind: "string" },
1781
+ { name: "parentId", in: "body", required: false, kind: "string", description: "Convex folder ID" }
1782
+ ]
1783
+ },
1784
+ resend_webhook_delivery: {
1785
+ method: "POST",
1786
+ path: "/webhook_deliveries/{id}/resend",
1787
+ tag: "Webhooks",
1788
+ summary: "Resend a webhook delivery",
1789
+ body: true,
1790
+ response: "json",
1791
+ params: [
1792
+ { name: "id", in: "path", required: true, kind: "string" },
1793
+ { name: "organizationId", in: "body", required: false, kind: "string", description: "Convex organization ID \u2014 get it from GET /v1/me or the get_me tool (memberships[].orgId), not the organization name. Optional with a company key or an app's key: the key's company is used." }
1794
+ ]
1795
+ },
1796
+ restore_list: {
1797
+ method: "POST",
1798
+ path: "/lists/{id}/restore",
1799
+ tag: "Lists",
1800
+ summary: "Restore an archived list",
1801
+ body: true,
1802
+ response: "empty",
1803
+ params: [
1804
+ { name: "id", in: "path", required: true, kind: "string", description: "Convex product list ID" },
1805
+ { name: "organizationId", in: "body", required: false, kind: "string", description: "Convex organization ID \u2014 get it from GET /v1/me or the get_me tool (memberships[].orgId), not the organization name. Optional with a company key or an app's key: the key's company is used." }
1806
+ ]
1807
+ },
1808
+ restore_page: {
1809
+ method: "POST",
1810
+ path: "/pages/{id}/restore",
1811
+ tag: "Pages",
1812
+ summary: "Restore an archived page",
1813
+ body: false,
1814
+ response: "empty",
1815
+ params: [
1816
+ { name: "id", in: "path", required: true, kind: "string", description: "Convex page ID" }
1817
+ ]
1818
+ },
1819
+ restore_project: {
1820
+ method: "POST",
1821
+ path: "/projects/{id}/restore",
1822
+ tag: "Projects",
1823
+ summary: "Restore an archived project",
1824
+ body: true,
1825
+ response: "empty",
1826
+ params: [
1827
+ { name: "id", in: "path", required: true, kind: "string", description: "Convex project ID" },
1828
+ { name: "organizationId", in: "body", required: false, kind: "string", description: "Convex organization ID \u2014 get it from GET /v1/me or the get_me tool (memberships[].orgId), not the organization name. Optional with a company key or an app's key: the key's company is used." }
1829
+ ]
1830
+ },
1831
+ run_approved_analysis: {
1832
+ method: "POST",
1833
+ path: "/projects/{id}/analysis/run",
1834
+ tag: "Takeoff",
1835
+ summary: "Run an approved analysis (kicks off a background AI task)",
1836
+ confirm: "This operation uses AI usage.",
1837
+ body: true,
1838
+ response: "json",
1839
+ params: [
1840
+ { name: "id", in: "path", required: true, kind: "string", description: "Convex project ID" },
1841
+ { name: "organizationId", in: "body", required: false, kind: "string", description: "Convex organization ID \u2014 get it from GET /v1/me or the get_me tool (memberships[].orgId), not the organization name. Optional with a company key or an app's key: the key's company is used." },
1842
+ { name: "pageIds", in: "body", required: true, kind: "array" },
1843
+ { name: "elements", in: "body", required: true, kind: "array" },
1844
+ { name: "layerName", in: "body", required: false, kind: "string" }
1845
+ ]
1846
+ },
1847
+ search_help_articles: {
1848
+ method: "POST",
1849
+ path: "/help/search",
1850
+ tag: "Help",
1851
+ summary: "Search help articles by keyword",
1852
+ body: true,
1853
+ response: "json",
1854
+ params: [
1855
+ { name: "query", in: "body", required: true, kind: "string" },
1856
+ { name: "limit", in: "body", required: false, kind: "integer" },
1857
+ { name: "section", in: "body", required: false, kind: "string" }
1858
+ ]
1859
+ },
1860
+ search_products: {
1861
+ method: "GET",
1862
+ path: "/products/search",
1863
+ tag: "Products",
1864
+ summary: "Full-text search products",
1865
+ body: false,
1866
+ response: "json",
1867
+ params: [
1868
+ { name: "organizationId", in: "query", required: false, kind: "string", description: "Convex organization ID \u2014 get it from GET /v1/me or the get_me tool (memberships[].orgId), not the organization name. Optional with a company key or an app's key: the key's company is used." },
1869
+ { name: "query", in: "query", required: true, kind: "string" },
1870
+ { name: "limit", in: "query", required: false, kind: "integer" }
1871
+ ]
1872
+ },
1873
+ search_project_files: {
1874
+ method: "GET",
1875
+ path: "/projects/{id}/files/search",
1876
+ tag: "Files",
1877
+ summary: "Search pages in a project by name or indexed content",
1878
+ body: false,
1879
+ response: "json",
1880
+ params: [
1881
+ { name: "id", in: "path", required: true, kind: "string", description: "Convex project ID" },
1882
+ { name: "organizationId", in: "query", required: false, kind: "string", description: "Convex organization ID \u2014 get it from GET /v1/me or the get_me tool (memberships[].orgId), not the organization name. Optional with a company key or an app's key: the key's company is used." },
1883
+ { name: "query", in: "query", required: false, kind: "string" }
1884
+ ]
1885
+ },
1886
+ search_project_line_items: {
1887
+ method: "GET",
1888
+ path: "/projects/{id}/line-items",
1889
+ tag: "Takeoff",
1890
+ summary: "Search line items across all symbols in a project",
1891
+ body: false,
1892
+ response: "json",
1893
+ params: [
1894
+ { name: "id", in: "path", required: true, kind: "string", description: "Convex project ID" },
1895
+ { name: "query", in: "query", required: false, kind: "string" },
1896
+ { name: "takeoffType", in: "query", required: false, kind: "string", enum: ["count", "linear", "area"] },
1897
+ { name: "limit", in: "query", required: false, kind: "integer" }
1898
+ ]
1899
+ },
1900
+ search_vendors: {
1901
+ method: "GET",
1902
+ path: "/vendors/search",
1903
+ tag: "Vendors",
1904
+ summary: "Full-text search vendors",
1905
+ body: false,
1906
+ response: "json",
1907
+ params: [
1908
+ { name: "organizationId", in: "query", required: false, kind: "string", description: "Convex organization ID \u2014 get it from GET /v1/me or the get_me tool (memberships[].orgId), not the organization name. Optional with a company key or an app's key: the key's company is used." },
1909
+ { name: "query", in: "query", required: true, kind: "string" },
1910
+ { name: "limit", in: "query", required: false, kind: "integer" }
1911
+ ]
1912
+ },
1913
+ send_cma_message: {
1914
+ method: "POST",
1915
+ path: "/cma/chat",
1916
+ tag: "Account",
1917
+ summary: "Send a message to the Managed Agent and return its reply",
1918
+ body: true,
1919
+ response: "json",
1920
+ params: [
1921
+ { name: "message", in: "body", required: true, kind: "string" },
1922
+ { name: "sessionId", in: "body", required: false, kind: "string" },
1923
+ { name: "organizationId", in: "body", required: false, kind: "string", description: "The organization this message is for and is billed to (memberships[].orgId in GET /v1/me). You must be a member of it; on the session of one of your Exayard conversations it must be that conversation's organization. Required unless sessionId is the session of one of your Exayard conversations (else 400). An organization without enough credits is answered 402." }
1924
+ ]
1925
+ },
1926
+ send_webhook_listener_test: {
1927
+ method: "POST",
1928
+ path: "/webhook_listeners/{id}/test",
1929
+ tag: "Webhooks",
1930
+ summary: "Send a test event to a webhook listener",
1931
+ body: true,
1932
+ response: "json",
1933
+ params: [
1934
+ { name: "id", in: "path", required: true, kind: "string" },
1935
+ { name: "organizationId", in: "body", required: false, kind: "string", description: "Convex organization ID \u2014 get it from GET /v1/me or the get_me tool (memberships[].orgId), not the organization name. Optional with a company key or an app's key: the key's company is used." },
1936
+ { name: "type", in: "body", required: true, kind: "string", enum: ["project.created", "project.updated", "project.archived", "assessment.started", "assessment.completed", "assessment.cancelled", "assessment.failed", "estimate.generated", "bid.generated", "bid.submitted", "bid.won", "bid.lost", "bid.accepted", "bid.declined", "bid.sent", "bid.viewed", "bid.commented", "file.processed", "addendum.uploaded", "quote.requested", "quote.received", "quote.accepted", "quote.rejected", "quote.expired", "conversation.started", "conversation.handoff_requested", "conversation.closed", "lead.captured", "contact.created", "appointment.booked", "appointment.rescheduled", "appointment.cancelled", "call.completed", "call.missed", "review.requested", "job.written_back", "payment.captured", "payment.failed"] }
1937
+ ]
1938
+ },
1939
+ send_webhook_test_event: {
1940
+ method: "POST",
1941
+ path: "/webhook_endpoints/{id}/test",
1942
+ tag: "Webhooks",
1943
+ summary: "Send a test event to a webhook endpoint",
1944
+ body: true,
1945
+ response: "json",
1946
+ params: [
1947
+ { name: "id", in: "path", required: true, kind: "string" },
1948
+ { name: "organizationId", in: "body", required: false, kind: "string", description: "Convex organization ID \u2014 get it from GET /v1/me or the get_me tool (memberships[].orgId), not the organization name. Optional with a company key or an app's key: the key's company is used." },
1949
+ { name: "type", in: "body", required: true, kind: "string", enum: ["project.created", "project.updated", "project.archived", "assessment.started", "assessment.completed", "assessment.cancelled", "assessment.failed", "estimate.generated", "bid.generated", "bid.submitted", "bid.won", "bid.lost", "bid.accepted", "bid.declined", "bid.sent", "bid.viewed", "bid.commented", "file.processed", "addendum.uploaded", "quote.requested", "quote.received", "quote.accepted", "quote.rejected", "quote.expired", "conversation.started", "conversation.handoff_requested", "conversation.closed", "lead.captured", "contact.created", "appointment.booked", "appointment.rescheduled", "appointment.cancelled", "call.completed", "call.missed", "review.requested", "job.written_back", "payment.captured", "payment.failed"] }
1950
+ ]
1951
+ },
1952
+ set_document_sharing: {
1953
+ method: "PATCH",
1954
+ path: "/documents/{id}/share",
1955
+ tag: "Documents",
1956
+ summary: "Turn document sharing on or off",
1957
+ body: true,
1958
+ response: "json",
1959
+ params: [
1960
+ { name: "id", in: "path", required: true, kind: "string", description: "Convex document ID" },
1961
+ { name: "organizationId", in: "body", required: false, kind: "string", description: "Convex organization ID \u2014 get it from GET /v1/me or the get_me tool (memberships[].orgId), not the organization name. Optional with a company key or an app's key: the key's company is used." },
1962
+ { name: "enabled", in: "body", required: true, kind: "boolean", description: "true mints (or reuses) the public share link and turns sharing on; false revokes access while keeping the secret for later re-enabling." }
1963
+ ]
1964
+ },
1965
+ set_vendor_price: {
1966
+ method: "PUT",
1967
+ path: "/products/{productId}/prices/{vendorId}",
1968
+ tag: "Vendor prices",
1969
+ summary: "Create or update a vendor price",
1970
+ body: true,
1971
+ response: "json",
1972
+ params: [
1973
+ { name: "productId", in: "path", required: true, kind: "string", description: "Convex product ID" },
1974
+ { name: "vendorId", in: "path", required: true, kind: "string", description: "Convex vendor ID" },
1975
+ { name: "organizationId", in: "body", required: false, kind: "string", description: "Convex organization ID \u2014 get it from GET /v1/me or the get_me tool (memberships[].orgId), not the organization name. Optional with a company key or an app's key: the key's company is used." },
1976
+ { name: "pricePerUnit", in: "body", required: true, kind: "number" },
1977
+ { name: "minOrderQuantity", in: "body", required: false, kind: "number" },
1978
+ { name: "volumeDiscounts", in: "body", required: false, kind: "array" },
1979
+ { name: "leadTimeDays", in: "body", required: false, kind: "integer" },
1980
+ { name: "expirationDate", in: "body", required: false, kind: "number" },
1981
+ { name: "notes", in: "body", required: false, kind: "string" }
1982
+ ]
1983
+ },
1984
+ start_webhook_listener: {
1985
+ method: "POST",
1986
+ path: "/webhook_listeners",
1987
+ tag: "Webhooks",
1988
+ summary: "Start a webhook listener (exayard listen)",
1989
+ body: true,
1990
+ response: "json",
1991
+ params: [
1992
+ { name: "organizationId", in: "body", required: false, kind: "string", description: "Convex organization ID \u2014 get it from GET /v1/me or the get_me tool (memberships[].orgId), not the organization name. Optional with a company key or an app's key: the key's company is used." },
1993
+ { name: "events", in: "body", required: false, kind: "array" },
1994
+ { name: "name", in: "body", required: false, kind: "string" }
1995
+ ]
1996
+ },
1997
+ stop_webhook_listener: {
1998
+ method: "DELETE",
1999
+ path: "/webhook_listeners/{id}",
2000
+ tag: "Webhooks",
2001
+ summary: "Stop a webhook listener",
2002
+ body: false,
2003
+ response: "empty",
2004
+ params: [
2005
+ { name: "id", in: "path", required: true, kind: "string" },
2006
+ { name: "organizationId", in: "query", required: false, kind: "string", description: "Convex organization ID \u2014 get it from GET /v1/me or the get_me tool (memberships[].orgId), not the organization name. Optional with a company key or an app's key: the key's company is used." }
2007
+ ]
2008
+ },
2009
+ stream_webhook_listener_events: {
2010
+ method: "GET",
2011
+ path: "/webhook_listeners/{id}/events",
2012
+ tag: "Webhooks",
2013
+ summary: "Stream a webhook listener's events",
2014
+ body: false,
2015
+ response: "stream",
2016
+ params: [
2017
+ { name: "id", in: "path", required: true, kind: "string" },
2018
+ { name: "organizationId", in: "query", required: false, kind: "string", description: "Convex organization ID \u2014 get it from GET /v1/me or the get_me tool (memberships[].orgId), not the organization name. Optional with a company key or an app's key: the key's company is used." },
2019
+ { name: "after", in: "query", required: false, kind: "integer" }
2020
+ ]
2021
+ },
2022
+ update_contact: {
2023
+ method: "PATCH",
2024
+ path: "/contacts/{id}",
2025
+ tag: "Contacts",
2026
+ summary: "Update a contact\u2019s details or CRM attributes",
2027
+ body: true,
2028
+ response: "empty",
2029
+ params: [
2030
+ { name: "id", in: "path", required: true, kind: "string", description: "Convex contact ID" },
2031
+ { name: "organizationId", in: "body", required: false, kind: "string", description: "Convex organization ID \u2014 get it from GET /v1/me or the get_me tool (memberships[].orgId), not the organization name. Optional with a company key or an app's key: the key's company is used." },
2032
+ { name: "name", in: "body", required: false, kind: "string" },
2033
+ { name: "email", in: "body", required: false, kind: "string" },
2034
+ { name: "phone", in: "body", required: false, kind: "string" },
2035
+ { name: "attributes", in: "body", required: false, kind: "object" }
2036
+ ]
2037
+ },
2038
+ update_conversation_title: {
2039
+ method: "PATCH",
2040
+ path: "/conversations/{id}",
2041
+ tag: "Conversations",
2042
+ summary: "Update conversation title or linked project",
2043
+ body: true,
2044
+ response: "empty",
2045
+ params: [
2046
+ { name: "id", in: "path", required: true, kind: "string", description: "Convex conversation ID" },
2047
+ { name: "organizationId", in: "body", required: false, kind: "string", description: "Convex organization ID \u2014 get it from GET /v1/me or the get_me tool (memberships[].orgId), not the organization name. Optional with a company key or an app's key: the key's company is used." },
2048
+ { name: "title", in: "body", required: false, kind: "string" },
2049
+ { name: "projectId", in: "body", required: false, kind: "string", description: "Convex project ID" }
2050
+ ]
2051
+ },
2052
+ update_custom_property: {
2053
+ method: "PATCH",
2054
+ path: "/custom-properties/{id}",
2055
+ tag: "Custom properties",
2056
+ summary: "Update a custom property",
2057
+ body: true,
2058
+ response: "empty",
2059
+ params: [
2060
+ { name: "id", in: "path", required: true, kind: "string", description: "Convex custom property ID" },
2061
+ { name: "organizationId", in: "body", required: false, kind: "string", description: "Convex organization ID \u2014 get it from GET /v1/me or the get_me tool (memberships[].orgId), not the organization name. Optional with a company key or an app's key: the key's company is used." },
2062
+ { name: "name", in: "body", required: false, kind: "string" },
2063
+ { name: "type", in: "body", required: false, kind: "string", enum: ["text", "textarea", "number", "date", "checkbox", "multiCheckbox", "select", "url", "email", "phone", "currency", "percentage"] },
2064
+ { name: "recordTypes", in: "body", required: false, kind: "array" },
2065
+ { name: "options", in: "body", required: false, kind: "array" }
2066
+ ]
2067
+ },
2068
+ update_group: {
2069
+ method: "PATCH",
2070
+ path: "/groups/{id}",
2071
+ tag: "Takeoff",
2072
+ summary: "Update a takeoff group",
2073
+ body: true,
2074
+ response: "empty",
2075
+ params: [
2076
+ { name: "id", in: "path", required: true, kind: "string", description: "Convex takeoff group ID" },
2077
+ { name: "projectId", in: "body", required: true, kind: "string", description: "Convex project ID" },
2078
+ { name: "name", in: "body", required: false, kind: "string" },
2079
+ { name: "position", in: "body", required: false, kind: "number" },
2080
+ { name: "parentId", in: "body", required: false, kind: "string", description: "Convex takeoff group ID" }
2081
+ ]
2082
+ },
2083
+ update_labor_crew: {
2084
+ method: "PATCH",
2085
+ path: "/labor/crews/{id}",
2086
+ tag: "Labor",
2087
+ summary: "Update a labor crew",
2088
+ body: true,
2089
+ response: "empty",
2090
+ params: [
2091
+ { name: "id", in: "path", required: true, kind: "string" },
2092
+ { name: "organizationId", in: "body", required: false, kind: "string", description: "Convex organization ID \u2014 get it from GET /v1/me or the get_me tool (memberships[].orgId), not the organization name. Optional with a company key or an app's key: the key's company is used." },
2093
+ { name: "name", in: "body", required: false, kind: "string" },
2094
+ { name: "description", in: "body", required: false, kind: "string" },
2095
+ { name: "members", in: "body", required: false, kind: "array" },
2096
+ { name: "equipmentIds", in: "body", required: false, kind: "array" },
2097
+ { name: "dailyCapacity", in: "body", required: false, kind: "object" }
2098
+ ]
2099
+ },
2100
+ update_labor_equipment: {
2101
+ method: "PATCH",
2102
+ path: "/labor/equipment/{id}",
2103
+ tag: "Labor",
2104
+ summary: "Update a labor equipment entry",
2105
+ body: true,
2106
+ response: "empty",
2107
+ params: [
2108
+ { name: "id", in: "path", required: true, kind: "string" },
2109
+ { name: "organizationId", in: "body", required: false, kind: "string", description: "Convex organization ID \u2014 get it from GET /v1/me or the get_me tool (memberships[].orgId), not the organization name. Optional with a company key or an app's key: the key's company is used." },
2110
+ { name: "name", in: "body", required: false, kind: "string" },
2111
+ { name: "description", in: "body", required: false, kind: "string" },
2112
+ { name: "category", in: "body", required: false, kind: "string" },
2113
+ { name: "dailyRate", in: "body", required: false, kind: "number" },
2114
+ { name: "hourlyRate", in: "body", required: false, kind: "number" },
2115
+ { name: "currency", in: "body", required: false, kind: "string", enum: ["USD", "CAD", "MXN", "BRL", "ARS", "CLP", "COP", "PEN", "UYU", "PYG", "BOB", "VES", "GYD", "SRD", "GTQ", "HNL", "NIO", "CRC", "PAB", "BZD", "JMD", "HTG", "DOP", "CUP", "BSD", "BBD", "TTD", "XCD", "AWG", "ANG", "KYD", "BMD", "FKP", "EUR", "GBP", "CHF", "NOK", "SEK", "DKK", "PLN", "CZK", "HUF", "RON", "BGN", "HRK", "RSD", "UAH", "RUB", "TRY", "ISK", "BAM", "MKD", "ALL", "MDL", "BYN", "GEL", "AMD", "AZN", "GIP", "ILS", "AED", "SAR", "QAR", "KWD", "BHD", "OMR", "JOD", "LBP", "EGP", "IQD", "IRR", "SYP", "YER", "ZAR", "NAD", "LSL", "SZL", "BWP", "NGN", "KES", "GHS", "MAD", "TND", "DZD", "LYD", "ETB", "ERN", "DJF", "SOS", "SDG", "SSP", "UGX", "TZS", "RWF", "BIF", "MWK", "ZMW", "ZWG", "MZN", "AOA", "CDF", "MGA", "MUR", "SCR", "KMF", "CVE", "GMD", "GNF", "LRD", "SLE", "MRU", "STN", "XOF", "XAF", "SHP", "JPY", "CNY", "HKD", "MOP", "TWD", "KRW", "KPW", "MNT", "SGD", "INR", "BTN", "PKR", "BDT", "LKR", "NPR", "MVR", "AFN", "IDR", "MYR", "PHP", "THB", "VND", "MMK", "KHR", "LAK", "BND", "KZT", "UZS", "KGS", "TJS", "TMT", "AUD", "NZD", "FJD", "PGK", "SBD", "VUV", "WST", "TOP", "XPF"], description: "ISO 4217 currency code" },
2116
+ { name: "isOwned", in: "body", required: false, kind: "boolean" }
2117
+ ]
2118
+ },
2119
+ update_labor_rate: {
2120
+ method: "PATCH",
2121
+ path: "/labor/rates/{id}",
2122
+ tag: "Labor",
2123
+ summary: "Update a labor rate",
2124
+ body: true,
2125
+ response: "empty",
2126
+ params: [
2127
+ { name: "id", in: "path", required: true, kind: "string" },
2128
+ { name: "organizationId", in: "body", required: false, kind: "string", description: "Convex organization ID \u2014 get it from GET /v1/me or the get_me tool (memberships[].orgId), not the organization name. Optional with a company key or an app's key: the key's company is used." },
2129
+ { name: "name", in: "body", required: false, kind: "string" },
2130
+ { name: "description", in: "body", required: false, kind: "string" },
2131
+ { name: "hourlyRate", in: "body", required: false, kind: "number" },
2132
+ { name: "burdenRate", in: "body", required: false, kind: "number" },
2133
+ { name: "currency", in: "body", required: false, kind: "string", enum: ["USD", "CAD", "MXN", "BRL", "ARS", "CLP", "COP", "PEN", "UYU", "PYG", "BOB", "VES", "GYD", "SRD", "GTQ", "HNL", "NIO", "CRC", "PAB", "BZD", "JMD", "HTG", "DOP", "CUP", "BSD", "BBD", "TTD", "XCD", "AWG", "ANG", "KYD", "BMD", "FKP", "EUR", "GBP", "CHF", "NOK", "SEK", "DKK", "PLN", "CZK", "HUF", "RON", "BGN", "HRK", "RSD", "UAH", "RUB", "TRY", "ISK", "BAM", "MKD", "ALL", "MDL", "BYN", "GEL", "AMD", "AZN", "GIP", "ILS", "AED", "SAR", "QAR", "KWD", "BHD", "OMR", "JOD", "LBP", "EGP", "IQD", "IRR", "SYP", "YER", "ZAR", "NAD", "LSL", "SZL", "BWP", "NGN", "KES", "GHS", "MAD", "TND", "DZD", "LYD", "ETB", "ERN", "DJF", "SOS", "SDG", "SSP", "UGX", "TZS", "RWF", "BIF", "MWK", "ZMW", "ZWG", "MZN", "AOA", "CDF", "MGA", "MUR", "SCR", "KMF", "CVE", "GMD", "GNF", "LRD", "SLE", "MRU", "STN", "XOF", "XAF", "SHP", "JPY", "CNY", "HKD", "MOP", "TWD", "KRW", "KPW", "MNT", "SGD", "INR", "BTN", "PKR", "BDT", "LKR", "NPR", "MVR", "AFN", "IDR", "MYR", "PHP", "THB", "VND", "MMK", "KHR", "LAK", "BND", "KZT", "UZS", "KGS", "TJS", "TMT", "AUD", "NZD", "FJD", "PGK", "SBD", "VUV", "WST", "TOP", "XPF"], description: "ISO 4217 currency code" },
2134
+ { name: "effectiveDate", in: "body", required: false, kind: "number" },
2135
+ { name: "isDefault", in: "body", required: false, kind: "boolean" }
2136
+ ]
2137
+ },
2138
+ update_layer: {
2139
+ method: "PATCH",
2140
+ path: "/layers/{id}",
2141
+ tag: "Takeoff",
2142
+ summary: "Rename a takeoff layer",
2143
+ body: true,
2144
+ response: "empty",
2145
+ params: [
2146
+ { name: "id", in: "path", required: true, kind: "string", description: "Convex takeoff layer ID" },
2147
+ { name: "projectId", in: "body", required: true, kind: "string", description: "Convex project ID" },
2148
+ { name: "name", in: "body", required: true, kind: "string" }
2149
+ ]
2150
+ },
2151
+ update_list: {
2152
+ method: "PATCH",
2153
+ path: "/lists/{id}",
2154
+ tag: "Lists",
2155
+ summary: "Update list name or metadata",
2156
+ body: true,
2157
+ response: "json",
2158
+ params: [
2159
+ { name: "id", in: "path", required: true, kind: "string", description: "Convex product list ID" },
2160
+ { name: "organizationId", in: "body", required: false, kind: "string", description: "Convex organization ID \u2014 get it from GET /v1/me or the get_me tool (memberships[].orgId), not the organization name. Optional with a company key or an app's key: the key's company is used." },
2161
+ { name: "name", in: "body", required: false, kind: "string" },
2162
+ { name: "description", in: "body", required: false, kind: "string" },
2163
+ { name: "privacy", in: "body", required: false, kind: "string", enum: ["private", "organization", "public", "system"] },
2164
+ { name: "category", in: "body", required: false, kind: "string" }
2165
+ ]
2166
+ },
2167
+ update_measurement: {
2168
+ method: "PATCH",
2169
+ path: "/measurements/{id}",
2170
+ tag: "Takeoff",
2171
+ summary: "Update a measurement",
2172
+ body: true,
2173
+ response: "empty",
2174
+ params: [
2175
+ { name: "id", in: "path", required: true, kind: "string", description: "Convex takeoff measurement ID" },
2176
+ { name: "projectId", in: "body", required: true, kind: "string", description: "Convex project ID" },
2177
+ { name: "value", in: "body", required: false, kind: "number", description: "The measured quantity in RAW PAGE PIXELS \u2014 the same coordinate space as the geometry points, NEVER feet, metres, or any other real-world unit. Linear measurements are a length in px, areas are px\xB2, counts are a tally. Exayard converts to real units at display time using the page scale, so a real-world number sent here is converted a second time and reads wrong. When geometry is supplied the server measures it and rejects a value that disagrees by 10\xD7 or more, naming both numbers. To send a real-world quantity, convert it to page pixels first (multiply by the page's pixels-per-unit; square that factor for an area)." },
2178
+ { name: "manualValue", in: "body", required: false, kind: "number", description: "A user's override of the measured quantity, in RAW PAGE PIXELS \u2014 the same coordinate space as the geometry points, NEVER feet, metres, or any other real-world unit. Linear measurements are a length in px, areas are px\xB2, counts are a tally. Exayard converts to real units at display time using the page scale, so a real-world number sent here is converted a second time and reads wrong. When geometry is supplied the server measures it and rejects a value that disagrees by 10\xD7 or more, naming both numbers. To send a real-world quantity, convert it to page pixels first (multiply by the page's pixels-per-unit; square that factor for an area). Send null to clear the override and fall back to the measured value." },
2179
+ { name: "geometry", in: "body", required: false, kind: "object", description: 'Measurement geometry: { "points": [{ "x": number, "y": number, "bulge"?: number }, ...], "isClosed": boolean, "holes"?: Point[][], "pathD"?: string }. When geometry is provided, "points" and "isClosed" are required; other keys (e.g. { "length": 280.9 }) are rejected.' },
2180
+ { name: "notes", in: "body", required: false, kind: "string" },
2181
+ { name: "name", in: "body", required: false, kind: "string" },
2182
+ { name: "layerId", in: "body", required: false, kind: "string", description: "Convex takeoff layer ID" }
2183
+ ]
2184
+ },
2185
+ update_message: {
2186
+ method: "PATCH",
2187
+ path: "/messages/{id}",
2188
+ tag: "Messages",
2189
+ summary: "Update a message",
2190
+ body: true,
2191
+ response: "empty",
2192
+ params: [
2193
+ { name: "id", in: "path", required: true, kind: "string", description: "Convex chat message ID" },
2194
+ { name: "content", in: "body", required: true, kind: "string" }
2195
+ ]
2196
+ },
2197
+ update_page: {
2198
+ method: "PATCH",
2199
+ path: "/pages/{id}",
2200
+ tag: "Pages",
2201
+ summary: "Update page name or rotation",
2202
+ body: true,
2203
+ response: "json",
2204
+ params: [
2205
+ { name: "id", in: "path", required: true, kind: "string", description: "Convex page ID" },
2206
+ { name: "name", in: "body", required: false, kind: "string" },
2207
+ { name: "rotation", in: "body", required: false, kind: "number" }
2208
+ ]
2209
+ },
2210
+ update_product: {
2211
+ method: "PATCH",
2212
+ path: "/products/{id}",
2213
+ tag: "Products",
2214
+ summary: "Update a product",
2215
+ body: true,
2216
+ response: "json",
2217
+ params: [
2218
+ { name: "id", in: "path", required: true, kind: "string", description: "Convex product ID" },
2219
+ { name: "organizationId", in: "body", required: false, kind: "string", description: "Convex organization ID \u2014 get it from GET /v1/me or the get_me tool (memberships[].orgId), not the organization name. Optional with a company key or an app's key: the key's company is used." },
2220
+ { name: "name", in: "body", required: false, kind: "string" },
2221
+ { name: "sku", in: "body", required: false, kind: "string" },
2222
+ { name: "unitOfMeasure", in: "body", required: false, kind: "string" },
2223
+ { name: "costPerUnit", in: "body", required: false, kind: "number" },
2224
+ { name: "currency", in: "body", required: false, kind: "string", enum: ["USD", "CAD", "MXN", "BRL", "ARS", "CLP", "COP", "PEN", "UYU", "PYG", "BOB", "VES", "GYD", "SRD", "GTQ", "HNL", "NIO", "CRC", "PAB", "BZD", "JMD", "HTG", "DOP", "CUP", "BSD", "BBD", "TTD", "XCD", "AWG", "ANG", "KYD", "BMD", "FKP", "EUR", "GBP", "CHF", "NOK", "SEK", "DKK", "PLN", "CZK", "HUF", "RON", "BGN", "HRK", "RSD", "UAH", "RUB", "TRY", "ISK", "BAM", "MKD", "ALL", "MDL", "BYN", "GEL", "AMD", "AZN", "GIP", "ILS", "AED", "SAR", "QAR", "KWD", "BHD", "OMR", "JOD", "LBP", "EGP", "IQD", "IRR", "SYP", "YER", "ZAR", "NAD", "LSL", "SZL", "BWP", "NGN", "KES", "GHS", "MAD", "TND", "DZD", "LYD", "ETB", "ERN", "DJF", "SOS", "SDG", "SSP", "UGX", "TZS", "RWF", "BIF", "MWK", "ZMW", "ZWG", "MZN", "AOA", "CDF", "MGA", "MUR", "SCR", "KMF", "CVE", "GMD", "GNF", "LRD", "SLE", "MRU", "STN", "XOF", "XAF", "SHP", "JPY", "CNY", "HKD", "MOP", "TWD", "KRW", "KPW", "MNT", "SGD", "INR", "BTN", "PKR", "BDT", "LKR", "NPR", "MVR", "AFN", "IDR", "MYR", "PHP", "THB", "VND", "MMK", "KHR", "LAK", "BND", "KZT", "UZS", "KGS", "TJS", "TMT", "AUD", "NZD", "FJD", "PGK", "SBD", "VUV", "WST", "TOP", "XPF"], description: "ISO 4217 currency code" },
2225
+ { name: "markupPercent", in: "body", required: false, kind: "number" },
2226
+ { name: "customProperties", in: "body", required: false, kind: "object" }
2227
+ ]
2228
+ },
2229
+ update_project: {
2230
+ method: "PATCH",
2231
+ path: "/projects/{id}",
2232
+ tag: "Projects",
2233
+ summary: "Update a project",
2234
+ body: true,
2235
+ response: "json",
2236
+ params: [
2237
+ { name: "id", in: "path", required: true, kind: "string", description: "Convex project ID" },
2238
+ { name: "organizationId", in: "body", required: false, kind: "string", description: "Convex organization ID \u2014 get it from GET /v1/me or the get_me tool (memberships[].orgId), not the organization name. Optional with a company key or an app's key: the key's company is used." },
2239
+ { name: "name", in: "body", required: false, kind: "string" },
2240
+ { name: "status", in: "body", required: false, kind: "string", enum: ["lead", "contacted", "qualified", "active", "archived"] },
2241
+ { name: "settings", in: "body", required: false, kind: "object" },
2242
+ { name: "tags", in: "body", required: false, kind: "array" },
2243
+ { name: "customProperties", in: "body", required: false, kind: "object" }
2244
+ ]
2245
+ },
2246
+ update_project_privacy: {
2247
+ method: "POST",
2248
+ path: "/projects/{id}/privacy",
2249
+ tag: "Projects",
2250
+ summary: "Change a project\u2019s privacy setting",
2251
+ body: true,
2252
+ response: "empty",
2253
+ params: [
2254
+ { name: "id", in: "path", required: true, kind: "string", description: "Convex project ID" },
2255
+ { name: "organizationId", in: "body", required: false, kind: "string", description: "Convex organization ID \u2014 get it from GET /v1/me or the get_me tool (memberships[].orgId), not the organization name. Optional with a company key or an app's key: the key's company is used." },
2256
+ { name: "privacy", in: "body", required: true, kind: "string", enum: ["private", "unlisted", "public"] }
2257
+ ]
2258
+ },
2259
+ update_symbol: {
2260
+ method: "PATCH",
2261
+ path: "/symbols/{id}",
2262
+ tag: "Takeoff",
2263
+ summary: "Update a takeoff symbol",
2264
+ body: true,
2265
+ response: "empty",
2266
+ params: [
2267
+ { name: "id", in: "path", required: true, kind: "string", description: "Convex takeoff symbol ID" },
2268
+ { name: "projectId", in: "body", required: true, kind: "string", description: "Convex project ID" },
2269
+ { name: "name", in: "body", required: false, kind: "string" },
2270
+ { name: "color", in: "body", required: false, kind: "string" },
2271
+ { name: "type", in: "body", required: false, kind: "string", enum: ["count", "linear", "area"] },
2272
+ { name: "category", in: "body", required: false, kind: "string" },
2273
+ { name: "subcategory", in: "body", required: false, kind: "string" },
2274
+ { name: "customProperties", in: "body", required: false, kind: "object" }
2275
+ ]
2276
+ },
2277
+ update_vendor: {
2278
+ method: "PATCH",
2279
+ path: "/vendors/{id}",
2280
+ tag: "Vendors",
2281
+ summary: "Update a vendor",
2282
+ body: true,
2283
+ response: "json",
2284
+ params: [
2285
+ { name: "id", in: "path", required: true, kind: "string", description: "Convex vendor ID" },
2286
+ { name: "organizationId", in: "body", required: false, kind: "string", description: "Convex organization ID \u2014 get it from GET /v1/me or the get_me tool (memberships[].orgId), not the organization name. Optional with a company key or an app's key: the key's company is used." },
2287
+ { name: "name", in: "body", required: false, kind: "string" },
2288
+ { name: "type", in: "body", required: false, kind: "string", enum: ["supplier", "subcontractor", "rental"] },
2289
+ { name: "kind", in: "body", required: false, kind: "string", enum: ["sub", "supplier", "surety", "insurer", "agency", "owner", "architect", "engineer", "other", null] },
2290
+ { name: "contactName", in: "body", required: false, kind: "string" },
2291
+ { name: "email", in: "body", required: false, kind: "string" },
2292
+ { name: "phone", in: "body", required: false, kind: "string" },
2293
+ { name: "website", in: "body", required: false, kind: "string" },
2294
+ { name: "address", in: "body", required: false, kind: "object" },
2295
+ { name: "paymentTerms", in: "body", required: false, kind: "string" },
2296
+ { name: "accountNumber", in: "body", required: false, kind: "string" },
2297
+ { name: "rating", in: "body", required: false, kind: "number" },
2298
+ { name: "notes", in: "body", required: false, kind: "string" },
2299
+ { name: "tags", in: "body", required: false, kind: "array" },
2300
+ { name: "isPreferred", in: "body", required: false, kind: "boolean" }
2301
+ ]
2302
+ },
2303
+ update_vendor_quote_status: {
2304
+ method: "PATCH",
2305
+ path: "/quotes/{id}/status",
2306
+ tag: "Vendor quotes",
2307
+ summary: "Accept, reject, or expire a vendor quote",
2308
+ body: true,
2309
+ response: "json",
2310
+ params: [
2311
+ { name: "id", in: "path", required: true, kind: "string" },
2312
+ { name: "organizationId", in: "body", required: false, kind: "string", description: "Convex organization ID \u2014 get it from GET /v1/me or the get_me tool (memberships[].orgId), not the organization name. Optional with a company key or an app's key: the key's company is used." },
2313
+ { name: "status", in: "body", required: true, kind: "string", enum: ["accepted", "rejected", "expired"] }
2314
+ ]
2315
+ },
2316
+ update_webhook_endpoint: {
2317
+ method: "PATCH",
2318
+ path: "/webhook_endpoints/{id}",
2319
+ tag: "Webhooks",
2320
+ summary: "Update a webhook endpoint",
2321
+ body: true,
2322
+ response: "empty",
2323
+ params: [
2324
+ { name: "id", in: "path", required: true, kind: "string" },
2325
+ { name: "organizationId", in: "body", required: false, kind: "string", description: "Convex organization ID \u2014 get it from GET /v1/me or the get_me tool (memberships[].orgId), not the organization name. Optional with a company key or an app's key: the key's company is used." },
2326
+ { name: "url", in: "body", required: false, kind: "string" },
2327
+ { name: "events", in: "body", required: false, kind: "array" },
2328
+ { name: "description", in: "body", required: false, kind: "string" },
2329
+ { name: "status", in: "body", required: false, kind: "string", enum: ["active", "paused"] }
2330
+ ]
2331
+ },
2332
+ update_widget: {
2333
+ method: "PATCH",
2334
+ path: "/widgets/{id}",
2335
+ tag: "Widgets",
2336
+ summary: "Update a widget",
2337
+ body: true,
2338
+ response: "empty",
2339
+ params: [
2340
+ { name: "id", in: "path", required: true, kind: "string", description: "Convex widget ID" },
2341
+ { name: "organizationId", in: "body", required: false, kind: "string", description: "Convex organization ID \u2014 get it from GET /v1/me or the get_me tool (memberships[].orgId), not the organization name. Optional with a company key or an app's key: the key's company is used." },
2342
+ { name: "name", in: "body", required: false, kind: "string" },
2343
+ { name: "isActive", in: "body", required: false, kind: "boolean" },
2344
+ { name: "config", in: "body", required: false, kind: "object" }
2345
+ ]
2346
+ }
2347
+ };
2348
+ exports2.OPERATION_METHODS = {
2349
+ addSymbolLineItem: "add_symbol_line_item",
2350
+ archiveConversation: "archive_conversation",
2351
+ archiveLaborCrew: "archive_labor_crew",
2352
+ archiveLaborEquipment: "archive_labor_equipment",
2353
+ archiveLaborRate: "archive_labor_rate",
2354
+ archiveList: "archive_list",
2355
+ archivePage: "archive_page",
2356
+ archiveProduct: "archive_product",
2357
+ archiveProject: "archive_project",
2358
+ archiveVendor: "archive_vendor",
2359
+ attachFileToProject: "attach_file_to_project",
2360
+ batchCreateProducts: "batch_create_products",
2361
+ cancelAssessment: "cancel_assessment",
2362
+ confirmFileUpload: "confirm_file_upload",
2363
+ createAgentSession: "create_agent_session",
2364
+ createAssessment: "create_assessment",
2365
+ createBid: "create_bid",
2366
+ createConversation: "create_conversation",
2367
+ createCustomProperty: "create_custom_property",
2368
+ createEstimate: "create_estimate",
2369
+ createFolder: "create_folder",
2370
+ createGroup: "create_group",
2371
+ createLaborCrew: "create_labor_crew",
2372
+ createLaborEquipment: "create_labor_equipment",
2373
+ createLaborRate: "create_labor_rate",
2374
+ createLayer: "create_layer",
2375
+ createList: "create_list",
2376
+ createMeasurement: "create_measurement",
2377
+ createMessage: "create_message",
2378
+ createOrganization: "create_organization",
2379
+ createProduct: "create_product",
2380
+ createProject: "create_project",
2381
+ createSymbol: "create_symbol",
2382
+ createVendor: "create_vendor",
2383
+ createVendorQuote: "create_vendor_quote",
2384
+ createWebhookEndpoint: "create_webhook_endpoint",
2385
+ createWidget: "create_widget",
2386
+ deleteOrganization: "delete_organization",
2387
+ deleteWebhookEndpoint: "delete_webhook_endpoint",
2388
+ disconnectIntegration: "disconnect_integration",
2389
+ exportProject: "export_project",
2390
+ findSymbolElements: "find_symbol_elements",
2391
+ generateEstimate: "generate_estimate",
2392
+ getAssessment: "get_assessment",
2393
+ getBestPriceForProduct: "get_best_price_for_product",
2394
+ getContact: "get_contact",
2395
+ getConversation: "get_conversation",
2396
+ getCreditBalance: "get_credit_balance",
2397
+ getCreditUsageHistory: "get_credit_usage_history",
2398
+ getCreditUsageStats: "get_credit_usage_stats",
2399
+ getCustomProperty: "get_custom_property",
2400
+ getDocument: "get_document",
2401
+ getFile: "get_file",
2402
+ getFileUrl: "get_file_url",
2403
+ getFolder: "get_folder",
2404
+ getLaborCrew: "get_labor_crew",
2405
+ getLaborEquipment: "get_labor_equipment",
2406
+ getLaborRate: "get_labor_rate",
2407
+ getLatestAssessment: "get_latest_assessment",
2408
+ getList: "get_list",
2409
+ getMe: "get_me",
2410
+ getMeasurement: "get_measurement",
2411
+ getPage: "get_page",
2412
+ getPageContent: "get_page_content",
2413
+ getPricesForProduct: "get_prices_for_product",
2414
+ getProduct: "get_product",
2415
+ getProject: "get_project",
2416
+ getProjectRelations: "get_project_relations",
2417
+ getSpreadsheet: "get_spreadsheet",
2418
+ getSymbol: "get_symbol",
2419
+ getSymbolLineItem: "get_symbol_line_item",
2420
+ getTakeoffSummary: "get_takeoff_summary",
2421
+ getUnreadNotificationCount: "get_unread_notification_count",
2422
+ getUsage: "get_usage",
2423
+ getVendor: "get_vendor",
2424
+ getVendorQuote: "get_vendor_quote",
2425
+ getWidget: "get_widget",
2426
+ listAssessments: "list_assessments",
2427
+ listContacts: "list_contacts",
2428
+ listConversationFiles: "list_conversation_files",
2429
+ listConversations: "list_conversations",
2430
+ listCustomProperties: "list_custom_properties",
2431
+ listDocuments: "list_documents",
2432
+ listFiles: "list_files",
2433
+ listFolderContents: "list_folder_contents",
2434
+ listFolders: "list_folders",
2435
+ listGroups: "list_groups",
2436
+ listInstallations: "list_installations",
2437
+ listIntegrations: "list_integrations",
2438
+ listLaborCrews: "list_labor_crews",
2439
+ listLaborEquipment: "list_labor_equipment",
2440
+ listLaborRates: "list_labor_rates",
2441
+ listLayers: "list_layers",
2442
+ listLists: "list_lists",
2443
+ listMeasurementsByPage: "list_measurements_by_page",
2444
+ listMeasurementsBySymbol: "list_measurements_by_symbol",
2445
+ listMessages: "list_messages",
2446
+ listNotifications: "list_notifications",
2447
+ listOrgDocuments: "list_org_documents",
2448
+ listOrganizations: "list_organizations",
2449
+ listPages: "list_pages",
2450
+ listProducts: "list_products",
2451
+ listProductsInList: "list_products_in_list",
2452
+ listProjectConversations: "list_project_conversations",
2453
+ listProjects: "list_projects",
2454
+ listSpreadsheets: "list_spreadsheets",
2455
+ listSymbolLineItems: "list_symbol_line_items",
2456
+ listSymbols: "list_symbols",
2457
+ listVendorQuotes: "list_vendor_quotes",
2458
+ listVendors: "list_vendors",
2459
+ listWebhookDeliveries: "list_webhook_deliveries",
2460
+ listWebhookEndpoints: "list_webhook_endpoints",
2461
+ listWidgetConversations: "list_widget_conversations",
2462
+ listWidgets: "list_widgets",
2463
+ markAllNotificationsRead: "mark_all_notifications_read",
2464
+ markNotificationRead: "mark_notification_read",
2465
+ patchSymbolLineItem: "patch_symbol_line_item",
2466
+ presignFileUpload: "presign_file_upload",
2467
+ proposeAnalysis: "propose_analysis",
2468
+ provisionCma: "provision_cma",
2469
+ receiveVendorQuote: "receive_vendor_quote",
2470
+ removeCustomProperty: "remove_custom_property",
2471
+ removeFile: "remove_file",
2472
+ removeFolder: "remove_folder",
2473
+ removeGroup: "remove_group",
2474
+ removeLayer: "remove_layer",
2475
+ removeMeasurement: "remove_measurement",
2476
+ removeMessage: "remove_message",
2477
+ removeSymbol: "remove_symbol",
2478
+ removeSymbolLineItem: "remove_symbol_line_item",
2479
+ removeVendorPrice: "remove_vendor_price",
2480
+ removeWidget: "remove_widget",
2481
+ renameFile: "rename_file",
2482
+ renameFolder: "rename_folder",
2483
+ resendWebhookDelivery: "resend_webhook_delivery",
2484
+ restoreList: "restore_list",
2485
+ restorePage: "restore_page",
2486
+ restoreProject: "restore_project",
2487
+ runApprovedAnalysis: "run_approved_analysis",
2488
+ searchHelpArticles: "search_help_articles",
2489
+ searchProducts: "search_products",
2490
+ searchProjectFiles: "search_project_files",
2491
+ searchProjectLineItems: "search_project_line_items",
2492
+ searchVendors: "search_vendors",
2493
+ sendCmaMessage: "send_cma_message",
2494
+ sendWebhookListenerTest: "send_webhook_listener_test",
2495
+ sendWebhookTestEvent: "send_webhook_test_event",
2496
+ setDocumentSharing: "set_document_sharing",
2497
+ setVendorPrice: "set_vendor_price",
2498
+ startWebhookListener: "start_webhook_listener",
2499
+ stopWebhookListener: "stop_webhook_listener",
2500
+ streamWebhookListenerEvents: "stream_webhook_listener_events",
2501
+ updateContact: "update_contact",
2502
+ updateConversationTitle: "update_conversation_title",
2503
+ updateCustomProperty: "update_custom_property",
2504
+ updateGroup: "update_group",
2505
+ updateLaborCrew: "update_labor_crew",
2506
+ updateLaborEquipment: "update_labor_equipment",
2507
+ updateLaborRate: "update_labor_rate",
2508
+ updateLayer: "update_layer",
2509
+ updateList: "update_list",
2510
+ updateMeasurement: "update_measurement",
2511
+ updateMessage: "update_message",
2512
+ updatePage: "update_page",
2513
+ updateProduct: "update_product",
2514
+ updateProject: "update_project",
2515
+ updateProjectPrivacy: "update_project_privacy",
2516
+ updateSymbol: "update_symbol",
2517
+ updateVendor: "update_vendor",
2518
+ updateVendorQuoteStatus: "update_vendor_quote_status",
2519
+ updateWebhookEndpoint: "update_webhook_endpoint",
2520
+ updateWidget: "update_widget"
2521
+ };
2522
+ }
2523
+ });
2524
+
33
2525
  // ../sdk/dist/error.js
34
2526
  var require_error = __commonJS({
35
2527
  "../sdk/dist/error.js"(exports2) {
36
2528
  "use strict";
37
2529
  Object.defineProperty(exports2, "__esModule", { value: true });
38
- exports2.ExayardError = void 0;
2530
+ exports2.errorFromProblem = exports2.APIConnectionError = exports2.InternalServerError = exports2.RateLimitError = exports2.UnprocessableEntityError = exports2.ConflictError = exports2.NotFoundError = exports2.PermissionDeniedError = exports2.PaymentRequiredError = exports2.AuthenticationError = exports2.BadRequestError = exports2.ExayardError = void 0;
2531
+ var KNOWN_FIELDS = /* @__PURE__ */ new Set(["type", "title", "status", "detail", "instance", "code", "param", "doc_url", "request_id"]);
39
2532
  var ExayardError = class extends Error {
40
2533
  type;
41
2534
  title;
@@ -46,6 +2539,8 @@ var require_error = __commonJS({
46
2539
  param;
47
2540
  docUrl;
48
2541
  requestId;
2542
+ /** Problem extension members, such as `limit` on usage_limit_reached. */
2543
+ extensions;
49
2544
  constructor(body) {
50
2545
  const title = body.title ?? "error";
51
2546
  const status = typeof body.status === "number" ? body.status : 0;
@@ -53,7 +2548,7 @@ var require_error = __commonJS({
53
2548
  const detail = body.detail ?? "";
54
2549
  const message = detail ? `${title} (${code}): ${detail}` : `HTTP ${status || "unknown"}: ${title}`;
55
2550
  super(message);
56
- this.name = "ExayardError";
2551
+ this.name = new.target.name;
57
2552
  this.type = body.type ?? "about:blank";
58
2553
  this.title = title;
59
2554
  this.status = status;
@@ -63,13 +2558,14 @@ var require_error = __commonJS({
63
2558
  this.param = body.param;
64
2559
  this.docUrl = body.doc_url;
65
2560
  this.requestId = body.request_id;
2561
+ this.extensions = Object.fromEntries(Object.entries(body).filter(([key]) => !KNOWN_FIELDS.has(key)));
66
2562
  }
67
2563
  // Convenience guards for the most common branching points.
68
2564
  isRateLimited() {
69
2565
  return this.code === "rate_limited" || this.status === 429;
70
2566
  }
71
2567
  isUnauthenticated() {
72
- return this.code === "unauthenticated" || this.status === 401;
2568
+ return this.code === "unauthenticated" || this.code === "api_key_expired" || this.status === 401;
73
2569
  }
74
2570
  isNotFound() {
75
2571
  return this.code === "not_found" || this.status === 404;
@@ -82,6 +2578,341 @@ var require_error = __commonJS({
82
2578
  }
83
2579
  };
84
2580
  exports2.ExayardError = ExayardError;
2581
+ var BadRequestError = class extends ExayardError {
2582
+ };
2583
+ exports2.BadRequestError = BadRequestError;
2584
+ var AuthenticationError = class extends ExayardError {
2585
+ };
2586
+ exports2.AuthenticationError = AuthenticationError;
2587
+ var PaymentRequiredError = class extends ExayardError {
2588
+ };
2589
+ exports2.PaymentRequiredError = PaymentRequiredError;
2590
+ var PermissionDeniedError = class extends ExayardError {
2591
+ };
2592
+ exports2.PermissionDeniedError = PermissionDeniedError;
2593
+ var NotFoundError = class extends ExayardError {
2594
+ };
2595
+ exports2.NotFoundError = NotFoundError;
2596
+ var ConflictError = class extends ExayardError {
2597
+ };
2598
+ exports2.ConflictError = ConflictError;
2599
+ var UnprocessableEntityError = class extends ExayardError {
2600
+ };
2601
+ exports2.UnprocessableEntityError = UnprocessableEntityError;
2602
+ var RateLimitError = class extends ExayardError {
2603
+ retryAfter;
2604
+ constructor(body, retryAfter) {
2605
+ super(body);
2606
+ this.retryAfter = retryAfter;
2607
+ }
2608
+ };
2609
+ exports2.RateLimitError = RateLimitError;
2610
+ var InternalServerError = class extends ExayardError {
2611
+ };
2612
+ exports2.InternalServerError = InternalServerError;
2613
+ var APIConnectionError = class extends ExayardError {
2614
+ };
2615
+ exports2.APIConnectionError = APIConnectionError;
2616
+ var BY_CODE = {
2617
+ invalid_request: BadRequestError,
2618
+ invalid_end_user: BadRequestError,
2619
+ company_required: BadRequestError,
2620
+ company_mismatch: BadRequestError,
2621
+ unauthenticated: AuthenticationError,
2622
+ api_key_expired: AuthenticationError,
2623
+ insufficient_credits: PaymentRequiredError,
2624
+ subscription_required: PaymentRequiredError,
2625
+ upgrade_required: PaymentRequiredError,
2626
+ paid_plan_required: PaymentRequiredError,
2627
+ project_limit_reached: PaymentRequiredError,
2628
+ usage_limit_reached: PaymentRequiredError,
2629
+ forbidden: PermissionDeniedError,
2630
+ insufficient_scope: PermissionDeniedError,
2631
+ app_not_installed: PermissionDeniedError,
2632
+ app_suspended: PermissionDeniedError,
2633
+ partner_company_limit_reached: PermissionDeniedError,
2634
+ not_found: NotFoundError,
2635
+ conflict: ConflictError,
2636
+ idempotency_key_reused: ConflictError,
2637
+ rate_limited: RateLimitError,
2638
+ internal_error: InternalServerError
2639
+ };
2640
+ var BY_STATUS = {
2641
+ 400: BadRequestError,
2642
+ 401: AuthenticationError,
2643
+ 402: PaymentRequiredError,
2644
+ 403: PermissionDeniedError,
2645
+ 404: NotFoundError,
2646
+ 409: ConflictError,
2647
+ 422: UnprocessableEntityError,
2648
+ 429: RateLimitError
2649
+ };
2650
+ var errorFromProblem = (body, retryAfter) => {
2651
+ const status = typeof body.status === "number" ? body.status : 0;
2652
+ const cls = BY_CODE[body.code] ?? BY_STATUS[status] ?? (status >= 500 ? InternalServerError : ExayardError);
2653
+ if (cls === RateLimitError)
2654
+ return new RateLimitError(body, retryAfter);
2655
+ return new cls(body);
2656
+ };
2657
+ exports2.errorFromProblem = errorFromProblem;
2658
+ }
2659
+ });
2660
+
2661
+ // ../sdk/dist/retry.js
2662
+ var require_retry = __commonJS({
2663
+ "../sdk/dist/retry.js"(exports2) {
2664
+ "use strict";
2665
+ Object.defineProperty(exports2, "__esModule", { value: true });
2666
+ exports2.sleep = exports2.retryDelayMs = exports2.parseRetryAfter = exports2.isRetryableStatus = exports2.MAX_RETRY_AFTER_SECONDS = exports2.DEFAULT_MAX_RETRIES = void 0;
2667
+ exports2.DEFAULT_MAX_RETRIES = 2;
2668
+ exports2.MAX_RETRY_AFTER_SECONDS = 60;
2669
+ var BASE_DELAY_MS = 500;
2670
+ var MAX_BACKOFF_MS = 8e3;
2671
+ var isRetryableStatus = (status) => status === 429 || status >= 500;
2672
+ exports2.isRetryableStatus = isRetryableStatus;
2673
+ var parseRetryAfter = (value, now = Date.now()) => {
2674
+ if (value === null || value === void 0)
2675
+ return void 0;
2676
+ const trimmed = value.trim();
2677
+ if (trimmed === "")
2678
+ return void 0;
2679
+ if (/^\d+(\.\d+)?$/.test(trimmed))
2680
+ return Number(trimmed);
2681
+ const at = Date.parse(trimmed);
2682
+ if (Number.isNaN(at))
2683
+ return void 0;
2684
+ return Math.max(0, (at - now) / 1e3);
2685
+ };
2686
+ exports2.parseRetryAfter = parseRetryAfter;
2687
+ var retryDelayMs = (attempt, retryAfterSeconds, random = Math.random) => {
2688
+ if (retryAfterSeconds !== void 0)
2689
+ return Math.ceil(retryAfterSeconds * 1e3);
2690
+ const backoff = Math.min(MAX_BACKOFF_MS, BASE_DELAY_MS * 2 ** (attempt - 1));
2691
+ return Math.round(backoff * (1 - 0.25 * random()));
2692
+ };
2693
+ exports2.retryDelayMs = retryDelayMs;
2694
+ var sleep = (ms, signal) => new Promise((resolve, reject) => {
2695
+ if (signal?.aborted) {
2696
+ reject(signal.reason ?? new Error("Aborted"));
2697
+ return;
2698
+ }
2699
+ const timer = setTimeout(() => {
2700
+ signal?.removeEventListener("abort", onAbort);
2701
+ resolve();
2702
+ }, ms);
2703
+ const onAbort = () => {
2704
+ clearTimeout(timer);
2705
+ reject(signal?.reason ?? new Error("Aborted"));
2706
+ };
2707
+ signal?.addEventListener("abort", onAbort, { once: true });
2708
+ });
2709
+ exports2.sleep = sleep;
2710
+ }
2711
+ });
2712
+
2713
+ // ../sdk/dist/listeners.js
2714
+ var require_listeners = __commonJS({
2715
+ "../sdk/dist/listeners.js"(exports2) {
2716
+ "use strict";
2717
+ Object.defineProperty(exports2, "__esModule", { value: true });
2718
+ exports2.WebhookListeners = void 0;
2719
+ exports2.parseServerSentEvents = parseServerSentEvents;
2720
+ var error_1 = require_error();
2721
+ var retry_1 = require_retry();
2722
+ async function* parseServerSentEvents(body) {
2723
+ const reader = body.getReader();
2724
+ const decoder = new TextDecoder();
2725
+ let buffer = "";
2726
+ let current = { data: [] };
2727
+ const flush = () => {
2728
+ const out = current;
2729
+ current = { data: [] };
2730
+ if (out.data.length === 0 && out.retry === void 0 && out.event === void 0)
2731
+ return null;
2732
+ return {
2733
+ ...out.id !== void 0 ? { id: out.id } : {},
2734
+ ...out.event !== void 0 ? { event: out.event } : {},
2735
+ data: out.data.join("\n"),
2736
+ ...out.retry !== void 0 ? { retry: out.retry } : {}
2737
+ };
2738
+ };
2739
+ const take = function* (line) {
2740
+ if (line === "") {
2741
+ const event = flush();
2742
+ if (event)
2743
+ yield event;
2744
+ return;
2745
+ }
2746
+ if (line.startsWith(":"))
2747
+ return;
2748
+ const colon = line.indexOf(":");
2749
+ const field = colon === -1 ? line : line.slice(0, colon);
2750
+ let value = colon === -1 ? "" : line.slice(colon + 1);
2751
+ if (value.startsWith(" "))
2752
+ value = value.slice(1);
2753
+ if (field === "data")
2754
+ current.data.push(value);
2755
+ else if (field === "event")
2756
+ current.event = value;
2757
+ else if (field === "id")
2758
+ current.id = value;
2759
+ else if (field === "retry" && /^\d+$/.test(value))
2760
+ current.retry = Number(value);
2761
+ };
2762
+ try {
2763
+ for (; ; ) {
2764
+ const { done, value } = await reader.read();
2765
+ if (done)
2766
+ break;
2767
+ buffer += decoder.decode(value, { stream: true });
2768
+ let newline = buffer.search(/\r\n|\r|\n/);
2769
+ while (newline !== -1) {
2770
+ const line = buffer.slice(0, newline);
2771
+ const width = buffer.startsWith("\r\n", newline) ? 2 : 1;
2772
+ if (width === 1 && buffer[newline] === "\r" && newline === buffer.length - 1)
2773
+ break;
2774
+ buffer = buffer.slice(newline + width);
2775
+ yield* take(line);
2776
+ newline = buffer.search(/\r\n|\r|\n/);
2777
+ }
2778
+ }
2779
+ buffer += decoder.decode();
2780
+ if (buffer !== "")
2781
+ yield* take(buffer);
2782
+ } finally {
2783
+ reader.releaseLock();
2784
+ }
2785
+ }
2786
+ var DEFAULT_RETRY_MS = 1e3;
2787
+ var DEFAULT_MAX_RECONNECTS = 10;
2788
+ var WebhookListeners = class {
2789
+ transport;
2790
+ constructor(transport) {
2791
+ this.transport = transport;
2792
+ }
2793
+ /** Start a listener. `events` defaults to every event (`["*"]`). Needs `write:webhooks`. */
2794
+ start(body = {}, opts = {}) {
2795
+ return this.transport.request({
2796
+ method: "POST",
2797
+ path: "/webhook_listeners",
2798
+ body: { ...body, organizationId: this.transport.organizationId(body.organizationId) },
2799
+ options: opts
2800
+ });
2801
+ }
2802
+ /** Send a test event of a concrete type to the listener; it arrives on the stream with `test: true`. */
2803
+ sendTest(id, body, opts = {}) {
2804
+ return this.transport.request({
2805
+ method: "POST",
2806
+ path: `/webhook_listeners/${encodeURIComponent(id)}/test`,
2807
+ body: { ...body, organizationId: this.transport.organizationId(body.organizationId) },
2808
+ options: opts
2809
+ });
2810
+ }
2811
+ /** Stop the listener and delete its events. */
2812
+ async stop(id, query = {}, opts = {}) {
2813
+ await this.transport.request({
2814
+ method: "DELETE",
2815
+ path: `/webhook_listeners/${encodeURIComponent(id)}`,
2816
+ query: { organizationId: this.transport.organizationId(query.organizationId) },
2817
+ options: opts
2818
+ });
2819
+ }
2820
+ /** Open the stream once and return the raw Response (text/event-stream). Most callers want `events()`. */
2821
+ open(id, opts = {}) {
2822
+ return this.transport.request({
2823
+ method: "GET",
2824
+ path: `/webhook_listeners/${encodeURIComponent(id)}/events`,
2825
+ query: { organizationId: this.transport.organizationId(opts.organizationId) },
2826
+ headers: {
2827
+ Accept: "text/event-stream",
2828
+ ...opts.after !== void 0 ? { "Last-Event-ID": String(opts.after) } : {}
2829
+ },
2830
+ raw: true,
2831
+ // The stream stays open for minutes: no per-attempt timeout once the answer has started.
2832
+ options: { signal: opts.signal, timeoutMs: 3e4 }
2833
+ });
2834
+ }
2835
+ /**
2836
+ * The listener's events, in order, for as long as the listener lives: reconnects on `event: reconnect` and on a
2837
+ * dropped stream, resuming after the last event it yielded. Ends when `signal` aborts. Throws NotFoundError when
2838
+ * the listener was stopped or swept, and the last error after `maxReconnects` failed reconnects in a row.
2839
+ */
2840
+ async *events(id, opts = {}) {
2841
+ let after = opts.after;
2842
+ let retryMs = DEFAULT_RETRY_MS;
2843
+ let failures = 0;
2844
+ let first = true;
2845
+ const maxReconnects = opts.maxReconnects ?? DEFAULT_MAX_RECONNECTS;
2846
+ while (!opts.signal?.aborted) {
2847
+ if (!first)
2848
+ opts.onReconnect?.(after);
2849
+ first = false;
2850
+ let res;
2851
+ try {
2852
+ res = await this.open(id, { after, organizationId: opts.organizationId, signal: opts.signal });
2853
+ } catch (err) {
2854
+ if (opts.signal?.aborted)
2855
+ return;
2856
+ if (err instanceof error_1.NotFoundError || err instanceof error_1.ExayardError && err.status >= 400 && err.status < 500 && err.status !== 429)
2857
+ throw err;
2858
+ failures += 1;
2859
+ if (failures > maxReconnects)
2860
+ throw err;
2861
+ await sleepUnlessAborted(Math.max(retryMs, (0, retry_1.retryDelayMs)(failures)), opts.signal);
2862
+ continue;
2863
+ }
2864
+ if (!res.body)
2865
+ throw new Error("Exayard: the listener stream answered with no body.");
2866
+ failures = 0;
2867
+ let reconnectAtOnce = false;
2868
+ try {
2869
+ for await (const message of parseServerSentEvents(res.body)) {
2870
+ if (message.retry !== void 0)
2871
+ retryMs = message.retry;
2872
+ if (message.event === "reconnect") {
2873
+ reconnectAtOnce = true;
2874
+ break;
2875
+ }
2876
+ if (message.event !== "webhook" || message.data === "")
2877
+ continue;
2878
+ const event = JSON.parse(message.data);
2879
+ after = event.seq;
2880
+ yield event;
2881
+ }
2882
+ } catch (err) {
2883
+ if (opts.signal?.aborted)
2884
+ return;
2885
+ if (err instanceof SyntaxError)
2886
+ throw err;
2887
+ } finally {
2888
+ await res.body.cancel().catch(() => void 0);
2889
+ }
2890
+ if (!reconnectAtOnce)
2891
+ await sleepUnlessAborted(retryMs, opts.signal);
2892
+ }
2893
+ }
2894
+ };
2895
+ exports2.WebhookListeners = WebhookListeners;
2896
+ var sleepUnlessAborted = async (ms, signal) => {
2897
+ try {
2898
+ await (0, retry_1.sleep)(ms, signal);
2899
+ } catch {
2900
+ }
2901
+ };
2902
+ }
2903
+ });
2904
+
2905
+ // ../sdk/dist/options.js
2906
+ var require_options = __commonJS({
2907
+ "../sdk/dist/options.js"(exports2) {
2908
+ "use strict";
2909
+ Object.defineProperty(exports2, "__esModule", { value: true });
2910
+ exports2.isValidEndUser = exports2.END_USER_MAX_LENGTH = exports2.END_USER_HEADER = void 0;
2911
+ exports2.END_USER_HEADER = "Exayard-End-User";
2912
+ exports2.END_USER_MAX_LENGTH = 128;
2913
+ var END_USER_PATTERN = new RegExp(`^[\\x20-\\x7E]{1,${exports2.END_USER_MAX_LENGTH}}$`);
2914
+ var isValidEndUser = (value) => END_USER_PATTERN.test(value);
2915
+ exports2.isValidEndUser = isValidEndUser;
85
2916
  }
86
2917
  });
87
2918
 
@@ -90,7 +2921,48 @@ var require_webhooks = __commonJS({
90
2921
  "../sdk/dist/webhooks.js"(exports2) {
91
2922
  "use strict";
92
2923
  Object.defineProperty(exports2, "__esModule", { value: true });
93
- exports2.constructWebhookEvent = exports2.WebhookSignatureError = void 0;
2924
+ exports2.constructWebhookEvent = exports2.signWebhookPayload = exports2.DEFAULT_WEBHOOK_TOLERANCE_SECONDS = exports2.WebhookSignatureError = exports2.WEBHOOK_SIGNATURE_HEADER = exports2.WEBHOOK_EVENT_TYPES = void 0;
2925
+ exports2.WEBHOOK_EVENT_TYPES = [
2926
+ "project.created",
2927
+ "project.updated",
2928
+ "project.archived",
2929
+ "assessment.started",
2930
+ "assessment.completed",
2931
+ "assessment.cancelled",
2932
+ "assessment.failed",
2933
+ "estimate.generated",
2934
+ "bid.generated",
2935
+ "bid.submitted",
2936
+ "bid.won",
2937
+ "bid.lost",
2938
+ "bid.accepted",
2939
+ "bid.declined",
2940
+ "bid.sent",
2941
+ "bid.viewed",
2942
+ "bid.commented",
2943
+ "file.processed",
2944
+ "addendum.uploaded",
2945
+ "quote.requested",
2946
+ "quote.received",
2947
+ "quote.accepted",
2948
+ "quote.rejected",
2949
+ "quote.expired",
2950
+ "conversation.started",
2951
+ "conversation.handoff_requested",
2952
+ "conversation.closed",
2953
+ "lead.captured",
2954
+ "contact.created",
2955
+ "appointment.booked",
2956
+ "appointment.rescheduled",
2957
+ "appointment.cancelled",
2958
+ "call.completed",
2959
+ "call.missed",
2960
+ "review.requested",
2961
+ "job.written_back",
2962
+ "payment.captured",
2963
+ "payment.failed"
2964
+ ];
2965
+ exports2.WEBHOOK_SIGNATURE_HEADER = "Exayard-Signature";
94
2966
  var WebhookSignatureError2 = class extends Error {
95
2967
  code;
96
2968
  constructor(code, message) {
@@ -100,27 +2972,27 @@ var require_webhooks = __commonJS({
100
2972
  }
101
2973
  };
102
2974
  exports2.WebhookSignatureError = WebhookSignatureError2;
103
- var REPLAY_WINDOW_SECONDS = 5 * 60;
2975
+ exports2.DEFAULT_WEBHOOK_TOLERANCE_SECONDS = 5 * 60;
104
2976
  var parseSignatureHeader = (header) => {
105
- const parts = header.split(",").map((s) => s.trim());
106
2977
  let t;
107
- let v1;
108
- for (const part of parts) {
2978
+ const v1 = [];
2979
+ for (const part of header.split(",").map((s) => s.trim())) {
109
2980
  const eq = part.indexOf("=");
110
2981
  if (eq === -1)
111
2982
  continue;
112
2983
  const k = part.slice(0, eq);
113
2984
  const v = part.slice(eq + 1);
114
- if (k === "t")
115
- t = parseInt(v, 10);
116
- if (k === "v1")
117
- v1 = v;
2985
+ if (k === "t" && /^\d+$/.test(v))
2986
+ t = Number(v);
2987
+ if (k === "v1" && v)
2988
+ v1.push(v);
118
2989
  }
119
- if (t === void 0 || Number.isNaN(t) || !v1) {
120
- throw new WebhookSignatureError2("malformed_header", `Exayard-Signature must be "t=<unix>,v1=<digest>" \u2014 got "${header}".`);
2990
+ if (t === void 0 || v1.length === 0) {
2991
+ throw new WebhookSignatureError2("malformed_header", 'Exayard-Signature must be "t=<unix seconds>,v1=<digest>".');
121
2992
  }
122
2993
  return { t, v1 };
123
2994
  };
2995
+ var toText = (payload) => typeof payload === "string" ? payload : new TextDecoder().decode(payload);
124
2996
  var hmacSha256Base64 = async (secret, payload) => {
125
2997
  const key = await crypto.subtle.importKey("raw", new TextEncoder().encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
126
2998
  const digest = await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(payload));
@@ -138,17 +3010,27 @@ var require_webhooks = __commonJS({
138
3010
  result |= a.charCodeAt(i) ^ b.charCodeAt(i);
139
3011
  return result === 0;
140
3012
  };
141
- var constructWebhookEvent2 = async (rawBody, signatureHeader, secret, options = {}) => {
3013
+ var signWebhookPayload2 = async (payload, secret, options = {}) => {
3014
+ const t = options.timestamp ?? Math.floor(Date.now() / 1e3);
3015
+ const v1 = await hmacSha256Base64(secret, `${t}.${toText(payload)}`);
3016
+ return `t=${t},v1=${v1}`;
3017
+ };
3018
+ exports2.signWebhookPayload = signWebhookPayload2;
3019
+ var constructWebhookEvent2 = async (payload, signatureHeader, secret, options = {}) => {
3020
+ if (!signatureHeader)
3021
+ throw new WebhookSignatureError2("malformed_header", "The Exayard-Signature header is missing.");
142
3022
  const { t, v1 } = parseSignatureHeader(signatureHeader);
3023
+ const tolerance = options.toleranceSeconds ?? exports2.DEFAULT_WEBHOOK_TOLERANCE_SECONDS;
143
3024
  const nowSec = Math.floor((options.now ?? Date.now()) / 1e3);
144
- if (Math.abs(nowSec - t) > REPLAY_WINDOW_SECONDS) {
145
- throw new WebhookSignatureError2("replay_window_exceeded", `Signature timestamp ${t} is outside the ${REPLAY_WINDOW_SECONDS}-second window (now=${nowSec}).`);
3025
+ if (Math.abs(nowSec - t) > tolerance) {
3026
+ throw new WebhookSignatureError2("replay_window_exceeded", `Signature timestamp ${t} is outside the ${tolerance}-second window (now=${nowSec}).`);
146
3027
  }
147
- const expected = await hmacSha256Base64(secret, `${t}.${rawBody}`);
148
- if (!safeCompare(expected, v1)) {
3028
+ const text = toText(payload);
3029
+ const expected = await hmacSha256Base64(secret, `${t}.${text}`);
3030
+ if (!v1.some((candidate) => safeCompare(expected, candidate))) {
149
3031
  throw new WebhookSignatureError2("invalid_signature", "HMAC digest mismatch. Check the endpoint secret.");
150
3032
  }
151
- return JSON.parse(rawBody);
3033
+ return JSON.parse(text);
152
3034
  };
153
3035
  exports2.constructWebhookEvent = constructWebhookEvent2;
154
3036
  }
@@ -160,7 +3042,11 @@ var require_client = __commonJS({
160
3042
  "use strict";
161
3043
  Object.defineProperty(exports2, "__esModule", { value: true });
162
3044
  exports2.Exayard = void 0;
3045
+ var operations_1 = require_operations();
163
3046
  var error_1 = require_error();
3047
+ var listeners_1 = require_listeners();
3048
+ var options_1 = require_options();
3049
+ var retry_1 = require_retry();
164
3050
  var webhooks_1 = require_webhooks();
165
3051
  var UNSAFE_METHODS = /* @__PURE__ */ new Set(["POST", "PATCH", "PUT", "DELETE"]);
166
3052
  var randomIdempotencyKey = () => {
@@ -169,24 +3055,91 @@ var require_client = __commonJS({
169
3055
  }
170
3056
  return `idem_${Date.now().toString(36)}${Math.random().toString(36).slice(2, 10)}`;
171
3057
  };
3058
+ var envApiKey = () => {
3059
+ if (typeof process === "undefined" || !process.env)
3060
+ return void 0;
3061
+ const value = process.env.EXAYARD_API_KEY;
3062
+ return value && value.trim() !== "" ? value.trim() : void 0;
3063
+ };
3064
+ var checkEndUser = (value) => {
3065
+ if (value === void 0)
3066
+ return void 0;
3067
+ if (!(0, options_1.isValidEndUser)(value)) {
3068
+ throw new TypeError(`Exayard: endUser must be 1 to ${options_1.END_USER_MAX_LENGTH} printable characters.`);
3069
+ }
3070
+ return value;
3071
+ };
3072
+ var transportProblem = (code, status, detail, requestId) => ({
3073
+ type: `https://developers.exayard.com/concepts/errors#${code}`,
3074
+ title: code === "transport_error" ? "Transport Error" : code === "timeout" ? "Timeout" : "Connection Error",
3075
+ status,
3076
+ detail,
3077
+ code,
3078
+ doc_url: `https://developers.exayard.com/concepts/errors#${code}`,
3079
+ ...requestId ? { request_id: requestId } : {}
3080
+ });
172
3081
  var Exayard2 = class {
173
- apiKey;
174
- bearerToken;
3082
+ credential;
175
3083
  baseUrl;
176
3084
  timeoutMs;
3085
+ maxRetries;
177
3086
  fetchImpl;
3087
+ defaultOrganizationId;
3088
+ defaultEndUser;
178
3089
  constructor(opts = {}) {
179
- if (!opts.apiKey && !opts.bearerToken) {
180
- throw new Error("Exayard: pass `apiKey` or `bearerToken` \u2014 see https://developers.exayard.com/api-reference.");
3090
+ const credential = opts.apiKey ?? opts.bearerToken ?? envApiKey();
3091
+ if (!credential) {
3092
+ throw new Error("Exayard: pass `apiKey` (or set EXAYARD_API_KEY) or `bearerToken` \u2014 see https://developers.exayard.com/api-reference.");
181
3093
  }
182
- this.apiKey = opts.apiKey;
183
- this.bearerToken = opts.bearerToken;
3094
+ this.credential = credential;
184
3095
  this.baseUrl = (opts.baseUrl ?? "https://api.exayard.com/v1").replace(/\/$/, "");
185
3096
  this.timeoutMs = opts.timeoutMs ?? 6e4;
186
- this.fetchImpl = opts.fetch ?? fetch;
3097
+ this.maxRetries = Math.max(0, opts.maxRetries ?? retry_1.DEFAULT_MAX_RETRIES);
3098
+ this.fetchImpl = opts.fetch ?? ((input, init) => fetch(input, init));
3099
+ this.defaultOrganizationId = opts.organizationId;
3100
+ this.defaultEndUser = checkEndUser(opts.endUser);
3101
+ }
3102
+ /** The company a call names, else the client's default; undefined lets the key imply it. */
3103
+ organizationFor(named) {
3104
+ return named ?? this.defaultOrganizationId;
3105
+ }
3106
+ // One attempt: the per-attempt timeout covers the wait for the answer's headers only, so a stream body is never
3107
+ // cut by it; the caller's signal aborts both (its listener stays while the body may still be read).
3108
+ async attempt(url, init, timeoutMs, signal) {
3109
+ const ac = new AbortController();
3110
+ let timedOut = false;
3111
+ const onAbort = () => ac.abort(signal?.reason);
3112
+ if (signal?.aborted)
3113
+ ac.abort(signal.reason);
3114
+ signal?.addEventListener("abort", onAbort, { once: true });
3115
+ const timer = timeoutMs > 0 ? setTimeout(() => (timedOut = true, ac.abort()), timeoutMs) : void 0;
3116
+ try {
3117
+ return await this.fetchImpl(url, { ...init, signal: ac.signal });
3118
+ } catch (err) {
3119
+ signal?.removeEventListener("abort", onAbort);
3120
+ if (signal?.aborted)
3121
+ throw err;
3122
+ throw new error_1.APIConnectionError(timedOut ? transportProblem("timeout", 0, `No answer within ${timeoutMs} ms.`) : transportProblem("connection_error", 0, err?.message || "The connection failed."));
3123
+ } finally {
3124
+ if (timer)
3125
+ clearTimeout(timer);
3126
+ }
3127
+ }
3128
+ async toError(res) {
3129
+ const retryAfter = (0, retry_1.parseRetryAfter)(res.headers.get("Retry-After"));
3130
+ const ct = res.headers.get("Content-Type") ?? "";
3131
+ if (ct.includes("problem+json") || ct.includes("application/json")) {
3132
+ const body2 = await res.json().catch(() => null);
3133
+ if (body2 && typeof body2 === "object")
3134
+ return (0, error_1.errorFromProblem)({ ...body2, status: body2.status ?? res.status }, retryAfter);
3135
+ }
3136
+ const text = await res.text().catch(() => "");
3137
+ const body = transportProblem("transport_error", res.status, text.slice(0, 500) || `HTTP ${res.status}`, res.headers.get("X-Request-Id") ?? void 0);
3138
+ return (0, error_1.errorFromProblem)({ ...body, code: res.status === 429 ? "rate_limited" : body.code }, retryAfter);
187
3139
  }
188
3140
  // Internal request primitive. Public resource methods compose this.
189
3141
  async request(opts) {
3142
+ const call = opts.options ?? {};
190
3143
  const url = new URL(`${this.baseUrl}${opts.path}`);
191
3144
  if (opts.query) {
192
3145
  for (const [k, v] of Object.entries(opts.query)) {
@@ -196,79 +3149,282 @@ var require_client = __commonJS({
196
3149
  }
197
3150
  const headers = {
198
3151
  Accept: "application/json",
199
- Authorization: `Bearer ${this.apiKey ?? this.bearerToken}`
3152
+ Authorization: `Bearer ${this.credential}`,
3153
+ ...opts.headers
200
3154
  };
201
3155
  if (opts.body !== void 0)
202
3156
  headers["Content-Type"] = "application/json";
203
- const autoIdem = opts.autoIdempotencyKey ?? UNSAFE_METHODS.has(opts.method);
204
- if (opts.idempotencyKey) {
205
- headers["Idempotency-Key"] = opts.idempotencyKey;
206
- } else if (autoIdem) {
3157
+ const endUser = checkEndUser(call.endUser) ?? this.defaultEndUser;
3158
+ if (endUser !== void 0)
3159
+ headers[options_1.END_USER_HEADER] = endUser;
3160
+ if (call.idempotencyKey)
3161
+ headers["Idempotency-Key"] = call.idempotencyKey;
3162
+ else if (UNSAFE_METHODS.has(opts.method))
207
3163
  headers["Idempotency-Key"] = randomIdempotencyKey();
3164
+ const init = {
3165
+ method: opts.method,
3166
+ headers,
3167
+ body: opts.body !== void 0 ? JSON.stringify(opts.body) : void 0
3168
+ };
3169
+ const maxRetries = Math.max(0, call.maxRetries ?? this.maxRetries);
3170
+ const timeoutMs = call.timeoutMs ?? this.timeoutMs;
3171
+ for (let attempt = 0; ; attempt++) {
3172
+ let res;
3173
+ try {
3174
+ res = await this.attempt(url.toString(), init, timeoutMs, call.signal);
3175
+ } catch (err) {
3176
+ if (err instanceof error_1.APIConnectionError && attempt < maxRetries) {
3177
+ await (0, retry_1.sleep)((0, retry_1.retryDelayMs)(attempt + 1), call.signal);
3178
+ continue;
3179
+ }
3180
+ throw err;
3181
+ }
3182
+ if (res.ok) {
3183
+ if (opts.raw)
3184
+ return res;
3185
+ if (res.status === 204)
3186
+ return void 0;
3187
+ const text = await res.text();
3188
+ return text === "" ? void 0 : JSON.parse(text);
3189
+ }
3190
+ if ((0, retry_1.isRetryableStatus)(res.status) && attempt < maxRetries) {
3191
+ const retryAfter = (0, retry_1.parseRetryAfter)(res.headers.get("Retry-After"));
3192
+ if (retryAfter === void 0 || retryAfter <= retry_1.MAX_RETRY_AFTER_SECONDS) {
3193
+ await res.body?.cancel().catch(() => void 0);
3194
+ await (0, retry_1.sleep)((0, retry_1.retryDelayMs)(attempt + 1, retryAfter), call.signal);
3195
+ continue;
3196
+ }
3197
+ }
3198
+ throw await this.toError(res);
208
3199
  }
209
- const ac = new AbortController();
210
- const timeout = setTimeout(() => ac.abort(), this.timeoutMs);
211
- let res;
212
- try {
213
- res = await this.fetchImpl(url.toString(), {
214
- method: opts.method,
215
- headers,
216
- body: opts.body !== void 0 ? JSON.stringify(opts.body) : void 0,
217
- signal: ac.signal
218
- });
219
- } finally {
220
- clearTimeout(timeout);
3200
+ }
3201
+ /**
3202
+ * Call any API operation by its name: the spec's operationId, which is also
3203
+ * its MCP tool name and its CLI command. The input is the operation's path,
3204
+ * query and body fields merged flat, the same input its MCP tool takes.
3205
+ */
3206
+ call(name, input, opts = {}) {
3207
+ const op = operations_1.OPERATIONS[name];
3208
+ if (!op)
3209
+ throw new Error(`Exayard: no operation named ${String(name)}`);
3210
+ const args = { ...input };
3211
+ const known = new Set(op.params.map((param) => param.name));
3212
+ const unknown = Object.keys(args).filter((field) => !known.has(field));
3213
+ if (unknown.length > 0)
3214
+ throw new Error(`Exayard: ${name} does not take ${unknown.join(", ")}`);
3215
+ if (known.has("organizationId") && args.organizationId === void 0 && this.defaultOrganizationId) {
3216
+ args.organizationId = this.defaultOrganizationId;
221
3217
  }
222
- if (!res.ok) {
223
- const ct = res.headers.get("Content-Type") ?? "";
224
- if (ct.includes("problem+json") || ct.includes("application/json")) {
225
- const body = await res.json();
226
- throw new error_1.ExayardError(body);
3218
+ let path = op.path;
3219
+ const query = {};
3220
+ const body = {};
3221
+ for (const param of op.params) {
3222
+ const value = args[param.name];
3223
+ if (param.in === "path") {
3224
+ if (value === void 0 || value === null || value === "")
3225
+ throw new Error(`Exayard: ${name} needs ${param.name}`);
3226
+ path = path.replace(`{${param.name}}`, encodeURIComponent(String(value)));
3227
+ } else if (value !== void 0) {
3228
+ if (param.in === "query")
3229
+ query[param.name] = value;
3230
+ else
3231
+ body[param.name] = value;
227
3232
  }
228
- const text = await res.text().catch(() => "");
229
- throw new error_1.ExayardError({
230
- type: "https://developers.exayard.com/concepts/errors#transport_error",
231
- title: "Transport Error",
232
- status: res.status,
233
- detail: text.slice(0, 500) || `HTTP ${res.status}`,
234
- code: "transport_error",
235
- doc_url: "https://developers.exayard.com/concepts/errors#transport_error",
236
- request_id: res.headers.get("X-Request-Id") ?? void 0
237
- });
238
3233
  }
239
- if (res.status === 204)
240
- return void 0;
241
- return await res.json();
3234
+ return this.request({
3235
+ method: op.method,
3236
+ path,
3237
+ query,
3238
+ body: op.body ? body : void 0,
3239
+ raw: op.response === "stream",
3240
+ options: opts
3241
+ });
242
3242
  }
3243
+ /**
3244
+ * Every API operation as a typed method, generated from the API spec:
3245
+ * `exa.api.listVendorQuotes({ id: projectId })`.
3246
+ */
3247
+ api = Object.fromEntries(Object.entries(operations_1.OPERATION_METHODS).map(([method, name]) => [
3248
+ method,
3249
+ (input, opts) => this.call(name, input, opts)
3250
+ ]));
243
3251
  // ---------------------------------------------------------------------------
244
- // Resource surface — a curated subset covering the flows agents and integrations
245
- // most often hit. Full OpenAPI coverage can be layered on via generated code
246
- // without changing the public shape of this client.
3252
+ // Resource surface — a curated layer over the generated operations for the
3253
+ // flows agents and integrations most often hit. Each method is a call() of the
3254
+ // operation it names, so its input and output types come from the API spec.
3255
+ // Everything else is on `exa.api` and `exa.call`. `organizationId` is optional
3256
+ // everywhere: the key implies the company, or the client's default fills it.
247
3257
  // ---------------------------------------------------------------------------
248
3258
  me = {
249
- get: () => this.request({ method: "GET", path: "/me" })
3259
+ get: () => this.call("get_me", {})
250
3260
  };
251
3261
  projects = {
252
- // GET /v1/projects returns a raw array (OpenAPI schema z.array(project));
253
- // there is no { items, next_cursor } envelope and the endpoint accepts only
254
- // organizationId/status/search (no limit/cursor pagination yet).
255
- list: (query) => this.request({ method: "GET", path: "/projects", query }),
256
- get: (id, query) => this.request({ method: "GET", path: `/projects/${id}`, query }),
257
- create: (body, opts = {}) => this.request({ method: "POST", path: "/projects", body, idempotencyKey: opts.idempotencyKey }),
258
- export: (id, query) => this.request({ method: "GET", path: `/projects/${id}/export`, query }),
259
- archive: (id, query) => this.request({ method: "POST", path: `/projects/${id}/archive`, query })
3262
+ // GET /v1/projects returns a raw array; there is no { items, next_cursor }
3263
+ // envelope and no limit/cursor pagination yet.
3264
+ list: (query) => this.call("list_projects", query),
3265
+ get: (id, query = {}) => this.call("get_project", { id, ...query }),
3266
+ create: (body, opts = {}) => this.call("create_project", body, opts),
3267
+ export: (id, query = {}) => this.call("export_project", { id, ...query }),
3268
+ archive: (id, query = {}) => this.call("archive_project", { id, ...query })
3269
+ };
3270
+ // File upload is a 3-step dance: presign (get an R2 upload URL) → PUT the
3271
+ // bytes straight to R2 → confirm (which also kicks off PDF page extraction).
3272
+ // `upload` wraps all three. A PDF becomes pages asynchronously after confirm —
3273
+ // poll pages.list() until processingStatus is 'complete' before proposing/running.
3274
+ // The project decides the organization, so none of the three takes one.
3275
+ files = {
3276
+ presign: (body) => this.call("presign_file_upload", body),
3277
+ confirm: (fileId, r2Key) => this.call("confirm_file_upload", { id: fileId, r2Key }),
3278
+ // Convenience: presign → PUT bytes to the presigned R2 URL → confirm.
3279
+ // The PUT goes directly to R2 (a different host, self-authenticated by the
3280
+ // signed URL) — not through the /v1 request primitive.
3281
+ upload: async (body) => {
3282
+ const buf = body.bytes instanceof ArrayBuffer ? new Uint8Array(body.bytes) : body.bytes;
3283
+ const { fileId, uploadUrl, r2Key } = await this.files.presign({
3284
+ projectId: body.projectId,
3285
+ filename: body.filename,
3286
+ mimeType: body.mimeType,
3287
+ fileSize: buf.byteLength,
3288
+ folderId: body.folderId
3289
+ });
3290
+ const put = await this.fetchImpl(uploadUrl, {
3291
+ method: "PUT",
3292
+ headers: { "Content-Type": body.mimeType },
3293
+ // Node/undici fetch accepts a Uint8Array body at runtime; the DOM
3294
+ // BodyInit type doesn't list it, so cast at this boundary.
3295
+ body: buf
3296
+ });
3297
+ if (!put.ok) {
3298
+ throw new error_1.ExayardError({
3299
+ type: "https://developers.exayard.com/concepts/errors#transport_error",
3300
+ title: "Upload Failed",
3301
+ status: put.status,
3302
+ detail: `PUT to storage failed: HTTP ${put.status}`,
3303
+ code: "transport_error",
3304
+ doc_url: "https://developers.exayard.com/concepts/errors#transport_error"
3305
+ });
3306
+ }
3307
+ await this.files.confirm(fileId, r2Key);
3308
+ return { fileId };
3309
+ }
3310
+ };
3311
+ // List a project's pages (grouped by file). Use to discover pageIds + poll
3312
+ // each file's processingStatus until extraction finishes.
3313
+ pages = {
3314
+ list: (projectId, query = {}) => this.call("list_pages", { id: projectId, ...query })
3315
+ };
3316
+ // AI takeoff assessments. Reads resolve the org from the project/assessment
3317
+ // (no organizationId). `run` kicks off an AI run on an existing project's
3318
+ // pages — pair with the assessment.completed webhook + latest()/takeoffSummary
3319
+ // to pull results once it finishes.
3320
+ assessments = {
3321
+ list: (projectId) => this.call("list_assessments", { id: projectId }),
3322
+ latest: (projectId) => this.call("get_latest_assessment", { id: projectId }),
3323
+ get: (id) => this.call("get_assessment", { id }),
3324
+ takeoffSummary: (projectId) => this.call("get_takeoff_summary", { id: projectId }),
3325
+ // Ask the AI what to measure: a natural-language prompt + pageIds → a
3326
+ // proposed `elements` array (id/name/category/hexColor) you can pass
3327
+ // straight to run(). The bridge for callers who don't already know the
3328
+ // elements.
3329
+ propose: (projectId, body) => this.call("propose_analysis", { id: projectId, ...body }),
3330
+ // Run an AI takeoff analysis on an existing project and let it complete
3331
+ // end-to-end (no human approval step). You specify exactly what to detect
3332
+ // via `elements` (each: id, name, category area|linear|count, hexColor) on
3333
+ // the given pageIds. Returns { assessmentId, pageIds }; track completion via
3334
+ // the assessment.completed webhook + latest()/takeoffSummary.
3335
+ //
3336
+ // (The auto-detect POST /assessments path pauses at awaiting_approval,
3337
+ // which needs a human, so it isn't wrapped here; it is exa.api.createAssessment.)
3338
+ run: (projectId, body, opts = {}) => this.call("run_approved_analysis", { id: projectId, ...body }, opts)
3339
+ };
3340
+ // Generate an estimate from a project's takeoff and persist it as a document.
3341
+ // Blocking: the estimator typically runs 30–60s, which is at the default
3342
+ // 60s client timeout — raise `timeoutMs` when constructing the client.
3343
+ //
3344
+ // The saved document is PRIVATE to your organization. `share: true` also
3345
+ // publishes it on a public link anyone holding the URL can read, and makes
3346
+ // `documentUrl` the public path instead of the signed-in one. Set it only
3347
+ // when the customer asked for a shareable link; `documents.setSharing` can
3348
+ // share or unshare later either way.
3349
+ //
3350
+ // (The streaming sibling POST /v1/projects/{id}/estimates/generate persists
3351
+ // no document and has no `share` flag; it is exa.api.generateEstimate, which
3352
+ // returns the event-stream Response.)
3353
+ estimates = {
3354
+ create: (projectId, body, opts = {}) => this.call("create_estimate", { id: projectId, ...body }, opts)
3355
+ };
3356
+ // Same estimator wrapped with bid-formatting instructions, persisted as a
3357
+ // bid document. Same blocking timing and same private-by-default `share`
3358
+ // semantics as estimates.create. When `measurementContext` is omitted the
3359
+ // takeoff is auto-loaded server-side, which can 422 on line items with
3360
+ // missing formula variables — retry with `ignoreMissingVariables: true` to
3361
+ // skip them (the skipped rows come back in `warnings`).
3362
+ bids = {
3363
+ create: (projectId, body, opts = {}) => this.call("create_bid", { id: projectId, ...body }, opts)
3364
+ };
3365
+ // Documents (estimates, bids, and anything else saved to a project) plus the
3366
+ // sharing switch. Documents are private to the organization until someone
3367
+ // enables sharing; enabling mints a secret and publishes a link readable by
3368
+ // anyone on the internet who has the URL — no sign-in, no expiry.
3369
+ //
3370
+ // Disabling revokes access but KEEPS the secret, so re-enabling later revives
3371
+ // the same URL. There is no rotate operation: a leaked link means the
3372
+ // document is burned, not that a disable/enable cycle gives you a fresh one.
3373
+ documents = {
3374
+ // Both list endpoints return a raw array (no { items, next_cursor }
3375
+ // envelope), like projects.list. They're here because setSharing needs a
3376
+ // document id and only documents you just generated hand you one directly.
3377
+ list: (query) => this.call("list_org_documents", query),
3378
+ listByProject: (projectId, query = {}) => this.call("list_documents", { id: projectId, ...query }),
3379
+ // Current sharing state rides the document body: `shareEnabled` and
3380
+ // `shareSecret` come back on GET.
3381
+ get: (id, query = {}) => this.call("get_document", { id, ...query }),
3382
+ // A document id belonging to another organization answers 404 exactly like
3383
+ // an id that doesn't exist.
3384
+ setSharing: (id, body, opts = {}) => this.call("set_document_sharing", { id, ...body }, opts)
3385
+ };
3386
+ // Vendor quotes. Request a quote from a vendor for a project, advance it to
3387
+ // `received` once the vendor responds (with priced line items), then record a
3388
+ // terminal accepted | rejected | expired decision. Each transition fires a
3389
+ // quote.* webhook. Note: there is no product UI for vendor quotes yet — these
3390
+ // are visible only via this API and the webhooks.
3391
+ quotes = {
3392
+ list: (projectId, query) => this.call("list_vendor_quotes", { id: projectId, ...query }),
3393
+ get: (id, query = {}) => this.call("get_vendor_quote", { id, ...query }),
3394
+ create: (projectId, body, opts = {}) => this.call("create_vendor_quote", { id: projectId, ...body }, opts),
3395
+ receive: (id, body, opts = {}) => this.call("receive_vendor_quote", { id, ...body }, opts),
3396
+ updateStatus: (id, body, opts = {}) => this.call("update_vendor_quote_status", { id, ...body }, opts)
260
3397
  };
261
3398
  help = {
262
- search: (body) => this.request({ method: "POST", path: "/help/search", body, autoIdempotencyKey: false })
3399
+ search: (body) => this.call("search_help_articles", body)
263
3400
  };
264
3401
  webhooks = {
265
- listEndpoints: (query) => this.request({ method: "GET", path: "/webhook_endpoints", query }),
266
- createEndpoint: (body) => this.request({ method: "POST", path: "/webhook_endpoints", body }),
267
- deleteEndpoint: (id, query) => this.request({ method: "DELETE", path: `/webhook_endpoints/${id}`, query }),
268
- listDeliveries: (id, query) => this.request({ method: "GET", path: `/webhook_endpoints/${id}/deliveries`, query }),
3402
+ listEndpoints: (query = {}) => this.call("list_webhook_endpoints", query),
3403
+ createEndpoint: (body) => this.call("create_webhook_endpoint", body),
3404
+ deleteEndpoint: (id, query = {}) => this.call("delete_webhook_endpoint", { id, ...query }),
3405
+ listDeliveries: (id, query = {}) => this.call("list_webhook_deliveries", { id, ...query }),
3406
+ /** Send a test event of one type to an endpoint, signed as usual, its body marked `"test": true`. */
3407
+ sendTestEvent: (id, body, opts = {}) => this.request({
3408
+ method: "POST",
3409
+ path: `/webhook_endpoints/${encodeURIComponent(id)}/test`,
3410
+ body: { ...body, organizationId: this.organizationFor(body.organizationId) },
3411
+ options: opts
3412
+ }),
3413
+ /** Send a delivery again, as a new delivery with the same event id and body. */
3414
+ resendDelivery: (deliveryId, body = {}, opts = {}) => this.request({
3415
+ method: "POST",
3416
+ path: `/webhook_deliveries/${encodeURIComponent(deliveryId)}/resend`,
3417
+ body: { organizationId: this.organizationFor(body.organizationId) },
3418
+ options: opts
3419
+ }),
3420
+ /** Receive the company's events over a stream, without a public endpoint (what `exayard listen` runs on). */
3421
+ listeners: new listeners_1.WebhookListeners({
3422
+ request: (opts) => this.request(opts),
3423
+ organizationId: (named) => this.organizationFor(named)
3424
+ }),
269
3425
  /**
270
- * Parse + verify an inbound webhook delivery. Throws WebhookSignatureError
271
- * on any signature failure — always catch and return 400 to let us retry.
3426
+ * Parse + verify an inbound webhook delivery: `constructEvent(rawBody, signatureHeader, secret)`. Throws
3427
+ * WebhookSignatureError on any signature failure — always catch and return 400 to let us retry.
272
3428
  */
273
3429
  constructEvent: webhooks_1.constructWebhookEvent
274
3430
  };
@@ -282,22 +3438,95 @@ var require_dist = __commonJS({
282
3438
  "../sdk/dist/index.js"(exports2) {
283
3439
  "use strict";
284
3440
  Object.defineProperty(exports2, "__esModule", { value: true });
285
- exports2.WebhookSignatureError = exports2.constructWebhookEvent = exports2.ExayardError = exports2.Exayard = void 0;
3441
+ exports2.parseRetryAfter = exports2.DEFAULT_MAX_RETRIES = exports2.WebhookListeners = exports2.parseServerSentEvents = exports2.DEFAULT_WEBHOOK_TOLERANCE_SECONDS = exports2.WEBHOOK_SIGNATURE_HEADER = exports2.WEBHOOK_EVENT_TYPES = exports2.WebhookSignatureError = exports2.signWebhookPayload = exports2.constructWebhookEvent = exports2.UnprocessableEntityError = exports2.RateLimitError = exports2.PermissionDeniedError = exports2.PaymentRequiredError = exports2.NotFoundError = exports2.InternalServerError = exports2.ExayardError = exports2.errorFromProblem = exports2.ConflictError = exports2.BadRequestError = exports2.AuthenticationError = exports2.APIConnectionError = exports2.isValidEndUser = exports2.END_USER_MAX_LENGTH = exports2.END_USER_HEADER = exports2.OPERATIONS = exports2.Exayard = void 0;
286
3442
  var client_1 = require_client();
287
3443
  Object.defineProperty(exports2, "Exayard", { enumerable: true, get: function() {
288
3444
  return client_1.Exayard;
289
3445
  } });
3446
+ var operations_1 = require_operations();
3447
+ Object.defineProperty(exports2, "OPERATIONS", { enumerable: true, get: function() {
3448
+ return operations_1.OPERATIONS;
3449
+ } });
3450
+ var options_1 = require_options();
3451
+ Object.defineProperty(exports2, "END_USER_HEADER", { enumerable: true, get: function() {
3452
+ return options_1.END_USER_HEADER;
3453
+ } });
3454
+ Object.defineProperty(exports2, "END_USER_MAX_LENGTH", { enumerable: true, get: function() {
3455
+ return options_1.END_USER_MAX_LENGTH;
3456
+ } });
3457
+ Object.defineProperty(exports2, "isValidEndUser", { enumerable: true, get: function() {
3458
+ return options_1.isValidEndUser;
3459
+ } });
290
3460
  var error_1 = require_error();
3461
+ Object.defineProperty(exports2, "APIConnectionError", { enumerable: true, get: function() {
3462
+ return error_1.APIConnectionError;
3463
+ } });
3464
+ Object.defineProperty(exports2, "AuthenticationError", { enumerable: true, get: function() {
3465
+ return error_1.AuthenticationError;
3466
+ } });
3467
+ Object.defineProperty(exports2, "BadRequestError", { enumerable: true, get: function() {
3468
+ return error_1.BadRequestError;
3469
+ } });
3470
+ Object.defineProperty(exports2, "ConflictError", { enumerable: true, get: function() {
3471
+ return error_1.ConflictError;
3472
+ } });
3473
+ Object.defineProperty(exports2, "errorFromProblem", { enumerable: true, get: function() {
3474
+ return error_1.errorFromProblem;
3475
+ } });
291
3476
  Object.defineProperty(exports2, "ExayardError", { enumerable: true, get: function() {
292
3477
  return error_1.ExayardError;
293
3478
  } });
3479
+ Object.defineProperty(exports2, "InternalServerError", { enumerable: true, get: function() {
3480
+ return error_1.InternalServerError;
3481
+ } });
3482
+ Object.defineProperty(exports2, "NotFoundError", { enumerable: true, get: function() {
3483
+ return error_1.NotFoundError;
3484
+ } });
3485
+ Object.defineProperty(exports2, "PaymentRequiredError", { enumerable: true, get: function() {
3486
+ return error_1.PaymentRequiredError;
3487
+ } });
3488
+ Object.defineProperty(exports2, "PermissionDeniedError", { enumerable: true, get: function() {
3489
+ return error_1.PermissionDeniedError;
3490
+ } });
3491
+ Object.defineProperty(exports2, "RateLimitError", { enumerable: true, get: function() {
3492
+ return error_1.RateLimitError;
3493
+ } });
3494
+ Object.defineProperty(exports2, "UnprocessableEntityError", { enumerable: true, get: function() {
3495
+ return error_1.UnprocessableEntityError;
3496
+ } });
294
3497
  var webhooks_1 = require_webhooks();
295
3498
  Object.defineProperty(exports2, "constructWebhookEvent", { enumerable: true, get: function() {
296
3499
  return webhooks_1.constructWebhookEvent;
297
3500
  } });
3501
+ Object.defineProperty(exports2, "signWebhookPayload", { enumerable: true, get: function() {
3502
+ return webhooks_1.signWebhookPayload;
3503
+ } });
298
3504
  Object.defineProperty(exports2, "WebhookSignatureError", { enumerable: true, get: function() {
299
3505
  return webhooks_1.WebhookSignatureError;
300
3506
  } });
3507
+ Object.defineProperty(exports2, "WEBHOOK_EVENT_TYPES", { enumerable: true, get: function() {
3508
+ return webhooks_1.WEBHOOK_EVENT_TYPES;
3509
+ } });
3510
+ Object.defineProperty(exports2, "WEBHOOK_SIGNATURE_HEADER", { enumerable: true, get: function() {
3511
+ return webhooks_1.WEBHOOK_SIGNATURE_HEADER;
3512
+ } });
3513
+ Object.defineProperty(exports2, "DEFAULT_WEBHOOK_TOLERANCE_SECONDS", { enumerable: true, get: function() {
3514
+ return webhooks_1.DEFAULT_WEBHOOK_TOLERANCE_SECONDS;
3515
+ } });
3516
+ var listeners_1 = require_listeners();
3517
+ Object.defineProperty(exports2, "parseServerSentEvents", { enumerable: true, get: function() {
3518
+ return listeners_1.parseServerSentEvents;
3519
+ } });
3520
+ Object.defineProperty(exports2, "WebhookListeners", { enumerable: true, get: function() {
3521
+ return listeners_1.WebhookListeners;
3522
+ } });
3523
+ var retry_1 = require_retry();
3524
+ Object.defineProperty(exports2, "DEFAULT_MAX_RETRIES", { enumerable: true, get: function() {
3525
+ return retry_1.DEFAULT_MAX_RETRIES;
3526
+ } });
3527
+ Object.defineProperty(exports2, "parseRetryAfter", { enumerable: true, get: function() {
3528
+ return retry_1.parseRetryAfter;
3529
+ } });
301
3530
  }
302
3531
  });
303
3532
 
@@ -312,13 +3541,22 @@ __export(index_exports, {
312
3541
  module.exports = __toCommonJS(index_exports);
313
3542
 
314
3543
  // src/commands.ts
315
- var import_sdk = __toESM(require_dist());
3544
+ var import_sdk3 = __toESM(require_dist());
316
3545
 
317
3546
  // src/config.ts
318
3547
  var import_node_fs = require("fs");
319
3548
  var import_node_os = require("os");
320
3549
  var import_node_path = require("path");
321
3550
  var configPath = () => (0, import_node_path.join)((0, import_node_os.homedir)(), ".exayard", "config.json");
3551
+ var readConfigFile = () => {
3552
+ const path = configPath();
3553
+ if (!(0, import_node_fs.existsSync)(path)) return {};
3554
+ try {
3555
+ return JSON.parse((0, import_node_fs.readFileSync)(path, "utf8"));
3556
+ } catch {
3557
+ return {};
3558
+ }
3559
+ };
322
3560
  var readConfig = () => {
323
3561
  const fromEnv = {
324
3562
  apiKey: process.env.EXAYARD_API_KEY,
@@ -327,37 +3565,341 @@ var readConfig = () => {
327
3565
  };
328
3566
  const path = configPath();
329
3567
  if (!(0, import_node_fs.existsSync)(path)) return fromEnv;
330
- try {
331
- const raw = JSON.parse((0, import_node_fs.readFileSync)(path, "utf8"));
332
- return { ...raw, ...Object.fromEntries(Object.entries(fromEnv).filter(([, v]) => v !== void 0 && v !== "")) };
333
- } catch {
334
- return fromEnv;
335
- }
3568
+ return { ...readConfigFile(), ...Object.fromEntries(Object.entries(fromEnv).filter(([, v]) => v !== void 0 && v !== "")) };
336
3569
  };
337
3570
  var writeConfig = (config) => {
338
3571
  const path = configPath();
339
- (0, import_node_fs.mkdirSync)((0, import_node_path.dirname)(path), { recursive: true });
340
- (0, import_node_fs.writeFileSync)(path, JSON.stringify(config, null, 2));
3572
+ (0, import_node_fs.mkdirSync)((0, import_node_path.dirname)(path), { recursive: true, mode: 448 });
341
3573
  try {
342
- (0, import_node_fs.chmodSync)(path, 384);
3574
+ (0, import_node_fs.chmodSync)((0, import_node_path.dirname)(path), 448);
343
3575
  } catch {
344
3576
  }
3577
+ const tmp = `${path}.${process.pid}.tmp`;
3578
+ (0, import_node_fs.writeFileSync)(tmp, JSON.stringify(config, null, 2), { mode: 384 });
3579
+ try {
3580
+ (0, import_node_fs.chmodSync)(tmp, 384);
3581
+ } catch {
3582
+ }
3583
+ (0, import_node_fs.renameSync)(tmp, path);
3584
+ };
3585
+ var saveOAuthSession = (session) => {
3586
+ writeConfig({ ...readConfigFile(), oauth: session });
345
3587
  };
346
3588
  var clearConfig = () => {
347
3589
  try {
348
- (0, import_node_fs.writeFileSync)(configPath(), JSON.stringify({}));
3590
+ writeConfig({});
349
3591
  } catch {
350
3592
  }
351
3593
  };
352
3594
 
3595
+ // src/listen.ts
3596
+ var import_node_os2 = require("os");
3597
+ var import_sdk = __toESM(require_dist());
3598
+ var organizationOf = (payload) => {
3599
+ try {
3600
+ const parsed = JSON.parse(payload);
3601
+ return typeof parsed.organizationId === "string" ? parsed.organizationId : void 0;
3602
+ } catch {
3603
+ return void 0;
3604
+ }
3605
+ };
3606
+ var forwardEvent = async (event, forwardTo, secret, fetchImpl = fetch) => {
3607
+ const organizationId = organizationOf(event.payload);
3608
+ try {
3609
+ const res = await fetchImpl(forwardTo, {
3610
+ method: "POST",
3611
+ headers: {
3612
+ "Content-Type": "application/json",
3613
+ "User-Agent": "Exayard-CLI-Listen",
3614
+ "Exayard-Signature": await (0, import_sdk.signWebhookPayload)(event.payload, secret),
3615
+ "Exayard-Event-Id": event.eventId,
3616
+ "Exayard-Event-Type": event.type,
3617
+ ...organizationId ? { "Exayard-Organization-Id": organizationId } : {}
3618
+ },
3619
+ body: event.payload,
3620
+ redirect: "manual"
3621
+ });
3622
+ await res.body?.cancel().catch(() => void 0);
3623
+ return { status: res.status };
3624
+ } catch (err) {
3625
+ return { error: err.message || "request failed" };
3626
+ }
3627
+ };
3628
+ var runListen = async (exa, opts) => {
3629
+ const listener = await exa.webhooks.listeners.start({
3630
+ organizationId: opts.organizationId,
3631
+ ...opts.events && opts.events.length > 0 ? { events: opts.events } : {},
3632
+ name: (opts.name ?? (0, import_node_os2.hostname)()).slice(0, 100)
3633
+ });
3634
+ const target = opts.forwardTo ? ` and forwarding to ${opts.forwardTo}` : "";
3635
+ if (opts.json) opts.write(JSON.stringify({ ready: true, listenerId: listener.id, secret: listener.secret, events: listener.events }));
3636
+ else {
3637
+ opts.writeErr(`Ready. Listening for ${listener.events.join(", ")}${target}. Press Ctrl-C to stop.`);
3638
+ opts.writeErr(`Your webhook signing secret is ${listener.secret}`);
3639
+ }
3640
+ let code = 0;
3641
+ try {
3642
+ for await (const event of exa.webhooks.listeners.events(listener.id, { organizationId: opts.organizationId, signal: opts.signal })) {
3643
+ if (!opts.forwardTo) {
3644
+ opts.write(opts.json ? JSON.stringify({ seq: event.seq, type: event.type, eventId: event.eventId, payload: event.payload }) : event.payload);
3645
+ continue;
3646
+ }
3647
+ if (!opts.json) opts.writeErr(`--> ${event.type}${event.test ? " (test)" : ""} [${event.eventId}]`);
3648
+ const result = await forwardEvent(event, opts.forwardTo, listener.secret, opts.fetch);
3649
+ if (opts.json) opts.write(JSON.stringify({ seq: event.seq, type: event.type, eventId: event.eventId, ...result }));
3650
+ else if ("status" in result) opts.writeErr(`<-- [${result.status}] POST ${opts.forwardTo}`);
3651
+ else opts.writeErr(`<-- failed POST ${opts.forwardTo}: ${result.error}`);
3652
+ }
3653
+ } catch (err) {
3654
+ if (!opts.signal.aborted) {
3655
+ opts.writeErr(`The listener stopped: ${err.message}`);
3656
+ code = 1;
3657
+ }
3658
+ } finally {
3659
+ await exa.webhooks.listeners.stop(listener.id, { organizationId: opts.organizationId }, { maxRetries: 1 }).catch(() => void 0);
3660
+ }
3661
+ if (!opts.json && code === 0) opts.writeErr("Stopped.");
3662
+ return code;
3663
+ };
3664
+
3665
+ // src/oauth.ts
3666
+ var import_node_crypto = require("crypto");
3667
+ var import_node_child_process = require("child_process");
3668
+ var import_node_http = require("http");
3669
+ var OAUTH_SCOPES = ["openid", "profile", "email", "offline_access"];
3670
+ var REFRESH_MARGIN_MS = 6e4;
3671
+ var OAuthLoginError = class extends Error {
3672
+ constructor(message) {
3673
+ super(message);
3674
+ this.name = "OAuthLoginError";
3675
+ }
3676
+ };
3677
+ var base64url = (bytes) => bytes.toString("base64url");
3678
+ var pkcePair = () => {
3679
+ const verifier = base64url((0, import_node_crypto.randomBytes)(32));
3680
+ return { verifier, challenge: base64url((0, import_node_crypto.createHash)("sha256").update(verifier).digest()) };
3681
+ };
3682
+ var getJson = async (fetchImpl, url, what) => {
3683
+ let res;
3684
+ try {
3685
+ res = await fetchImpl(url, { headers: { Accept: "application/json" } });
3686
+ } catch (err) {
3687
+ throw new OAuthLoginError(`Could not reach ${url} (${what}): ${err.message}`);
3688
+ }
3689
+ if (!res.ok) throw new OAuthLoginError(`${url} (${what}) answered ${res.status}.`);
3690
+ return await res.json();
3691
+ };
3692
+ var stringField = (obj, key, where) => {
3693
+ const value = obj[key];
3694
+ if (typeof value !== "string" || value === "") throw new OAuthLoginError(`${where} has no ${key}.`);
3695
+ return value;
3696
+ };
3697
+ var discoverOAuth = async (apiBaseUrl, fetchImpl) => {
3698
+ const origin = new URL(apiBaseUrl).origin;
3699
+ const prm = await getJson(fetchImpl, `${origin}/.well-known/oauth-protected-resource`, "protected resource metadata");
3700
+ const servers = prm.authorization_servers;
3701
+ const issuer = Array.isArray(servers) && typeof servers[0] === "string" ? servers[0] : void 0;
3702
+ if (!issuer) throw new OAuthLoginError(`${origin} names no authorization server.`);
3703
+ const asUrl = `${issuer.replace(/\/$/, "")}/.well-known/oauth-authorization-server`;
3704
+ const meta = await getJson(fetchImpl, asUrl, "authorization server metadata");
3705
+ const methods = meta.code_challenge_methods_supported;
3706
+ if (Array.isArray(methods) && !methods.includes("S256")) throw new OAuthLoginError(`${issuer} does not support PKCE S256.`);
3707
+ return {
3708
+ resource: typeof prm.resource === "string" && prm.resource !== "" ? prm.resource : origin,
3709
+ authorizationEndpoint: stringField(meta, "authorization_endpoint", asUrl),
3710
+ tokenEndpoint: stringField(meta, "token_endpoint", asUrl),
3711
+ registrationEndpoint: stringField(meta, "registration_endpoint", asUrl)
3712
+ };
3713
+ };
3714
+ var registerClient = async (registrationEndpoint, redirectUri, fetchImpl) => {
3715
+ const res = await fetchImpl(registrationEndpoint, {
3716
+ method: "POST",
3717
+ headers: { "Content-Type": "application/json", Accept: "application/json" },
3718
+ body: JSON.stringify({
3719
+ client_name: "Exayard CLI",
3720
+ redirect_uris: [redirectUri],
3721
+ grant_types: ["authorization_code", "refresh_token"],
3722
+ response_types: ["code"],
3723
+ token_endpoint_auth_method: "none",
3724
+ scope: OAUTH_SCOPES.join(" ")
3725
+ })
3726
+ });
3727
+ const body = await res.json().catch(() => ({}));
3728
+ if (!res.ok) throw new OAuthLoginError(`Registering the CLI failed (${res.status}): ${oauthErrorText(body)}`);
3729
+ return stringField(body, "client_id", "The registration answer");
3730
+ };
3731
+ var oauthErrorText = (body) => [body.error, body.error_description].filter((v) => typeof v === "string" && v !== "").join(": ") || "no detail";
3732
+ var tokenRequest = async (tokenEndpoint, form, fetchImpl, now) => {
3733
+ const params = new URLSearchParams();
3734
+ for (const [k, v] of Object.entries(form)) if (v !== void 0) params.set(k, v);
3735
+ const res = await fetchImpl(tokenEndpoint, {
3736
+ method: "POST",
3737
+ headers: { "Content-Type": "application/x-www-form-urlencoded", Accept: "application/json" },
3738
+ body: params.toString()
3739
+ });
3740
+ const body = await res.json().catch(() => ({}));
3741
+ if (!res.ok) throw new OAuthLoginError(`The token request failed (${res.status}): ${oauthErrorText(body)}`);
3742
+ const accessToken = stringField(body, "access_token", "The token answer");
3743
+ const expiresIn = typeof body.expires_in === "number" ? body.expires_in : Number(body.expires_in);
3744
+ return {
3745
+ accessToken,
3746
+ ...typeof body.refresh_token === "string" && body.refresh_token !== "" ? { refreshToken: body.refresh_token } : {},
3747
+ ...Number.isFinite(expiresIn) && expiresIn > 0 ? { expiresAt: now() + expiresIn * 1e3 } : {}
3748
+ };
3749
+ };
3750
+ var CALLBACK_PAGE = (title) => `<!doctype html><html><head><meta charset="utf-8"><title>Exayard CLI</title></head><body style="font-family:system-ui,sans-serif;padding:3rem"><h1>${title}</h1><p>You can close this tab and go back to the terminal.</p></body></html>`;
3751
+ var startLoopback = async (port, signal) => {
3752
+ let resolveCallback = () => void 0;
3753
+ let rejectCallback = () => void 0;
3754
+ const callback = new Promise((resolve, reject) => {
3755
+ resolveCallback = resolve;
3756
+ rejectCallback = reject;
3757
+ });
3758
+ const server = (0, import_node_http.createServer)((req, res) => {
3759
+ const url = new URL(req.url ?? "/", "http://127.0.0.1");
3760
+ if (url.pathname !== "/callback") {
3761
+ res.writeHead(404, { "Content-Type": "text/plain" }).end("Not found");
3762
+ return;
3763
+ }
3764
+ const failed = url.searchParams.has("error") || !url.searchParams.has("code");
3765
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" }).end(CALLBACK_PAGE(failed ? "Sign-in did not finish" : "You are signed in"));
3766
+ resolveCallback(url.searchParams);
3767
+ });
3768
+ await new Promise((resolve, reject) => {
3769
+ server.once("error", reject);
3770
+ server.listen(port, "127.0.0.1", () => resolve());
3771
+ });
3772
+ const close = () => {
3773
+ server.closeAllConnections();
3774
+ server.close();
3775
+ };
3776
+ const onAbort = () => rejectCallback(new OAuthLoginError("Sign-in cancelled."));
3777
+ if (signal?.aborted) onAbort();
3778
+ signal?.addEventListener("abort", onAbort, { once: true });
3779
+ const { port: bound } = server.address();
3780
+ return { redirectUri: `http://127.0.0.1:${bound}/callback`, callback, close };
3781
+ };
3782
+ var openInBrowser = (url) => {
3783
+ const [cmd, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["cmd", ["/c", "start", '""', url.replace(/&/g, "^&")]] : ["xdg-open", [url]];
3784
+ try {
3785
+ const child = (0, import_node_child_process.spawn)(cmd, args, { stdio: "ignore", detached: true });
3786
+ child.on("error", () => void 0);
3787
+ child.unref();
3788
+ } catch {
3789
+ }
3790
+ };
3791
+ var browserLogin = async (opts) => {
3792
+ const fetchImpl = opts.fetch ?? ((input, init) => fetch(input, init));
3793
+ const now = opts.now ?? Date.now;
3794
+ const discovery = await discoverOAuth(opts.apiBaseUrl, fetchImpl);
3795
+ const loopback = await startLoopback(opts.port ?? 0, opts.signal);
3796
+ try {
3797
+ const clientId = await registerClient(discovery.registrationEndpoint, loopback.redirectUri, fetchImpl);
3798
+ const { verifier, challenge } = pkcePair();
3799
+ const state = base64url((0, import_node_crypto.randomBytes)(16));
3800
+ const authorize = new URL(discovery.authorizationEndpoint);
3801
+ authorize.search = new URLSearchParams({
3802
+ response_type: "code",
3803
+ client_id: clientId,
3804
+ redirect_uri: loopback.redirectUri,
3805
+ scope: OAUTH_SCOPES.join(" "),
3806
+ state,
3807
+ code_challenge: challenge,
3808
+ code_challenge_method: "S256",
3809
+ resource: discovery.resource
3810
+ }).toString();
3811
+ opts.writeErr("Opening your browser to sign in. If it does not open, visit:");
3812
+ opts.writeErr(authorize.toString());
3813
+ await Promise.resolve(opts.openBrowser ? opts.openBrowser(authorize.toString()) : openInBrowser(authorize.toString())).catch(() => void 0);
3814
+ const query = await loopback.callback;
3815
+ if (query.get("error")) {
3816
+ throw new OAuthLoginError(`Sign-in refused: ${[query.get("error"), query.get("error_description")].filter(Boolean).join(": ")}`);
3817
+ }
3818
+ if (query.get("state") !== state) throw new OAuthLoginError("Sign-in answer did not match this login (state). Run `exayard login` again.");
3819
+ const code = query.get("code");
3820
+ if (!code) throw new OAuthLoginError("Sign-in answer carried no code.");
3821
+ const tokens = await tokenRequest(
3822
+ discovery.tokenEndpoint,
3823
+ {
3824
+ grant_type: "authorization_code",
3825
+ code,
3826
+ redirect_uri: loopback.redirectUri,
3827
+ client_id: clientId,
3828
+ code_verifier: verifier,
3829
+ resource: discovery.resource
3830
+ },
3831
+ fetchImpl,
3832
+ now
3833
+ );
3834
+ return { ...tokens, clientId, tokenEndpoint: discovery.tokenEndpoint, redirectUri: loopback.redirectUri, resource: discovery.resource };
3835
+ } finally {
3836
+ loopback.close();
3837
+ }
3838
+ };
3839
+ var refreshSession = async (session, fetchImpl, now = Date.now) => {
3840
+ if (!session.refreshToken) throw new OAuthLoginError("Your sign-in ended. Run `exayard login` again.");
3841
+ let tokens;
3842
+ try {
3843
+ tokens = await tokenRequest(
3844
+ session.tokenEndpoint,
3845
+ { grant_type: "refresh_token", refresh_token: session.refreshToken, client_id: session.clientId, resource: session.resource },
3846
+ fetchImpl,
3847
+ now
3848
+ );
3849
+ } catch (err) {
3850
+ throw new OAuthLoginError(`Your sign-in could not be renewed (${err.message}). Run \`exayard login\` again.`);
3851
+ }
3852
+ return { ...session, ...tokens, refreshToken: tokens.refreshToken ?? session.refreshToken };
3853
+ };
3854
+ var needsRefresh = (session, now) => session.expiresAt !== void 0 && session.expiresAt - now < REFRESH_MARGIN_MS;
3855
+ var sessionFetch = (initial, save, fetchImpl = (input, init) => fetch(input, init), now = Date.now) => {
3856
+ let session = initial;
3857
+ let refreshing = null;
3858
+ const current = async () => {
3859
+ if (!needsRefresh(session, now())) return session;
3860
+ refreshing ??= refreshSession(session, fetchImpl, now).then((next) => {
3861
+ session = next;
3862
+ save(next);
3863
+ return next;
3864
+ }).finally(() => {
3865
+ refreshing = null;
3866
+ });
3867
+ return refreshing;
3868
+ };
3869
+ return async (input, init) => {
3870
+ const headers = new Headers(init?.headers);
3871
+ if (!headers.has("Authorization")) return fetchImpl(input, init);
3872
+ let accessToken;
3873
+ try {
3874
+ ;
3875
+ ({ accessToken } = await current());
3876
+ } catch (err) {
3877
+ return new Response(
3878
+ JSON.stringify({ type: "about:blank", title: "Sign-in ended", status: 401, code: "unauthenticated", detail: err.message }),
3879
+ { status: 401, headers: { "Content-Type": "application/problem+json" } }
3880
+ );
3881
+ }
3882
+ headers.set("Authorization", `Bearer ${accessToken}`);
3883
+ return fetchImpl(input, { ...init, headers });
3884
+ };
3885
+ };
3886
+
3887
+ // src/operations.ts
3888
+ var import_sdk2 = __toESM(require_dist());
3889
+ var kebab = (name) => name.replace(/_/g, "-");
3890
+ var BY_COMMAND = new Map(
3891
+ Object.keys(import_sdk2.OPERATIONS).map((name) => [kebab(name), name])
3892
+ );
3893
+
353
3894
  // src/commands.ts
354
3895
  var makeClient = (ctx) => {
355
3896
  const cfg = readConfig();
356
3897
  const apiKey = (typeof ctx.flags["api-key"] === "string" ? ctx.flags["api-key"] : void 0) ?? cfg.apiKey;
357
- if (!apiKey) {
358
- throw new Error("No API key. Run `exayard login` or set EXAYARD_API_KEY.");
3898
+ if (apiKey) return new import_sdk3.Exayard({ apiKey, baseUrl: cfg.baseUrl });
3899
+ if (cfg.oauth) {
3900
+ return new import_sdk3.Exayard({ bearerToken: cfg.oauth.accessToken, baseUrl: cfg.baseUrl, fetch: sessionFetch(cfg.oauth, saveOAuthSession) });
359
3901
  }
360
- return new import_sdk.Exayard({ apiKey, baseUrl: cfg.baseUrl });
3902
+ throw new Error("Not logged in. Run `exayard login` or set EXAYARD_API_KEY.");
361
3903
  };
362
3904
  var renderProject = (p) => {
363
3905
  const id = String(p._id ?? p.id ?? "???");
@@ -366,29 +3908,60 @@ var renderProject = (p) => {
366
3908
  return `${id} ${status.padEnd(10)} ${name}`;
367
3909
  };
368
3910
  var loginCommand = async (ctx) => {
369
- const pasted = typeof ctx.flags["api-key"] === "string" ? ctx.flags["api-key"] : void 0;
370
- if (!pasted) {
371
- ctx.writeErr("Paste an API key from https://exayard.com/settings/api-keys:");
372
- ctx.writeErr(" exayard login --api-key sk_live_...");
3911
+ if (ctx.flags["api-key"] === true) {
3912
+ ctx.writeErr("Usage: exayard login --api-key exa_live_... (or just `exayard login` to sign in in the browser)");
373
3913
  return 2;
374
3914
  }
3915
+ const pasted = typeof ctx.flags["api-key"] === "string" ? ctx.flags["api-key"] : void 0;
375
3916
  const orgId = typeof ctx.flags.org === "string" ? ctx.flags.org : void 0;
376
3917
  const baseUrl = (typeof ctx.flags["base-url"] === "string" ? ctx.flags["base-url"] : void 0) ?? readConfig().baseUrl;
3918
+ if (!pasted) return await browserLoginCommand(ctx, baseUrl, orgId);
377
3919
  writeConfig({ apiKey: pasted, organizationId: orgId, baseUrl });
378
- const exa = new import_sdk.Exayard({ apiKey: pasted, baseUrl });
3920
+ const exa = new import_sdk3.Exayard({ apiKey: pasted, baseUrl });
379
3921
  await exa.me.get();
380
3922
  ctx.write(ctx.json ? JSON.stringify({ ok: true }) : "Logged in. Token saved to ~/.exayard/config.json.");
381
3923
  return 0;
382
3924
  };
3925
+ var DEFAULT_BASE_URL = "https://api.exayard.com/v1";
3926
+ var browserLoginCommand = async (ctx, baseUrl, orgId) => {
3927
+ const ac = new AbortController();
3928
+ const onSigint = () => ac.abort();
3929
+ process.once("SIGINT", onSigint);
3930
+ let session;
3931
+ try {
3932
+ session = await browserLogin({ apiBaseUrl: baseUrl ?? DEFAULT_BASE_URL, writeErr: ctx.writeErr, openBrowser: ctx.openBrowser, signal: ac.signal });
3933
+ } catch (err) {
3934
+ if (!(err instanceof OAuthLoginError)) throw err;
3935
+ ctx.writeErr(err.message);
3936
+ return 1;
3937
+ } finally {
3938
+ process.off("SIGINT", onSigint);
3939
+ }
3940
+ const organizationId = orgId ?? readConfigFile().organizationId;
3941
+ writeConfig({ ...baseUrl ? { baseUrl } : {}, ...organizationId ? { organizationId } : {}, oauth: session });
3942
+ const exa = new import_sdk3.Exayard({ bearerToken: session.accessToken, baseUrl, fetch: sessionFetch(session, saveOAuthSession) });
3943
+ const me = await exa.me.get();
3944
+ const companies = me.memberships.map((m) => m.orgId);
3945
+ let saved = organizationId;
3946
+ if (!saved && companies.length === 1) {
3947
+ saved = companies[0];
3948
+ writeConfig({ ...readConfigFile(), organizationId: saved });
3949
+ }
3950
+ if (ctx.json) {
3951
+ ctx.write(JSON.stringify({ ok: true, organizationId: saved ?? null, memberships: me.memberships }));
3952
+ } else {
3953
+ ctx.write(`Logged in${saved ? ` (company ${saved})` : ""}. Saved to ~/.exayard/config.json.`);
3954
+ if (!saved && companies.length > 1) {
3955
+ ctx.writeErr("You belong to several companies. Pass --org <id> on calls, or save one with `exayard login --org <id>`:");
3956
+ for (const m of me.memberships) ctx.writeErr(` ${m.orgId}${m.role ? ` ${m.role}` : ""}`);
3957
+ }
3958
+ }
3959
+ if (process.env.EXAYARD_API_KEY) ctx.writeErr("EXAYARD_API_KEY is set; calls use that key, not this sign-in, while it is.");
3960
+ return 0;
3961
+ };
383
3962
  var projectsListCommand = async (ctx) => {
384
3963
  const exa = makeClient(ctx);
385
- const cfg = readConfig();
386
- const organizationId = (typeof ctx.flags.org === "string" ? ctx.flags.org : void 0) ?? cfg.organizationId;
387
- if (!organizationId) {
388
- ctx.writeErr("--org <organizationId> required (or set EXAYARD_ORG_ID).");
389
- return 2;
390
- }
391
- const res = await exa.projects.list({ organizationId });
3964
+ const res = await exa.projects.list({ organizationId: resolveOrgId(ctx) });
392
3965
  if (ctx.json) {
393
3966
  ctx.write(JSON.stringify(res, null, 2));
394
3967
  } else {
@@ -398,39 +3971,154 @@ var projectsListCommand = async (ctx) => {
398
3971
  }
399
3972
  return 0;
400
3973
  };
401
- var webhooksListenCommand = async (ctx) => {
402
- if (process.env.EXAYARD_EXPERIMENTAL !== "1") {
3974
+ var assessmentsListCommand = async (ctx) => {
3975
+ const projectId = typeof ctx.flags.project === "string" ? ctx.flags.project : ctx.positional[0];
3976
+ if (!projectId) {
3977
+ ctx.writeErr("Usage: exayard assessments list <project_id>");
3978
+ return 2;
3979
+ }
3980
+ const exa = makeClient(ctx);
3981
+ const res = await exa.assessments.list(projectId);
3982
+ ctx.write(JSON.stringify(res, null, 2));
3983
+ return 0;
3984
+ };
3985
+ var assessmentsRunCommand = async (ctx) => {
3986
+ const projectId = typeof ctx.flags.project === "string" ? ctx.flags.project : ctx.positional[0];
3987
+ if (!projectId) {
403
3988
  ctx.writeErr(
404
- [
405
- "exayard listen is not yet implemented.",
406
- "",
407
- "The tunnel implementation is still in progress. To preview the command",
408
- "surface, set EXAYARD_EXPERIMENTAL=1 \u2014 but note the tunnel itself will not",
409
- "forward events yet.",
410
- "",
411
- "For now, register a webhook endpoint pointing at a public tunnel",
412
- "(ngrok, cloudflared) and verify signatures locally with:",
413
- "",
414
- ' echo "$RAW_BODY" | exayard webhooks verify --signature "$EXAYARD_SIGNATURE" --secret whsec_...'
415
- ].join("\n")
3989
+ `Usage: exayard assessments run <project_id> --pages <id1,id2> --elements '[{"id","name","category","hexColor"}]' [--org <org_id>]`
416
3990
  );
417
3991
  return 2;
418
3992
  }
419
- const forwardTo = typeof ctx.flags["forward-to"] === "string" ? ctx.flags["forward-to"] : void 0;
420
- if (!forwardTo) {
421
- ctx.writeErr("Usage: exayard listen --forward-to http://localhost:3000/webhook [--secret whsec_...]");
3993
+ const organizationId = resolveOrgId(ctx);
3994
+ const pageIds = typeof ctx.flags.pages === "string" ? ctx.flags.pages.split(",").map((s) => s.trim()).filter(Boolean) : [];
3995
+ if (pageIds.length === 0) {
3996
+ ctx.writeErr("--pages <pageId1,pageId2> required.");
3997
+ return 2;
3998
+ }
3999
+ if (typeof ctx.flags.elements !== "string") {
4000
+ ctx.writeErr(`--elements '[{"id","name","category","hexColor"}]' required (what to detect).`);
4001
+ return 2;
4002
+ }
4003
+ const elements = JSON.parse(ctx.flags.elements);
4004
+ const exa = makeClient(ctx);
4005
+ const res = await exa.assessments.run(projectId, { organizationId, pageIds, elements });
4006
+ ctx.write(JSON.stringify(res, null, 2));
4007
+ return 0;
4008
+ };
4009
+ var filesUploadCommand = async (ctx) => {
4010
+ const path = typeof ctx.flags.file === "string" ? ctx.flags.file : ctx.positional[0];
4011
+ const projectId = typeof ctx.flags.project === "string" ? ctx.flags.project : void 0;
4012
+ if (!path || !projectId) {
4013
+ ctx.writeErr("Usage: exayard files upload <path> --project <projectId>");
422
4014
  return 2;
423
4015
  }
424
- ctx.writeErr(
425
- [
426
- `exayard listen \u2192 ${forwardTo}`,
427
- "",
428
- "[experimental] Tunnel implementation pending; command surface is stable but",
429
- "no events will be forwarded. Track progress at https://developers.exayard.com."
430
- ].join("\n")
4016
+ const { readFileSync: readFileSync2 } = await import("fs");
4017
+ const { basename, extname } = await import("path");
4018
+ const bytes = readFileSync2(path);
4019
+ const ext = extname(path).toLowerCase();
4020
+ const mimeType = ext === ".pdf" ? "application/pdf" : ext === ".png" ? "image/png" : ext === ".jpg" || ext === ".jpeg" ? "image/jpeg" : "application/octet-stream";
4021
+ const exa = makeClient(ctx);
4022
+ const res = await exa.files.upload({
4023
+ projectId,
4024
+ filename: basename(path),
4025
+ mimeType,
4026
+ bytes: new Uint8Array(bytes)
4027
+ });
4028
+ ctx.write(
4029
+ ctx.json ? JSON.stringify(res, null, 2) : `uploaded ${res.fileId} ${basename(path)} \u2014 extraction started; run 'exayard pages list ${projectId}' to watch`
431
4030
  );
432
4031
  return 0;
433
4032
  };
4033
+ var pagesListCommand = async (ctx) => {
4034
+ const projectId = typeof ctx.flags.project === "string" ? ctx.flags.project : ctx.positional[0];
4035
+ if (!projectId) {
4036
+ ctx.writeErr("Usage: exayard pages list <project_id> [--org <org_id>]");
4037
+ return 2;
4038
+ }
4039
+ const organizationId = resolveOrgId(ctx);
4040
+ const exa = makeClient(ctx);
4041
+ const res = await exa.pages.list(projectId, { organizationId });
4042
+ ctx.write(JSON.stringify(res, null, 2));
4043
+ return 0;
4044
+ };
4045
+ var assessmentsProposeCommand = async (ctx) => {
4046
+ const projectId = typeof ctx.flags.project === "string" ? ctx.flags.project : ctx.positional[0];
4047
+ if (!projectId) {
4048
+ ctx.writeErr('Usage: exayard assessments propose <project_id> --prompt "..." [--pages id1,id2] [--org <org_id>]');
4049
+ return 2;
4050
+ }
4051
+ const organizationId = resolveOrgId(ctx);
4052
+ const prompt = typeof ctx.flags.prompt === "string" ? ctx.flags.prompt : ctx.positional[1];
4053
+ if (!prompt) {
4054
+ ctx.writeErr('--prompt "<what to measure>" required.');
4055
+ return 2;
4056
+ }
4057
+ const pageIds = typeof ctx.flags.pages === "string" ? ctx.flags.pages.split(",").map((s) => s.trim()).filter(Boolean) : void 0;
4058
+ const exa = makeClient(ctx);
4059
+ const res = await exa.assessments.propose(projectId, { organizationId, prompt, pageIds });
4060
+ ctx.write(JSON.stringify(res, null, 2));
4061
+ return 0;
4062
+ };
4063
+ var assessmentsLatestCommand = async (ctx) => {
4064
+ const projectId = typeof ctx.flags.project === "string" ? ctx.flags.project : ctx.positional[0];
4065
+ if (!projectId) {
4066
+ ctx.writeErr("Usage: exayard assessments latest <project_id>");
4067
+ return 2;
4068
+ }
4069
+ const exa = makeClient(ctx);
4070
+ const res = await exa.assessments.latest(projectId);
4071
+ ctx.write(JSON.stringify(res, null, 2));
4072
+ return 0;
4073
+ };
4074
+ var assessmentsGetCommand = async (ctx) => {
4075
+ const id = typeof ctx.flags.id === "string" ? ctx.flags.id : ctx.positional[0];
4076
+ if (!id) {
4077
+ ctx.writeErr("Usage: exayard assessments get <assessment_id>");
4078
+ return 2;
4079
+ }
4080
+ const exa = makeClient(ctx);
4081
+ const res = await exa.assessments.get(id);
4082
+ ctx.write(JSON.stringify(res, null, 2));
4083
+ return 0;
4084
+ };
4085
+ var takeoffSummaryCommand = async (ctx) => {
4086
+ const projectId = typeof ctx.flags.project === "string" ? ctx.flags.project : ctx.positional[0];
4087
+ if (!projectId) {
4088
+ ctx.writeErr("Usage: exayard takeoff summary <project_id>");
4089
+ return 2;
4090
+ }
4091
+ const exa = makeClient(ctx);
4092
+ const res = await exa.assessments.takeoffSummary(projectId);
4093
+ ctx.write(JSON.stringify(res, null, 2));
4094
+ return 0;
4095
+ };
4096
+ var webhooksListenCommand = async (ctx) => {
4097
+ const forwardTo = typeof ctx.flags["forward-to"] === "string" ? ctx.flags["forward-to"] : void 0;
4098
+ if (ctx.flags["forward-to"] === true) {
4099
+ ctx.writeErr('Usage: exayard listen [--forward-to http://localhost:3000/webhooks] [--events "bid.won,project.created"]');
4100
+ return 2;
4101
+ }
4102
+ const events = typeof ctx.flags.events === "string" ? ctx.flags.events.split(",").map((s) => s.trim()).filter(Boolean) : void 0;
4103
+ const exa = makeClient(ctx);
4104
+ const ac = new AbortController();
4105
+ const onSigint = () => ac.abort();
4106
+ process.once("SIGINT", onSigint);
4107
+ try {
4108
+ return await runListen(exa, {
4109
+ forwardTo,
4110
+ events,
4111
+ organizationId: resolveOrgId(ctx),
4112
+ name: typeof ctx.flags.name === "string" ? ctx.flags.name : void 0,
4113
+ json: ctx.json,
4114
+ write: ctx.write,
4115
+ writeErr: ctx.writeErr,
4116
+ signal: ac.signal
4117
+ });
4118
+ } finally {
4119
+ process.off("SIGINT", onSigint);
4120
+ }
4121
+ };
434
4122
  var webhooksVerifyCommand = async (ctx) => {
435
4123
  const signature = typeof ctx.flags.signature === "string" ? ctx.flags.signature : void 0;
436
4124
  const secret = typeof ctx.flags.secret === "string" ? ctx.flags.secret : process.env.EXAYARD_WEBHOOK_SECRET;
@@ -440,11 +4128,11 @@ var webhooksVerifyCommand = async (ctx) => {
440
4128
  }
441
4129
  const body = await readAllStdin();
442
4130
  try {
443
- const event = await (0, import_sdk.constructWebhookEvent)(body, signature, secret);
4131
+ const event = await (0, import_sdk3.constructWebhookEvent)(body, signature, secret);
444
4132
  ctx.write(ctx.json ? JSON.stringify(event, null, 2) : `verified: ${event.type} (${event.id})`);
445
4133
  return 0;
446
4134
  } catch (err) {
447
- if (err instanceof import_sdk.WebhookSignatureError) {
4135
+ if (err instanceof import_sdk3.WebhookSignatureError) {
448
4136
  ctx.writeErr(`Signature invalid (${err.code}): ${err.message}`);
449
4137
  } else {
450
4138
  ctx.writeErr(`Verify failed: ${err.message}`);
@@ -479,35 +4167,27 @@ var helpSearchCommand = async (ctx) => {
479
4167
  };
480
4168
  var resolveOrgId = (ctx) => {
481
4169
  const fromFlag = typeof ctx.flags.org === "string" ? ctx.flags.org : void 0;
482
- return fromFlag ?? readConfig().organizationId ?? null;
4170
+ return fromFlag ?? readConfig().organizationId ?? void 0;
483
4171
  };
484
4172
  var projectsCreateCommand = async (ctx) => {
485
4173
  const name = typeof ctx.flags.name === "string" ? ctx.flags.name : ctx.positional[0];
486
4174
  if (!name) {
487
- ctx.writeErr('Usage: exayard projects create --name "<project name>" [--org org_...]');
4175
+ ctx.writeErr('Usage: exayard projects create --name "<project name>" [--org <org_id>]');
488
4176
  return 2;
489
4177
  }
490
4178
  const organizationId = resolveOrgId(ctx);
491
- if (!organizationId) {
492
- ctx.writeErr("--org <organizationId> required (or set EXAYARD_ORG_ID).");
493
- return 2;
494
- }
495
4179
  const exa = makeClient(ctx);
496
4180
  const res = await exa.projects.create({ organizationId, name });
497
- ctx.write(ctx.json ? JSON.stringify(res, null, 2) : `created ${res._id ?? res.id} ${name}`);
4181
+ ctx.write(ctx.json ? JSON.stringify(res, null, 2) : `created ${res.id} ${name}`);
498
4182
  return 0;
499
4183
  };
500
4184
  var projectsArchiveCommand = async (ctx) => {
501
4185
  const id = typeof ctx.flags.id === "string" ? ctx.flags.id : ctx.positional[0];
502
4186
  if (!id) {
503
- ctx.writeErr("Usage: exayard projects archive <project_id> [--org org_...]");
4187
+ ctx.writeErr("Usage: exayard projects archive <project_id> [--org <org_id>]");
504
4188
  return 2;
505
4189
  }
506
4190
  const organizationId = resolveOrgId(ctx);
507
- if (!organizationId) {
508
- ctx.writeErr("--org <organizationId> required (or set EXAYARD_ORG_ID).");
509
- return 2;
510
- }
511
4191
  const exa = makeClient(ctx);
512
4192
  await exa.projects.archive(id, { organizationId });
513
4193
  ctx.write(ctx.json ? JSON.stringify({ ok: true, archived: id }) : `archived ${id}`);
@@ -515,10 +4195,6 @@ var projectsArchiveCommand = async (ctx) => {
515
4195
  };
516
4196
  var webhooksListCommand = async (ctx) => {
517
4197
  const organizationId = resolveOrgId(ctx);
518
- if (!organizationId) {
519
- ctx.writeErr("--org <organizationId> required (or set EXAYARD_ORG_ID).");
520
- return 2;
521
- }
522
4198
  const exa = makeClient(ctx);
523
4199
  const res = await exa.webhooks.listEndpoints({ organizationId });
524
4200
  if (ctx.json) {
@@ -542,10 +4218,6 @@ var webhooksCreateCommand = async (ctx) => {
542
4218
  return 2;
543
4219
  }
544
4220
  const organizationId = resolveOrgId(ctx);
545
- if (!organizationId) {
546
- ctx.writeErr("--org <organizationId> required (or set EXAYARD_ORG_ID).");
547
- return 2;
548
- }
549
4221
  const events = eventsFlag.split(",").map((s) => s.trim()).filter(Boolean);
550
4222
  const exa = makeClient(ctx);
551
4223
  const res = await exa.webhooks.createEndpoint({ organizationId, url, events, description });
@@ -560,14 +4232,10 @@ var webhooksCreateCommand = async (ctx) => {
560
4232
  var webhooksDeleteCommand = async (ctx) => {
561
4233
  const id = typeof ctx.flags.id === "string" ? ctx.flags.id : ctx.positional[0];
562
4234
  if (!id) {
563
- ctx.writeErr("Usage: exayard webhooks delete <endpoint_id> [--org org_...]");
4235
+ ctx.writeErr("Usage: exayard webhooks delete <endpoint_id> [--org <org_id>]");
564
4236
  return 2;
565
4237
  }
566
4238
  const organizationId = resolveOrgId(ctx);
567
- if (!organizationId) {
568
- ctx.writeErr("--org <organizationId> required (or set EXAYARD_ORG_ID).");
569
- return 2;
570
- }
571
4239
  const exa = makeClient(ctx);
572
4240
  await exa.webhooks.deleteEndpoint(id, { organizationId });
573
4241
  ctx.write(ctx.json ? JSON.stringify({ ok: true, deleted: id }) : `deleted ${id}`);
@@ -595,6 +4263,14 @@ var COMMANDS = {
595
4263
  "projects:list": projectsListCommand,
596
4264
  "projects:create": projectsCreateCommand,
597
4265
  "projects:archive": projectsArchiveCommand,
4266
+ "files:upload": filesUploadCommand,
4267
+ "pages:list": pagesListCommand,
4268
+ "assessments:list": assessmentsListCommand,
4269
+ "assessments:latest": assessmentsLatestCommand,
4270
+ "assessments:get": assessmentsGetCommand,
4271
+ "assessments:propose": assessmentsProposeCommand,
4272
+ "assessments:run": assessmentsRunCommand,
4273
+ "takeoff:summary": takeoffSummaryCommand,
598
4274
  "webhooks:list": webhooksListCommand,
599
4275
  "webhooks:create": webhooksCreateCommand,
600
4276
  "webhooks:delete": webhooksDeleteCommand,