capsulemcp 1.6.0 → 1.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/http.js CHANGED
@@ -304,11 +304,22 @@ async function doFetch(url, options) {
304
304
  "Rate limit exceeded after one retry. Please slow down your requests."
305
305
  );
306
306
  }
307
- emitCapsuleRequest(method, url, retried.res, Date.now() - startedAt, true);
308
- return retried;
307
+ return { ...retried, startedAt, method, url, retriedAfter429: true };
308
+ }
309
+ return { ...first, startedAt, method, url, retriedAfter429: false };
310
+ }
311
+ async function consumeBody(start, body) {
312
+ try {
313
+ return await body();
314
+ } finally {
315
+ emitCapsuleRequest(
316
+ start.method,
317
+ start.url,
318
+ start.res,
319
+ Date.now() - start.startedAt,
320
+ start.retriedAfter429
321
+ );
309
322
  }
310
- emitCapsuleRequest(method, url, first.res, Date.now() - startedAt, false);
311
- return first;
312
323
  }
313
324
  function emitCapsuleRequest(method, url, res, durationMs, retriedAfter429) {
314
325
  let path = "";
@@ -358,13 +369,15 @@ function buildUrl(path, params) {
358
369
  async function capsuleGet(path, params) {
359
370
  const token = getToken();
360
371
  const url = buildUrl(path, params);
361
- const { res, cleanup } = await doFetch(url, { headers: baseHeaders(token) });
372
+ const start = await doFetch(url, { headers: baseHeaders(token) });
362
373
  try {
363
- const data = await handleResponse(res);
364
- const nextPage = parseNextPage(res.headers.get("Link"));
365
- return { data, nextPage };
374
+ return await consumeBody(start, async () => {
375
+ const data = await handleResponse(start.res);
376
+ const nextPage = parseNextPage(start.res.headers.get("Link"));
377
+ return { data, nextPage };
378
+ });
366
379
  } finally {
367
- cleanup();
380
+ start.cleanup();
368
381
  }
369
382
  }
370
383
  async function capsuleGetCached(path, params) {
@@ -399,123 +412,130 @@ async function capsulePost(path, body) {
399
412
  if (isReadOnly()) throw new CapsuleReadOnlyError("POST");
400
413
  const token = getToken();
401
414
  const url = buildUrl(path);
402
- const { res, cleanup } = await doFetch(url, {
415
+ const start = await doFetch(url, {
403
416
  method: "POST",
404
417
  headers: { ...baseHeaders(token), "Content-Type": "application/json" },
405
418
  body: JSON.stringify(body)
406
419
  });
407
420
  try {
408
- return await handleResponse(res);
421
+ return await consumeBody(start, () => handleResponse(start.res));
409
422
  } finally {
410
- cleanup();
423
+ start.cleanup();
411
424
  }
412
425
  }
413
426
  async function capsulePostNoContent(path) {
414
427
  if (isReadOnly()) throw new CapsuleReadOnlyError("POST");
415
428
  const token = getToken();
416
429
  const url = buildUrl(path);
417
- const { res, cleanup } = await doFetch(url, {
430
+ const start = await doFetch(url, {
418
431
  method: "POST",
419
432
  headers: baseHeaders(token)
420
433
  });
421
434
  try {
422
- if (res.status === 204) return;
423
- await throwForStatus(res);
424
- await mapAbort(res.text());
435
+ await consumeBody(start, async () => {
436
+ if (start.res.status === 204) return;
437
+ await throwForStatus(start.res);
438
+ await mapAbort(start.res.text());
439
+ });
425
440
  } finally {
426
- cleanup();
441
+ start.cleanup();
427
442
  }
428
443
  }
429
444
  async function capsuleSearch(path, body, params) {
430
445
  const token = getToken();
431
446
  const url = buildUrl(path, params);
432
- const { res, cleanup } = await doFetch(url, {
447
+ const start = await doFetch(url, {
433
448
  method: "POST",
434
449
  headers: { ...baseHeaders(token), "Content-Type": "application/json" },
435
450
  body: JSON.stringify(body)
436
451
  });
437
452
  try {
438
- const data = await handleResponse(res);
439
- const nextPage = parseNextPage(res.headers.get("Link"));
440
- return { data, nextPage };
453
+ return await consumeBody(start, async () => {
454
+ const data = await handleResponse(start.res);
455
+ const nextPage = parseNextPage(start.res.headers.get("Link"));
456
+ return { data, nextPage };
457
+ });
441
458
  } finally {
442
- cleanup();
459
+ start.cleanup();
443
460
  }
444
461
  }
445
462
  async function capsulePut(path, body) {
446
463
  if (isReadOnly()) throw new CapsuleReadOnlyError("PUT");
447
464
  const token = getToken();
448
465
  const url = buildUrl(path);
449
- const { res, cleanup } = await doFetch(url, {
466
+ const start = await doFetch(url, {
450
467
  method: "PUT",
451
468
  headers: { ...baseHeaders(token), "Content-Type": "application/json" },
452
469
  body: JSON.stringify(body)
453
470
  });
454
471
  try {
455
- return await handleResponse(res);
472
+ return await consumeBody(start, () => handleResponse(start.res));
456
473
  } finally {
457
- cleanup();
474
+ start.cleanup();
458
475
  }
459
476
  }
460
477
  async function capsuleGetBinary(path, maxBytes) {
461
478
  const token = getToken();
462
479
  const url = buildUrl(path);
463
- const { res, cleanup } = await doFetch(url, { headers: baseHeaders(token) });
480
+ const start = await doFetch(url, { headers: baseHeaders(token) });
464
481
  try {
465
- await throwForStatus(res);
466
- const contentType = res.headers.get("Content-Type") ?? "application/octet-stream";
467
- const declared = res.headers.get("Content-Length");
468
- const declaredBytes = declared ? Number(declared) : NaN;
469
- if (maxBytes !== void 0 && Number.isFinite(declaredBytes) && declaredBytes > maxBytes) {
470
- if (res.body) await res.body.cancel().catch(() => {
471
- });
472
- return {
473
- contentType,
474
- buffer: Buffer.alloc(0),
475
- truncated: true,
476
- sizeBytes: declaredBytes
477
- };
478
- }
479
- if (maxBytes !== void 0 && res.body) {
480
- const reader = res.body.getReader();
481
- const chunks = [];
482
- let total = 0;
483
- let truncated = false;
484
- while (true) {
485
- const { done, value } = await mapAbort(reader.read());
486
- if (done) break;
487
- total += value.byteLength;
488
- if (total > maxBytes) {
489
- truncated = true;
490
- await reader.cancel().catch(() => {
491
- });
492
- break;
493
- }
494
- chunks.push(value);
495
- }
496
- if (truncated) {
482
+ return await consumeBody(start, async () => {
483
+ const res = start.res;
484
+ await throwForStatus(res);
485
+ const contentType = res.headers.get("Content-Type") ?? "application/octet-stream";
486
+ const declared = res.headers.get("Content-Length");
487
+ const declaredBytes = declared ? Number(declared) : NaN;
488
+ if (maxBytes !== void 0 && Number.isFinite(declaredBytes) && declaredBytes > maxBytes) {
489
+ if (res.body) await res.body.cancel().catch(() => {
490
+ });
497
491
  return {
498
492
  contentType,
499
493
  buffer: Buffer.alloc(0),
500
494
  truncated: true,
501
- sizeBytes: total
495
+ sizeBytes: declaredBytes
502
496
  };
503
497
  }
504
- const buffer2 = Buffer.concat(chunks.map((c) => Buffer.from(c)));
505
- return { contentType, buffer: buffer2, sizeBytes: buffer2.length };
506
- }
507
- const arrayBuffer = await mapAbort(res.arrayBuffer());
508
- const buffer = Buffer.from(arrayBuffer);
509
- return { contentType, buffer, sizeBytes: buffer.length };
498
+ if (maxBytes !== void 0 && res.body) {
499
+ const reader = res.body.getReader();
500
+ const chunks = [];
501
+ let total = 0;
502
+ let truncated = false;
503
+ while (true) {
504
+ const { done, value } = await mapAbort(reader.read());
505
+ if (done) break;
506
+ total += value.byteLength;
507
+ if (total > maxBytes) {
508
+ truncated = true;
509
+ await reader.cancel().catch(() => {
510
+ });
511
+ break;
512
+ }
513
+ chunks.push(value);
514
+ }
515
+ if (truncated) {
516
+ return {
517
+ contentType,
518
+ buffer: Buffer.alloc(0),
519
+ truncated: true,
520
+ sizeBytes: total
521
+ };
522
+ }
523
+ const buffer2 = Buffer.concat(chunks.map((c) => Buffer.from(c)));
524
+ return { contentType, buffer: buffer2, sizeBytes: buffer2.length };
525
+ }
526
+ const arrayBuffer = await mapAbort(res.arrayBuffer());
527
+ const buffer = Buffer.from(arrayBuffer);
528
+ return { contentType, buffer, sizeBytes: buffer.length };
529
+ });
510
530
  } finally {
511
- cleanup();
531
+ start.cleanup();
512
532
  }
513
533
  }
514
534
  async function capsulePostBinary(path, body, contentType, filename) {
515
535
  if (isReadOnly()) throw new CapsuleReadOnlyError("POST");
516
536
  const token = getToken();
517
537
  const url = buildUrl(path);
518
- const { res, cleanup } = await doFetch(url, {
538
+ const start = await doFetch(url, {
519
539
  method: "POST",
520
540
  headers: {
521
541
  ...baseHeaders(token),
@@ -526,25 +546,27 @@ async function capsulePostBinary(path, body, contentType, filename) {
526
546
  body
527
547
  });
528
548
  try {
529
- return await handleResponse(res);
549
+ return await consumeBody(start, () => handleResponse(start.res));
530
550
  } finally {
531
- cleanup();
551
+ start.cleanup();
532
552
  }
533
553
  }
534
554
  async function capsuleDelete(path) {
535
555
  if (isReadOnly()) throw new CapsuleReadOnlyError("DELETE");
536
556
  const token = getToken();
537
557
  const url = buildUrl(path);
538
- const { res, cleanup } = await doFetch(url, {
558
+ const start = await doFetch(url, {
539
559
  method: "DELETE",
540
560
  headers: baseHeaders(token)
541
561
  });
542
562
  try {
543
- if (res.status === 204) return;
544
- await throwForStatus(res);
545
- await mapAbort(res.text());
563
+ await consumeBody(start, async () => {
564
+ if (start.res.status === 204) return;
565
+ await throwForStatus(start.res);
566
+ await mapAbort(start.res.text());
567
+ });
546
568
  } finally {
547
- cleanup();
569
+ start.cleanup();
548
570
  }
549
571
  }
550
572
 
@@ -1352,18 +1374,19 @@ function registerToolTask(server, name, description, schema, handler) {
1352
1374
  }
1353
1375
 
1354
1376
  // src/tools/parties.ts
1355
- import { z as z4 } from "zod";
1377
+ import { z as z7 } from "zod";
1356
1378
 
1357
- // src/tools/descriptions.ts
1358
- var EMBED_TAGS_FIELDS_DESCRIPTION = "Comma-separated embeds, e.g. 'tags,fields'";
1359
- var EMBED_ATTACHMENTS_PARTICIPANTS_DESCRIPTION = "Comma-separated embeds, e.g. 'attachments,participants'";
1379
+ // src/tools/body-helpers.ts
1380
+ function setRef(body, key, id) {
1381
+ if (id) body[key] = { id };
1382
+ }
1383
+ function setNullableRef(body, key, id) {
1384
+ if (id === null) body[key] = null;
1385
+ else if (id !== void 0) body[key] = { id };
1386
+ }
1360
1387
 
1361
- // src/tools/confirm-flag.ts
1388
+ // src/tools/define-batch.ts
1362
1389
  import { z as z2 } from "zod";
1363
- var CONFIRM_REQUIRED_MESSAGE = "confirm: true is required to perform this destructive operation (set the parameter explicitly to acknowledge the destructive intent)";
1364
- function confirmFlag() {
1365
- return z2.literal(true, { error: () => CONFIRM_REQUIRED_MESSAGE });
1366
- }
1367
1390
 
1368
1391
  // src/capsule/batch.ts
1369
1392
  function chunk(arr, size) {
@@ -1457,6 +1480,39 @@ function topFailureReasons(results, n) {
1457
1480
  return Array.from(counts.values()).sort((a, b) => b.count - a.count).slice(0, n);
1458
1481
  }
1459
1482
 
1483
+ // src/tools/define-batch.ts
1484
+ function defineBatch(args) {
1485
+ const schema = z2.object({
1486
+ items: z2.array(args.itemSchema).min(1).max(50).describe(args.itemDescription)
1487
+ });
1488
+ async function handler(input, opts = {}) {
1489
+ return batchExecute(args.toolName, input.items, args.itemHandler, opts);
1490
+ }
1491
+ return { schema, handler };
1492
+ }
1493
+
1494
+ // src/tools/descriptions.ts
1495
+ var EMBED_TAGS_FIELDS_DESCRIPTION = "Comma-separated embeds, e.g. 'tags,fields'";
1496
+ var EMBED_ATTACHMENTS_PARTICIPANTS_DESCRIPTION = "Comma-separated embeds, e.g. 'attachments,participants'";
1497
+
1498
+ // src/tools/define-delete.ts
1499
+ import { z as z5 } from "zod";
1500
+
1501
+ // src/tools/confirm-flag.ts
1502
+ import { z as z3 } from "zod";
1503
+ var CONFIRM_REQUIRED_MESSAGE = "confirm: true is required to perform this destructive operation (set the parameter explicitly to acknowledge the destructive intent)";
1504
+ function confirmFlag() {
1505
+ return z3.literal(true, { error: () => CONFIRM_REQUIRED_MESSAGE });
1506
+ }
1507
+
1508
+ // src/tools/shared-schemas.ts
1509
+ import { z as z4 } from "zod";
1510
+ var positiveId = z4.preprocess((input) => {
1511
+ if (typeof input !== "string") return input;
1512
+ const trimmed = input.trim();
1513
+ return /^\d+$/.test(trimmed) ? Number(trimmed) : input;
1514
+ }, z4.number().int().positive());
1515
+
1460
1516
  // src/capsule/idempotent.ts
1461
1517
  var isCapsule404 = (err) => err instanceof CapsuleApiError && err.status === 404;
1462
1518
  var isCapsuleTagNotFound = (err) => err instanceof CapsuleApiError && err.status === 422 && /tag not found/i.test(err.message);
@@ -1479,13 +1535,33 @@ async function idempotentWithResult(op, success, alreadyDone, isAlreadyDoneError
1479
1535
  }
1480
1536
  }
1481
1537
 
1538
+ // src/tools/define-delete.ts
1539
+ function defineDelete(args) {
1540
+ const { toolName, pathPrefix, confirmHint, idDescription } = args;
1541
+ const schema = z5.object({
1542
+ id: idDescription ? positiveId.describe(idDescription) : positiveId,
1543
+ confirm: confirmFlag().describe(confirmHint)
1544
+ });
1545
+ async function handler(input) {
1546
+ if (input.confirm !== true) {
1547
+ throw new Error(`${toolName} requires confirm: true`);
1548
+ }
1549
+ return idempotent(
1550
+ () => capsuleDelete(`${pathPrefix}/${input.id}`),
1551
+ () => ({ deleted: true, alreadyDeleted: false, id: input.id }),
1552
+ () => ({ deleted: true, alreadyDeleted: true, id: input.id })
1553
+ );
1554
+ }
1555
+ return { schema, handler };
1556
+ }
1557
+
1482
1558
  // src/tools/custom-field-helpers.ts
1483
- import { z as z3 } from "zod";
1484
- var CustomFieldWriteSchema = z3.object({
1485
- definitionId: z3.number().int().positive().describe(
1559
+ import { z as z6 } from "zod";
1560
+ var CustomFieldWriteSchema = z6.object({
1561
+ definitionId: positiveId.describe(
1486
1562
  "The custom-field definition id from list_custom_fields. Identifies which field on the entity to set."
1487
1563
  ),
1488
- value: z3.union([z3.string(), z3.number(), z3.boolean(), z3.null()]).describe(
1564
+ value: z6.union([z6.string(), z6.number(), z6.boolean(), z6.null()]).describe(
1489
1565
  "The new value. String for TEXT / DATE / LIST / LARGE_TEXT / LINK fields, number for NUMBER fields, boolean for BOOLEAN fields. Clearing: pass null for TEXT / NUMBER / DATE / LIST (Capsule removes the row). BOOLEAN does NOT accept null (Capsule returns 422 'invalid type for field'); use `value: false` instead. Note BOOLEAN fields are observably **two-state**: a row exists with `value: true`, or no row exists. Setting `value: false` removes the row entirely \u2014 readers should treat absent BOOLEAN rows as equivalent to false. Tri-state BOOLEAN semantics (true / false / unknown) are not achievable through Capsule's API. Audit-log noise: sending value=null on a field that's already empty/cleared is accepted by Capsule but still bumps the parent entity's `updatedAt`. Read the current value via embed='fields' first if `updatedAt` is being used as a 'last meaningful change' signal. NUMBER quirks: Capsule stores numerics correctly but the read-back via embed=fields returns them as STRINGS (e.g. value=3 reads as '3'); callers comparing values must coerce. TEXT quirks: value='' has the same observable effect as value=null (row removed); empty-string and never-set are indistinguishable."
1490
1566
  )
1491
1567
  });
@@ -1501,24 +1577,24 @@ function mapFieldsForBody(fields) {
1501
1577
  }
1502
1578
 
1503
1579
  // src/tools/parties.ts
1504
- var EmailAddressSchema = z4.object({
1505
- address: z4.string().email(),
1506
- type: z4.string().optional()
1580
+ var EmailAddressSchema = z7.object({
1581
+ address: z7.string().email(),
1582
+ type: z7.string().optional()
1507
1583
  });
1508
- var PhoneNumberSchema = z4.object({
1584
+ var PhoneNumberSchema = z7.object({
1509
1585
  // Capsule rejects empty strings with `phoneNumber.number: number is
1510
1586
  // required`. Enforce at the schema layer to catch typos pre-call,
1511
1587
  // matching how EmailAddressSchema's address field behaves.
1512
- number: z4.string().min(1),
1513
- type: z4.string().optional()
1588
+ number: z7.string().min(1),
1589
+ type: z7.string().optional()
1514
1590
  });
1515
1591
  var CountryDescription = "Country name. Capsule validates this against a small canonical-English-name dictionary; inputs not in the dictionary are REJECTED with 422 'address.country: unknown country' (NOT silently passed through or normalised). Probed examples \u2014 accepted: `United States`, `United Kingdom`, `Czechia`, `Germany`. Aliased: `USA \u2192 United States`. Rejected: `United States of America`, `Czech Republic` (use `Czechia`), `UK`/`Britain` (use `United Kingdom`), `Deutschland` (use `Germany`). Empty string is accepted and stored as `null` \u2014 a de-facto 'clear' shape. To discover an accepted name, read an existing party that already has the country set.";
1516
- var AddressSchema = z4.object({
1517
- street: z4.string().optional(),
1518
- city: z4.string().optional(),
1519
- state: z4.string().optional(),
1520
- country: z4.string().optional().describe(CountryDescription),
1521
- zip: z4.string().optional()
1592
+ var AddressSchema = z7.object({
1593
+ street: z7.string().optional(),
1594
+ city: z7.string().optional(),
1595
+ state: z7.string().optional(),
1596
+ country: z7.string().optional().describe(CountryDescription),
1597
+ zip: z7.string().optional()
1522
1598
  });
1523
1599
  function validateWebsiteAddress(data, ctx) {
1524
1600
  const isUrlService = data.service === void 0 || data.service === "URL";
@@ -1541,7 +1617,7 @@ function validateWebsiteAddress(data, ctx) {
1541
1617
  });
1542
1618
  }
1543
1619
  }
1544
- var WebsiteServiceEnum = z4.enum([
1620
+ var WebsiteServiceEnum = z7.enum([
1545
1621
  "URL",
1546
1622
  "SKYPE",
1547
1623
  "TWITTER",
@@ -1560,19 +1636,19 @@ var WebsiteServiceEnum = z4.enum([
1560
1636
  "BLUESKY",
1561
1637
  "SNAPCHAT"
1562
1638
  ]);
1563
- var WebsiteSchema = z4.object({
1564
- address: z4.string().min(1).describe(
1639
+ var WebsiteSchema = z7.object({
1640
+ address: z7.string().min(1).describe(
1565
1641
  "The website address. A URL when service='URL', or a handle (e.g. '@acmeco') for social services like 'TWITTER', 'INSTAGRAM'. Capsule names this field `address` regardless of service type."
1566
1642
  ),
1567
1643
  service: WebsiteServiceEnum.optional().describe(
1568
1644
  "Service type. One of: URL, SKYPE, TWITTER, LINKED_IN, FACEBOOK, XING, FEED, GOOGLE_PLUS, FLICKR, GITHUB, YOUTUBE, INSTAGRAM, PINTEREST, TIKTOK, THREADS, BLUESKY, SNAPCHAT. Defaults to 'URL' if omitted."
1569
1645
  )
1570
1646
  }).superRefine(validateWebsiteAddress);
1571
- var searchPartiesSchema = z4.object({
1572
- q: z4.string().optional().describe("Free-text search query"),
1573
- embed: z4.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
1574
- page: z4.number().int().positive().optional().default(1),
1575
- perPage: z4.number().int().min(1).max(100).optional().default(25)
1647
+ var searchPartiesSchema = z7.object({
1648
+ q: z7.string().optional().describe("Free-text search query"),
1649
+ embed: z7.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
1650
+ page: z7.number().int().positive().optional().default(1),
1651
+ perPage: z7.number().int().min(1).max(100).optional().default(25)
1576
1652
  });
1577
1653
  async function searchParties(input) {
1578
1654
  const path = input.q ? "/parties/search" : "/parties";
@@ -1584,9 +1660,9 @@ async function searchParties(input) {
1584
1660
  });
1585
1661
  return { ...data, nextPage };
1586
1662
  }
1587
- var getPartySchema = z4.object({
1588
- id: z4.number().int().positive().describe("Party ID"),
1589
- embed: z4.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
1663
+ var getPartySchema = z7.object({
1664
+ id: positiveId.describe("Party ID"),
1665
+ embed: z7.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
1590
1666
  });
1591
1667
  async function getParty(input) {
1592
1668
  const { data } = await capsuleGet(`/parties/${input.id}`, {
@@ -1594,11 +1670,11 @@ async function getParty(input) {
1594
1670
  });
1595
1671
  return data;
1596
1672
  }
1597
- var getPartiesSchema = z4.object({
1598
- ids: z4.array(z4.number().int().positive()).min(1).max(50).describe(
1673
+ var getPartiesSchema = z7.object({
1674
+ ids: z7.array(positiveId).min(1).max(50).describe(
1599
1675
  "Array of party IDs (1\u201350). Capsule's native batch-fetch endpoint caps at 10 per request; the connector transparently splits larger sets into 10-id chunks and fans out the Capsule calls in parallel. Result shape is identical regardless of input size."
1600
1676
  ),
1601
- embed: z4.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
1677
+ embed: z7.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
1602
1678
  });
1603
1679
  async function getParties(input) {
1604
1680
  const { ids, embed } = input;
@@ -1616,10 +1692,10 @@ async function getParties(input) {
1616
1692
  );
1617
1693
  return { parties: responses.flatMap((r) => r.data.parties) };
1618
1694
  }
1619
- var listPartyOpportunitiesSchema = z4.object({
1620
- partyId: z4.number().int().positive(),
1621
- page: z4.number().int().positive().optional().default(1),
1622
- perPage: z4.number().int().min(1).max(100).optional().default(25)
1695
+ var listPartyOpportunitiesSchema = z7.object({
1696
+ partyId: positiveId,
1697
+ page: z7.number().int().positive().optional().default(1),
1698
+ perPage: z7.number().int().min(1).max(100).optional().default(25)
1623
1699
  });
1624
1700
  async function listPartyOpportunities(input) {
1625
1701
  const { data, nextPage } = await capsuleGet(
@@ -1628,10 +1704,10 @@ async function listPartyOpportunities(input) {
1628
1704
  );
1629
1705
  return { ...data, nextPage };
1630
1706
  }
1631
- var listPartyProjectsSchema = z4.object({
1632
- partyId: z4.number().int().positive(),
1633
- page: z4.number().int().positive().optional().default(1),
1634
- perPage: z4.number().int().min(1).max(100).optional().default(25)
1707
+ var listPartyProjectsSchema = z7.object({
1708
+ partyId: positiveId,
1709
+ page: z7.number().int().positive().optional().default(1),
1710
+ perPage: z7.number().int().min(1).max(100).optional().default(25)
1635
1711
  });
1636
1712
  async function listPartyProjects(input) {
1637
1713
  const { data, nextPage } = await capsuleGet(
@@ -1641,50 +1717,50 @@ async function listPartyProjects(input) {
1641
1717
  return { ...data, nextPage };
1642
1718
  }
1643
1719
  var PartyWriteBaseSchema = {
1644
- about: z4.string().optional(),
1645
- emailAddresses: z4.array(EmailAddressSchema).optional().describe(
1720
+ about: z7.string().optional(),
1721
+ emailAddresses: z7.array(EmailAddressSchema).optional().describe(
1646
1722
  "APPEND-ONLY: items are merged into the existing list, never replaced. For atomic add/remove/replace use add_party_email_address and remove_party_email_address_by_id. Passing `[]` here is a silent no-op (does not clear the list and does not advance updatedAt)."
1647
1723
  ),
1648
- phoneNumbers: z4.array(PhoneNumberSchema).optional().describe(
1724
+ phoneNumbers: z7.array(PhoneNumberSchema).optional().describe(
1649
1725
  "APPEND-ONLY: items are merged into the existing list, never replaced. For atomic add/remove/replace use add_party_phone_number and remove_party_phone_number_by_id."
1650
1726
  ),
1651
- addresses: z4.array(AddressSchema).optional().describe(
1727
+ addresses: z7.array(AddressSchema).optional().describe(
1652
1728
  "APPEND-ONLY: items are merged into the existing list, never replaced. For atomic add/remove/replace use add_party_address and remove_party_address_by_id. The `country` field is mapped through Capsule's country dictionary \u2014 see `add_party_address.country` for the dictionary edges (small canonical-English-name list; inputs not in the dictionary are REJECTED with 422, not silently dropped)."
1653
1729
  ),
1654
- websites: z4.array(WebsiteSchema).optional().describe(
1730
+ websites: z7.array(WebsiteSchema).optional().describe(
1655
1731
  "APPEND-ONLY: items are merged into the existing list, never replaced. For atomic add/remove/replace use add_party_website and remove_party_website_by_id."
1656
1732
  ),
1657
- ownerId: z4.number().int().positive().optional().describe(
1733
+ ownerId: positiveId.optional().describe(
1658
1734
  "Assign to user ID. On create_party, defaults to the API-token owner when omitted. Once set, this connector cannot clear the owner back to null \u2014 use Capsule's web UI for that. Discover IDs via list_users."
1659
1735
  )
1660
1736
  };
1661
- var createPartySchema = z4.object({
1662
- type: z4.enum(["person", "organisation"]),
1737
+ var createPartySchema = z7.object({
1738
+ type: z7.enum(["person", "organisation"]),
1663
1739
  // person
1664
- firstName: z4.string().optional(),
1665
- lastName: z4.string().optional(),
1666
- title: z4.string().optional(),
1667
- jobTitle: z4.string().optional(),
1668
- organisationId: z4.number().int().positive().optional().describe("Link person to an existing organisation ID"),
1740
+ firstName: z7.string().optional(),
1741
+ lastName: z7.string().optional(),
1742
+ title: z7.string().optional(),
1743
+ jobTitle: z7.string().optional(),
1744
+ organisationId: positiveId.optional().describe("Link person to an existing organisation ID"),
1669
1745
  // organisation
1670
- name: z4.string().optional(),
1746
+ name: z7.string().optional(),
1671
1747
  ...PartyWriteBaseSchema
1672
1748
  });
1673
1749
  async function createParty(input) {
1674
1750
  const { ownerId, organisationId, ...rest } = input;
1675
1751
  const body = { ...rest };
1676
- if (ownerId) body["owner"] = { id: ownerId };
1677
- if (organisationId) body["organisation"] = { id: organisationId };
1752
+ setRef(body, "owner", ownerId);
1753
+ setRef(body, "organisation", organisationId);
1678
1754
  return capsulePost("/parties", { party: body });
1679
1755
  }
1680
- var updatePartySchema = z4.object({
1681
- id: z4.number().int().positive(),
1682
- firstName: z4.string().optional(),
1683
- lastName: z4.string().optional(),
1684
- title: z4.string().optional(),
1685
- jobTitle: z4.string().optional(),
1686
- name: z4.string().optional(),
1687
- fields: z4.array(CustomFieldWriteSchema).optional().describe(fieldsArrayDescriptor("get_party")),
1756
+ var updatePartySchema = z7.object({
1757
+ id: positiveId,
1758
+ firstName: z7.string().optional(),
1759
+ lastName: z7.string().optional(),
1760
+ title: z7.string().optional(),
1761
+ jobTitle: z7.string().optional(),
1762
+ name: z7.string().optional(),
1763
+ fields: z7.array(CustomFieldWriteSchema).optional().describe(fieldsArrayDescriptor("get_party")),
1688
1764
  ...PartyWriteBaseSchema
1689
1765
  });
1690
1766
  async function updateParty(input) {
@@ -1693,39 +1769,26 @@ async function updateParty(input) {
1693
1769
  for (const [k, v] of Object.entries(rest)) {
1694
1770
  if (v !== void 0) body[k] = v;
1695
1771
  }
1696
- if (ownerId) body["owner"] = { id: ownerId };
1772
+ setRef(body, "owner", ownerId);
1697
1773
  const mappedFields = mapFieldsForBody(fields);
1698
1774
  if (mappedFields !== void 0) body["fields"] = mappedFields;
1699
1775
  return capsulePut(`/parties/${id}`, { party: body });
1700
1776
  }
1701
- var batchUpdatePartySchema = z4.object({
1702
- items: z4.array(updatePartySchema).min(1).max(50).describe(
1703
- "Array of 1\u201350 update_party inputs. Each item is the same shape as a single update_party call \u2014 id is required, every other field is optional. Capped at 50 so a single tool call can't burn an outsized share of Capsule's hourly per-token rate budget (~4000 req/h)."
1704
- )
1777
+ var { schema: batchUpdatePartySchema, handler: batchUpdateParty } = defineBatch({
1778
+ toolName: "batch_update_party",
1779
+ itemSchema: updatePartySchema,
1780
+ itemDescription: "Array of 1\u201350 update_party inputs. Each item is the same shape as a single update_party call \u2014 id is required, every other field is optional. Capped at 50 so a single tool call can't burn an outsized share of Capsule's hourly per-token rate budget (~4000 req/h).",
1781
+ itemHandler: updateParty
1705
1782
  });
1706
- async function batchUpdateParty(input, opts = {}) {
1707
- return batchExecute("batch_update_party", input.items, (item) => updateParty(item), opts);
1708
- }
1709
- var deletePartySchema = z4.object({
1710
- id: z4.number().int().positive(),
1711
- confirm: confirmFlag().describe(
1712
- "Must be set to true. Deletes the party AND all linked notes, tasks, opportunities, and projects (kases). Deleting an ORGANISATION does NOT delete people linked to it via organisationId \u2014 their `organisation` field is silently cleared to null and they survive as standalone records. Irreversible."
1713
- )
1783
+ var { schema: deletePartySchema, handler: deleteParty } = defineDelete({
1784
+ toolName: "delete_party",
1785
+ pathPrefix: "/parties",
1786
+ confirmHint: "Must be set to true. Deletes the party AND all linked notes, tasks, opportunities, and projects (kases). Deleting an ORGANISATION does NOT delete people linked to it via organisationId \u2014 their `organisation` field is silently cleared to null and they survive as standalone records. Irreversible."
1714
1787
  });
1715
- async function deleteParty(input) {
1716
- if (input.confirm !== true) {
1717
- throw new Error("delete_party requires confirm: true");
1718
- }
1719
- return idempotent(
1720
- () => capsuleDelete(`/parties/${input.id}`),
1721
- () => ({ deleted: true, alreadyDeleted: false, id: input.id }),
1722
- () => ({ deleted: true, alreadyDeleted: true, id: input.id })
1723
- );
1724
- }
1725
- var addPartyEmailAddressSchema = z4.object({
1726
- partyId: z4.number().int().positive(),
1727
- address: z4.string().email(),
1728
- type: z4.string().optional().describe("Free-form label, e.g. 'Work', 'Home'.")
1788
+ var addPartyEmailAddressSchema = z7.object({
1789
+ partyId: positiveId,
1790
+ address: z7.string().email(),
1791
+ type: z7.string().optional().describe("Free-form label, e.g. 'Work', 'Home'.")
1729
1792
  });
1730
1793
  async function addPartyEmailAddress(input) {
1731
1794
  const { partyId, address, type } = input;
@@ -1735,9 +1798,9 @@ async function addPartyEmailAddress(input) {
1735
1798
  party: { emailAddresses: [item] }
1736
1799
  });
1737
1800
  }
1738
- var removePartyEmailAddressByIdSchema = z4.object({
1739
- partyId: z4.number().int().positive(),
1740
- emailAddressId: z4.number().int().positive().describe(
1801
+ var removePartyEmailAddressByIdSchema = z7.object({
1802
+ partyId: positiveId,
1803
+ emailAddressId: positiveId.describe(
1741
1804
  "Capsule's id for the email-address row. Read it from get_party (each entry in emailAddresses carries an id)."
1742
1805
  )
1743
1806
  });
@@ -1757,10 +1820,10 @@ async function removePartyEmailAddressById(input) {
1757
1820
  () => ({ removed: true, alreadyRemoved: true, partyId, emailAddressId })
1758
1821
  );
1759
1822
  }
1760
- var addPartyPhoneNumberSchema = z4.object({
1761
- partyId: z4.number().int().positive(),
1762
- number: z4.string().min(1),
1763
- type: z4.string().optional().describe("Free-form label, e.g. 'Work', 'Mobile'.")
1823
+ var addPartyPhoneNumberSchema = z7.object({
1824
+ partyId: positiveId,
1825
+ number: z7.string().min(1),
1826
+ type: z7.string().optional().describe("Free-form label, e.g. 'Work', 'Mobile'.")
1764
1827
  });
1765
1828
  async function addPartyPhoneNumber(input) {
1766
1829
  const { partyId, number, type } = input;
@@ -1770,9 +1833,9 @@ async function addPartyPhoneNumber(input) {
1770
1833
  party: { phoneNumbers: [item] }
1771
1834
  });
1772
1835
  }
1773
- var removePartyPhoneNumberByIdSchema = z4.object({
1774
- partyId: z4.number().int().positive(),
1775
- phoneNumberId: z4.number().int().positive().describe(
1836
+ var removePartyPhoneNumberByIdSchema = z7.object({
1837
+ partyId: positiveId,
1838
+ phoneNumberId: positiveId.describe(
1776
1839
  "Capsule's id for the phone-number row. Read it from get_party (each entry in phoneNumbers carries an id)."
1777
1840
  )
1778
1841
  });
@@ -1792,14 +1855,14 @@ async function removePartyPhoneNumberById(input) {
1792
1855
  () => ({ removed: true, alreadyRemoved: true, partyId, phoneNumberId })
1793
1856
  );
1794
1857
  }
1795
- var addPartyAddressSchema = z4.object({
1796
- partyId: z4.number().int().positive(),
1797
- street: z4.string().optional(),
1798
- city: z4.string().optional(),
1799
- state: z4.string().optional(),
1800
- country: z4.string().optional().describe(CountryDescription),
1801
- zip: z4.string().optional(),
1802
- type: z4.string().optional().describe("Free-form label, e.g. 'Office', 'Home'.")
1858
+ var addPartyAddressSchema = z7.object({
1859
+ partyId: positiveId,
1860
+ street: z7.string().optional(),
1861
+ city: z7.string().optional(),
1862
+ state: z7.string().optional(),
1863
+ country: z7.string().optional().describe(CountryDescription),
1864
+ zip: z7.string().optional(),
1865
+ type: z7.string().optional().describe("Free-form label, e.g. 'Office', 'Home'.")
1803
1866
  });
1804
1867
  async function addPartyAddress(input) {
1805
1868
  const { partyId, ...rest } = input;
@@ -1811,9 +1874,9 @@ async function addPartyAddress(input) {
1811
1874
  party: { addresses: [item] }
1812
1875
  });
1813
1876
  }
1814
- var removePartyAddressByIdSchema = z4.object({
1815
- partyId: z4.number().int().positive(),
1816
- addressId: z4.number().int().positive().describe(
1877
+ var removePartyAddressByIdSchema = z7.object({
1878
+ partyId: positiveId,
1879
+ addressId: positiveId.describe(
1817
1880
  "Capsule's id for the address row. Read it from get_party (each entry in addresses carries an id)."
1818
1881
  )
1819
1882
  });
@@ -1833,9 +1896,9 @@ async function removePartyAddressById(input) {
1833
1896
  () => ({ removed: true, alreadyRemoved: true, partyId, addressId })
1834
1897
  );
1835
1898
  }
1836
- var addPartyWebsiteSchema = z4.object({
1837
- partyId: z4.number().int().positive(),
1838
- address: z4.string().min(1).describe(
1899
+ var addPartyWebsiteSchema = z7.object({
1900
+ partyId: positiveId,
1901
+ address: z7.string().min(1).describe(
1839
1902
  "The website address. A URL when service='URL', or a handle (e.g. '@acmeco') for social services."
1840
1903
  ),
1841
1904
  service: WebsiteServiceEnum.optional().describe("Defaults to 'URL' if omitted.")
@@ -1848,9 +1911,9 @@ async function addPartyWebsite(input) {
1848
1911
  party: { websites: [item] }
1849
1912
  });
1850
1913
  }
1851
- var removePartyWebsiteByIdSchema = z4.object({
1852
- partyId: z4.number().int().positive(),
1853
- websiteId: z4.number().int().positive().describe(
1914
+ var removePartyWebsiteByIdSchema = z7.object({
1915
+ partyId: positiveId,
1916
+ websiteId: positiveId.describe(
1854
1917
  "Capsule's id for the website row. Read it from get_party (each entry in websites carries an id)."
1855
1918
  )
1856
1919
  });
@@ -1872,20 +1935,32 @@ async function removePartyWebsiteById(input) {
1872
1935
  }
1873
1936
 
1874
1937
  // src/tools/opportunities.ts
1875
- import { z as z5 } from "zod";
1876
- var OpportunityValueSchema = z5.object({
1877
- amount: z5.number().nonnegative(),
1878
- currency: z5.string({
1938
+ import { z as z8 } from "zod";
1939
+
1940
+ // src/tools/preserve-refs.ts
1941
+ async function readEntityRefs(path, responseKey) {
1942
+ const { data } = await capsuleGet(path);
1943
+ const entity = data[responseKey];
1944
+ return {
1945
+ teamId: entity?.team?.id ?? void 0,
1946
+ stageId: entity?.stage?.id ?? void 0
1947
+ };
1948
+ }
1949
+
1950
+ // src/tools/opportunities.ts
1951
+ var OpportunityValueSchema = z8.object({
1952
+ amount: z8.number().nonnegative(),
1953
+ currency: z8.string({
1879
1954
  error: (iss) => iss.code === "invalid_type" && iss.input === void 0 ? "currency is required when amount is set (3-letter ISO 4217 code, e.g. 'USD', 'EUR', 'GBP')" : void 0
1880
1955
  }).length(3).describe(
1881
1956
  "ISO 4217 currency code (3 letters), e.g. 'GBP', 'USD', 'EUR'. Required when amount is set."
1882
1957
  )
1883
1958
  });
1884
- var searchOpportunitiesSchema = z5.object({
1885
- q: z5.string().optional().describe("Free-text search query"),
1886
- embed: z5.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
1887
- page: z5.number().int().positive().optional().default(1),
1888
- perPage: z5.number().int().min(1).max(100).optional().default(25)
1959
+ var searchOpportunitiesSchema = z8.object({
1960
+ q: z8.string().optional().describe("Free-text search query"),
1961
+ embed: z8.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
1962
+ page: z8.number().int().positive().optional().default(1),
1963
+ perPage: z8.number().int().min(1).max(100).optional().default(25)
1889
1964
  });
1890
1965
  async function searchOpportunities(input) {
1891
1966
  const path = input.q ? "/opportunities/search" : "/opportunities";
@@ -1897,9 +1972,9 @@ async function searchOpportunities(input) {
1897
1972
  });
1898
1973
  return { ...data, nextPage };
1899
1974
  }
1900
- var getOpportunitySchema = z5.object({
1901
- id: z5.number().int().positive(),
1902
- embed: z5.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
1975
+ var getOpportunitySchema = z8.object({
1976
+ id: positiveId,
1977
+ embed: z8.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
1903
1978
  });
1904
1979
  async function getOpportunity(input) {
1905
1980
  const { data } = await capsuleGet(`/opportunities/${input.id}`, {
@@ -1907,11 +1982,11 @@ async function getOpportunity(input) {
1907
1982
  });
1908
1983
  return data;
1909
1984
  }
1910
- var getOpportunitiesSchema = z5.object({
1911
- ids: z5.array(z5.number().int().positive()).min(1).max(50).describe(
1985
+ var getOpportunitiesSchema = z8.object({
1986
+ ids: z8.array(positiveId).min(1).max(50).describe(
1912
1987
  "Array of opportunity IDs (1\u201350). Capsule's native batch-fetch endpoint caps at 10 per request; the connector transparently splits larger sets into 10-id chunks and fans out the Capsule calls in parallel."
1913
1988
  ),
1914
- embed: z5.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
1989
+ embed: z8.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
1915
1990
  });
1916
1991
  async function getOpportunities(input) {
1917
1992
  const { ids, embed } = input;
@@ -1932,102 +2007,96 @@ async function getOpportunities(input) {
1932
2007
  );
1933
2008
  return { opportunities: responses.flatMap((r) => r.data.opportunities) };
1934
2009
  }
1935
- var createOpportunitySchema = z5.object({
1936
- name: z5.string().min(1),
1937
- partyId: z5.number().int().positive().describe("ID of the party this opportunity belongs to"),
1938
- milestoneId: z5.number().int().positive().describe(
1939
- "ID of the pipeline milestone to place this opportunity at. The milestone implicitly determines the pipeline \u2014 there is no separate pipelineId parameter. Discover via list_pipelines / list_milestones."
2010
+ var createOpportunitySchema = z8.object({
2011
+ name: z8.string().min(1),
2012
+ partyId: positiveId.describe("ID of the party this opportunity belongs to"),
2013
+ milestoneId: positiveId.describe(
2014
+ "ID of the pipeline milestone to place this opportunity at. The milestone implicitly determines the pipeline \u2014 there is no separate pipelineId parameter. Discover via list_pipelines / list_milestones. NOTE: some Capsule tenants configure **pipeline / milestone-reached automation rules** that mutate `owner` and/or `team` immediately after creation \u2014 e.g. an 'Assign to a Team' action that fires on entry to a specific milestone and has been observed to clear `owner` as an automation side-effect. If you observe a newly-created opp landing with `owner: null` despite passing `ownerId`, the cause is almost certainly a milestone automation on the destination pipeline rather than the connector. Documented workaround: follow `create_opportunity` with an immediate `batch_update_opportunity({items: [{id, ownerId, teamId}]})` carrying both fields \u2014 PUT does not re-fire milestone-reached triggers, so the owner sticks."
1940
2015
  ),
1941
- description: z5.string().optional(),
2016
+ description: z8.string().optional(),
1942
2017
  value: OpportunityValueSchema.optional(),
1943
- expectedCloseOn: z5.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("YYYY-MM-DD"),
1944
- probability: z5.number().int().min(0).max(100).optional(),
1945
- ownerId: z5.number().int().positive().optional().describe(
1946
- "Assign to user ID. Defaults to the API-token owner when omitted \u2014 note that opportunities do NOT inherit owner from the linked party, even though one might expect it. Once set, this connector cannot clear the owner back to null (use Capsule's web UI). Discover IDs via list_users."
2018
+ expectedCloseOn: z8.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("YYYY-MM-DD"),
2019
+ probability: z8.number().int().min(0).max(100).optional(),
2020
+ ownerId: positiveId.optional().describe(
2021
+ "Assign to user ID. Defaults to the API-token owner when omitted \u2014 note that opportunities do NOT inherit owner from the linked party, even though one might expect it. Once set, this connector cannot clear the owner back to null (use Capsule's web UI). Discover IDs via list_users. WARNING: tenant pipeline / milestone-reached automation can mutate this field post-create \u2014 see the `milestoneId` description for details and the chained-PUT workaround."
2022
+ ),
2023
+ teamId: positiveId.optional().describe(
2024
+ "Assign to team ID (discover via list_teams). Independent from `ownerId` \u2014 setting one does NOT clear the other on create. Three ownership shapes are valid: owner alone, team alone, or owner+team (the owner must be a member of the team; users can belong to multiple teams \u2014 422 'owner is not a member of the team' otherwise)."
1947
2025
  )
1948
2026
  });
1949
2027
  async function createOpportunity(input) {
1950
- const { partyId, milestoneId, ownerId, ...rest } = input;
2028
+ const { partyId, milestoneId, ownerId, teamId, ...rest } = input;
1951
2029
  const body = {
1952
2030
  ...rest,
1953
2031
  party: { id: partyId },
1954
2032
  milestone: { id: milestoneId }
1955
2033
  };
1956
- if (ownerId) body["owner"] = { id: ownerId };
2034
+ setRef(body, "owner", ownerId);
2035
+ setRef(body, "team", teamId);
1957
2036
  return capsulePost("/opportunities", { opportunity: body });
1958
2037
  }
1959
- var updateOpportunitySchema = z5.object({
1960
- id: z5.number().int().positive(),
1961
- name: z5.string().min(1).optional(),
1962
- milestoneId: z5.number().int().positive().optional().describe(
1963
- "Move the opportunity to this milestone. Side effects depend on the target: closing milestones (Won/Lost) auto-set `closedOn` to today and `probability` to the milestone default (100/0), preserving `lastOpenMilestone` as the previous open stage; moving back to an open milestone clears `closedOn` and re-applies the milestone's default probability (Won/Lost is reversible \u2014 no separate reopen tool). WARNING: Capsule does NOT validate that the new milestone belongs to the opportunity's current pipeline. Passing a milestoneId from a different pipeline silently relocates the opportunity across pipelines, and `lastOpenMilestone` may then reference a milestone in the previous pipeline. Verify against the opportunity's current pipeline (read the opp first, list its pipeline's milestones via list_milestones) before passing a cross-pipeline id."
2038
+ var updateOpportunitySchema = z8.object({
2039
+ id: positiveId,
2040
+ name: z8.string().min(1).optional(),
2041
+ milestoneId: positiveId.optional().describe(
2042
+ "Move the opportunity to this milestone. Side effects depend on the target: closing milestones (Won/Lost) auto-set `closedOn` to today and `probability` to the milestone default (100/0), preserving `lastOpenMilestone` as the previous open stage; moving back to an open milestone clears `closedOn` and re-applies the milestone's default probability (Won/Lost is reversible \u2014 no separate reopen tool). WARNING: Capsule does NOT validate that the new milestone belongs to the opportunity's current pipeline. Passing a milestoneId from a different pipeline silently relocates the opportunity across pipelines, and `lastOpenMilestone` may then reference a milestone in the previous pipeline. Verify against the opportunity's current pipeline (read the opp first, list its pipeline's milestones via list_milestones) before passing a cross-pipeline id. NOTE: changing `milestoneId` can fire **pipeline / milestone-reached automations** that mutate `owner` / `team` on the destination milestone (same shape as `create_opportunity` \u2014 see its `milestoneId` description for the owner-clearing automation caveat). If a milestone-change-and-owner-set in the same call lands with `owner: null`, follow up with a second `update_opportunity` (or `batch_update_opportunity`) carrying both `ownerId` and `teamId` \u2014 milestone-reached triggers only fire on the transition, so a subsequent PUT preserves your values."
1964
2043
  ),
1965
- description: z5.string().optional(),
2044
+ description: z8.string().optional(),
1966
2045
  value: OpportunityValueSchema.optional(),
1967
- expectedCloseOn: z5.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional(),
1968
- probability: z5.number().int().min(0).max(100).optional().describe(
2046
+ expectedCloseOn: z8.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional(),
2047
+ probability: z8.number().int().min(0).max(100).optional().describe(
1969
2048
  "Win probability 0\u2013100. On an open milestone this overrides the milestone's default probability. CANNOT be set in the same call as a closing milestone (Won/Lost) \u2014 Capsule processes the milestone change first, the opportunity becomes closed, then the probability update is rejected as edit-on-closed-opp with 422 'probability can be updated only for open opportunity'. To close an opportunity, leave probability out of the call: it auto-snaps to 100% (Won) or 0% (Lost)."
1970
2049
  ),
1971
- lostReasonId: z5.number().int().positive().optional().describe(
2050
+ lostReasonId: positiveId.optional().describe(
1972
2051
  "Reason the opportunity was lost. Only meaningful when transitioning to a Lost milestone \u2014 Capsule silently drops it for other milestones. Without this set, a connector-driven Lost-close leaves `lostReason: null`. Discover IDs via list_lostreasons."
1973
2052
  ),
1974
- ownerId: z5.number().int().positive().optional().describe(
1975
- "Reassign owner to user ID. Once set, this connector cannot clear an owner back to null \u2014 use Capsule's web UI for that."
2053
+ ownerId: positiveId.optional().describe(
2054
+ "Reassign owner to user ID. Once set, this connector cannot clear an owner back to null \u2014 use Capsule's web UI for that. When you supply `ownerId` and omit `teamId`, the connector fetches the opportunity's current team and includes it in the PUT body to preserve it across the owner change. Without this defensive read, Capsule's PUT would clear the existing team (see NOTES-ON-CAPSULE-API.md \xA727 \u2014 same asymmetric semantic as /kases). Supply `teamId` explicitly on the same call to change the team instead."
2055
+ ),
2056
+ teamId: positiveId.nullable().optional().describe(
2057
+ "Reassign team: pass a team ID (discover via list_teams) to set, or `null` to unassign. Capsule preserves the existing owner across a team change (server-side), so `update_opportunity { teamId }` alone is safe \u2014 the owner is carried through. Owner must be a member of the new team or Capsule returns 422 'owner is not a member of the team'. Independent from `ownerId` \u2014 setting `teamId` does NOT clear the owner."
1976
2058
  ),
1977
- fields: z5.array(CustomFieldWriteSchema).optional().describe(fieldsArrayDescriptor("get_opportunity"))
2059
+ fields: z8.array(CustomFieldWriteSchema).optional().describe(fieldsArrayDescriptor("get_opportunity"))
1978
2060
  });
1979
2061
  async function updateOpportunity(input) {
1980
- const { id, milestoneId, ownerId, lostReasonId, fields, ...rest } = input;
2062
+ const { id, milestoneId, ownerId, teamId, lostReasonId, fields, ...rest } = input;
1981
2063
  const body = {};
1982
2064
  for (const [k, v] of Object.entries(rest)) {
1983
2065
  if (v !== void 0) body[k] = v;
1984
2066
  }
1985
- if (milestoneId) body["milestone"] = { id: milestoneId };
1986
- if (ownerId) body["owner"] = { id: ownerId };
1987
- if (lostReasonId) body["lostReason"] = { id: lostReasonId };
2067
+ setRef(body, "milestone", milestoneId);
2068
+ let resolvedTeamId = teamId;
2069
+ if (ownerId !== void 0 && teamId === void 0) {
2070
+ ({ teamId: resolvedTeamId } = await readEntityRefs(`/opportunities/${id}`, "opportunity"));
2071
+ }
2072
+ setRef(body, "owner", ownerId);
2073
+ setNullableRef(body, "team", resolvedTeamId);
2074
+ setRef(body, "lostReason", lostReasonId);
1988
2075
  const mappedFields = mapFieldsForBody(fields);
1989
2076
  if (mappedFields !== void 0) body["fields"] = mappedFields;
1990
2077
  return capsulePut(`/opportunities/${id}`, {
1991
2078
  opportunity: body
1992
2079
  });
1993
2080
  }
1994
- var batchUpdateOpportunitySchema = z5.object({
1995
- items: z5.array(updateOpportunitySchema).min(1).max(50).describe(
1996
- "Array of 1\u201350 update_opportunity inputs. Each item is the same shape as a single update_opportunity call \u2014 id is required, every other field is optional. Capped at 50 so a single tool call can't burn an outsized share of Capsule's hourly per-token rate budget."
1997
- )
2081
+ var { schema: batchUpdateOpportunitySchema, handler: batchUpdateOpportunity } = defineBatch({
2082
+ toolName: "batch_update_opportunity",
2083
+ itemSchema: updateOpportunitySchema,
2084
+ itemDescription: "Array of 1\u201350 update_opportunity inputs. Each item is the same shape as a single update_opportunity call \u2014 id is required, every other field is optional. Capped at 50 so a single tool call can't burn an outsized share of Capsule's hourly per-token rate budget.",
2085
+ itemHandler: updateOpportunity
1998
2086
  });
1999
- async function batchUpdateOpportunity(input, opts = {}) {
2000
- return batchExecute(
2001
- "batch_update_opportunity",
2002
- input.items,
2003
- (item) => updateOpportunity(item),
2004
- opts
2005
- );
2006
- }
2007
- var deleteOpportunitySchema = z5.object({
2008
- id: z5.number().int().positive(),
2009
- confirm: confirmFlag().describe(
2010
- "Must be set to true. Permanently deletes the opportunity. Irreversible."
2011
- )
2087
+ var { schema: deleteOpportunitySchema, handler: deleteOpportunity } = defineDelete({
2088
+ toolName: "delete_opportunity",
2089
+ pathPrefix: "/opportunities",
2090
+ confirmHint: "Must be set to true. Permanently deletes the opportunity. Irreversible."
2012
2091
  });
2013
- async function deleteOpportunity(input) {
2014
- if (input.confirm !== true) {
2015
- throw new Error("delete_opportunity requires confirm: true");
2016
- }
2017
- return idempotent(
2018
- () => capsuleDelete(`/opportunities/${input.id}`),
2019
- () => ({ deleted: true, alreadyDeleted: false, id: input.id }),
2020
- () => ({ deleted: true, alreadyDeleted: true, id: input.id })
2021
- );
2022
- }
2023
2092
 
2024
2093
  // src/tools/projects.ts
2025
- import { z as z6 } from "zod";
2026
- var listProjectsSchema = z6.object({
2027
- status: z6.enum(["OPEN", "CLOSED"]).optional(),
2028
- embed: z6.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
2029
- page: z6.number().int().positive().optional().default(1),
2030
- perPage: z6.number().int().min(1).max(100).optional().default(25)
2094
+ import { z as z9 } from "zod";
2095
+ var listProjectsSchema = z9.object({
2096
+ status: z9.enum(["OPEN", "CLOSED"]).optional(),
2097
+ embed: z9.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
2098
+ page: z9.number().int().positive().optional().default(1),
2099
+ perPage: z9.number().int().min(1).max(100).optional().default(25)
2031
2100
  });
2032
2101
  async function listProjects(input) {
2033
2102
  const { data, nextPage } = await capsuleGet("/kases", {
@@ -2038,9 +2107,9 @@ async function listProjects(input) {
2038
2107
  });
2039
2108
  return { ...data, nextPage };
2040
2109
  }
2041
- var getProjectSchema = z6.object({
2042
- id: z6.number().int().positive(),
2043
- embed: z6.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
2110
+ var getProjectSchema = z9.object({
2111
+ id: positiveId,
2112
+ embed: z9.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
2044
2113
  });
2045
2114
  async function getProject(input) {
2046
2115
  const { data } = await capsuleGet(`/kases/${input.id}`, {
@@ -2048,11 +2117,11 @@ async function getProject(input) {
2048
2117
  });
2049
2118
  return data;
2050
2119
  }
2051
- var getProjectsSchema = z6.object({
2052
- ids: z6.array(z6.number().int().positive()).min(1).max(50).describe(
2120
+ var getProjectsSchema = z9.object({
2121
+ ids: z9.array(positiveId).min(1).max(50).describe(
2053
2122
  "Array of project IDs (1\u201350). Capsule's native batch-fetch endpoint caps at 10 per request; the connector transparently splits larger sets into 10-id chunks and fans out the Capsule calls in parallel."
2054
2123
  ),
2055
- embed: z6.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
2124
+ embed: z9.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
2056
2125
  });
2057
2126
  async function getProjects(input) {
2058
2127
  const { ids, embed } = input;
@@ -2070,21 +2139,21 @@ async function getProjects(input) {
2070
2139
  );
2071
2140
  return { kases: responses.flatMap((r) => r.data.kases) };
2072
2141
  }
2073
- var createProjectSchema = z6.object({
2074
- name: z6.string().min(1),
2075
- partyId: z6.number().int().positive().describe("ID of the party linked to this project"),
2076
- description: z6.string().optional(),
2077
- status: z6.enum(["OPEN", "CLOSED"]).optional().describe("Defaults to OPEN when omitted."),
2078
- ownerId: z6.number().int().positive().optional().describe(
2142
+ var createProjectSchema = z9.object({
2143
+ name: z9.string().min(1),
2144
+ partyId: positiveId.describe("ID of the party linked to this project"),
2145
+ description: z9.string().optional(),
2146
+ status: z9.enum(["OPEN", "CLOSED"]).optional().describe("Defaults to OPEN when omitted."),
2147
+ ownerId: positiveId.optional().describe(
2079
2148
  "Assign to user ID. Defaults to the API-token owner when omitted, same as create_party / create_opportunity / create_task. NOTE: some Capsule tenants configure board-level **automation rules** that mutate `owner` (and `team`) on project creation \u2014 e.g. an automation that clears `owner` when a project enters a particular board. If you observe a project landing with unexpected `owner: null` after a create_project with `ownerId`, check the target board's automation configuration. Capsule's API itself does not drop `ownerId` when `stageId` is also supplied."
2080
2149
  ),
2081
- teamId: z6.number().int().positive().optional().describe(
2150
+ teamId: positiveId.optional().describe(
2082
2151
  "Assign to team ID (discover via list_teams). Capsule projects must always have at least one of {owner, team} set \u2014 Capsule returns 422 'owner or team is required' otherwise. Three ownership shapes are valid: owner alone, team alone, or owner+team (the user must be a member of the team \u2014 users can belong to multiple teams; 422 'owner is not a member of the team' otherwise). Tenant-specific board automations may set the team field on project creation (e.g. 'when project enters board X, set team to T'). If you observe a team set despite omitting `teamId`, check the target board's automation rules."
2083
2152
  ),
2084
- stageId: z6.number().int().positive().optional().describe(
2153
+ stageId: positiveId.optional().describe(
2085
2154
  "Stage (board column) to place the project on. Discover IDs via list_stages \u2014 each stage belongs to one Board, so picking a stageId implicitly picks the board. If omitted, the project is created with no stage assignment (and won't appear on any board). NOTE: tenant-specific board automation rules may run on project creation and mutate `owner` / `team` fields. See `create_project.ownerId` / `create_project.teamId` for the automation caveat. Capsule's create endpoint itself preserves the `ownerId` / `teamId` you supply \u2014 any clearing you observe traces to board automations, not the API."
2086
2155
  ),
2087
- expectedCloseOn: z6.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("YYYY-MM-DD")
2156
+ expectedCloseOn: z9.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("YYYY-MM-DD")
2088
2157
  });
2089
2158
  async function createProject(input) {
2090
2159
  const { partyId, ownerId, teamId, status, stageId, ...rest } = input;
@@ -2093,27 +2162,27 @@ async function createProject(input) {
2093
2162
  status: status ?? "OPEN",
2094
2163
  party: { id: partyId }
2095
2164
  };
2096
- if (ownerId) body["owner"] = { id: ownerId };
2097
- if (teamId) body["team"] = { id: teamId };
2165
+ setRef(body, "owner", ownerId);
2166
+ setRef(body, "team", teamId);
2098
2167
  if (stageId) body["stage"] = stageId;
2099
2168
  return capsulePost("/kases", { kase: body });
2100
2169
  }
2101
- var updateProjectSchema = z6.object({
2102
- id: z6.number().int().positive(),
2103
- name: z6.string().min(1).optional(),
2104
- description: z6.string().optional(),
2105
- status: z6.enum(["OPEN", "CLOSED"]).optional(),
2106
- ownerId: z6.number().int().positive().nullable().optional().describe(
2170
+ var updateProjectSchema = z9.object({
2171
+ id: positiveId,
2172
+ name: z9.string().min(1).optional(),
2173
+ description: z9.string().optional(),
2174
+ status: z9.enum(["OPEN", "CLOSED"]).optional(),
2175
+ ownerId: positiveId.nullable().optional().describe(
2107
2176
  "Reassign owner: pass a user ID to set, or `null` to unassign (matches the 'Unassign' option in Capsule's web UI). When you supply `ownerId` and omit `teamId` and/or `stageId`, the connector fetches the project's current omitted fields and includes them in the PUT body \u2014 this preserves them across the owner change (without it, Capsule's PUT would clear team; stage carry is defensive against the symmetric clear). Supply `teamId` and/or `stageId` explicitly on the same call to change them instead. `teamId: null` clears the team as part of an owner change. Constraints (Capsule enforces, 422 on violation): owner must be a member of the team if both are set; a project must always have at least one of {owner, team} set (cannot clear both)."
2108
2177
  ),
2109
- teamId: z6.number().int().positive().nullable().optional().describe(
2178
+ teamId: positiveId.nullable().optional().describe(
2110
2179
  "Reassign team: pass a team ID (discover via list_teams) to set, or `null` to unassign. Capsule preserves the existing owner across a team change (server-side), so `update_project { teamId }` alone is safe \u2014 the owner is carried through. Owner must be a member of the new team or Capsule returns 422 'owner is not a member of the team'. A project must always have at least one of {owner, team} set \u2014 `teamId: null` on a project whose owner is already null returns 422 'owner or team is required'."
2111
2180
  ),
2112
- stageId: z6.number().int().positive().optional().describe(
2181
+ stageId: positiveId.optional().describe(
2113
2182
  "Move the project to this stage (board column). Discover IDs via list_stages. Owner and team are preserved across stage-only updates (Capsule's PUT semantic). WARNING (cross-board): Capsule does NOT validate that the new stage belongs to the project's current board \u2014 passing a stageId from a different board silently relocates the project across boards. Team and other board-derived defaults are NOT updated to match the new board. Verify against the project's current board (read the project first, list its board's stages) before passing a cross-board id."
2114
2183
  ),
2115
- expectedCloseOn: z6.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("YYYY-MM-DD"),
2116
- fields: z6.array(CustomFieldWriteSchema).optional().describe(
2184
+ expectedCloseOn: z9.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("YYYY-MM-DD"),
2185
+ fields: z9.array(CustomFieldWriteSchema).optional().describe(
2117
2186
  fieldsArrayDescriptor("get_project") + " Project-specific: setting a field whose definition lives under a 'data tag' populates the row's internal tagId but does NOT auto-add the data tag to the project's tags array \u2014 use add_tag explicitly if you want it visible via embed=tags."
2118
2187
  )
2119
2188
  });
@@ -2126,55 +2195,38 @@ async function updateProject(input) {
2126
2195
  let resolvedTeamId = teamId;
2127
2196
  let resolvedStageId = stageId;
2128
2197
  if (ownerId !== void 0 && (teamId === void 0 || stageId === void 0)) {
2129
- const { data } = await capsuleGet(`/kases/${id}`);
2130
- if (teamId === void 0) {
2131
- resolvedTeamId = data.kase?.team?.id ?? void 0;
2132
- }
2133
- if (stageId === void 0) {
2134
- resolvedStageId = data.kase?.stage?.id ?? void 0;
2135
- }
2198
+ const current = await readEntityRefs(`/kases/${id}`, "kase");
2199
+ if (teamId === void 0) resolvedTeamId = current.teamId;
2200
+ if (stageId === void 0) resolvedStageId = current.stageId;
2136
2201
  }
2137
- if (ownerId === null) body["owner"] = null;
2138
- else if (ownerId !== void 0) body["owner"] = { id: ownerId };
2139
- if (resolvedTeamId === null) body["team"] = null;
2140
- else if (resolvedTeamId !== void 0) body["team"] = { id: resolvedTeamId };
2202
+ setNullableRef(body, "owner", ownerId);
2203
+ setNullableRef(body, "team", resolvedTeamId);
2141
2204
  if (resolvedStageId) body["stage"] = resolvedStageId;
2142
2205
  const mappedFields = mapFieldsForBody(fields);
2143
2206
  if (mappedFields !== void 0) body["fields"] = mappedFields;
2144
2207
  return capsulePut(`/kases/${id}`, { kase: body });
2145
2208
  }
2146
- var deleteProjectSchema = z6.object({
2147
- id: z6.number().int().positive(),
2148
- confirm: confirmFlag().describe(
2149
- "Must be set to true. Permanently deletes the project (case). Consider update_project status='CLOSED' instead. Irreversible."
2150
- )
2209
+ var { schema: deleteProjectSchema, handler: deleteProject } = defineDelete({
2210
+ toolName: "delete_project",
2211
+ pathPrefix: "/kases",
2212
+ confirmHint: "Must be set to true. Permanently deletes the project (case). Consider update_project status='CLOSED' instead. Irreversible."
2151
2213
  });
2152
- async function deleteProject(input) {
2153
- if (input.confirm !== true) {
2154
- throw new Error("delete_project requires confirm: true");
2155
- }
2156
- return idempotent(
2157
- () => capsuleDelete(`/kases/${input.id}`),
2158
- () => ({ deleted: true, alreadyDeleted: false, id: input.id }),
2159
- () => ({ deleted: true, alreadyDeleted: true, id: input.id })
2160
- );
2161
- }
2162
2214
 
2163
2215
  // src/tools/tasks.ts
2164
- import { z as z7 } from "zod";
2165
- var listTasksSchema = z7.object({
2216
+ import { z as z10 } from "zod";
2217
+ var listTasksSchema = z10.object({
2166
2218
  // Note: Capsule has a third internal status `PENDING` (a task that's
2167
2219
  // part of an active track but not yet "open"), but it can only be
2168
2220
  // reached via track machinery — it is NOT directly settable by
2169
2221
  // /tasks PUT, and a list filter for it returns the same as OPEN
2170
2222
  // anyway. We expose only the two values that are actually filterable
2171
2223
  // by the v2 API.
2172
- status: z7.enum(["OPEN", "COMPLETED"]).optional().describe(
2224
+ status: z10.enum(["OPEN", "COMPLETED"]).optional().describe(
2173
2225
  "Defaults to OPEN when omitted. Pass COMPLETED to filter to completed tasks, or 'OPEN' explicitly."
2174
2226
  ),
2175
- ownerId: z7.number().int().positive().optional().describe("Filter to tasks owned by this user ID"),
2176
- page: z7.number().int().positive().optional().default(1),
2177
- perPage: z7.number().int().min(1).max(100).optional().default(25)
2227
+ ownerId: positiveId.optional().describe("Filter to tasks owned by this user ID"),
2228
+ page: z10.number().int().positive().optional().default(1),
2229
+ perPage: z10.number().int().min(1).max(100).optional().default(25)
2178
2230
  });
2179
2231
  async function listTasks(input) {
2180
2232
  const { data, nextPage } = await capsuleGet("/tasks", {
@@ -2188,15 +2240,15 @@ async function listTasks(input) {
2188
2240
  });
2189
2241
  return { ...data, nextPage };
2190
2242
  }
2191
- var getTaskSchema = z7.object({
2192
- id: z7.number().int().positive().describe("Task ID")
2243
+ var getTaskSchema = z10.object({
2244
+ id: positiveId.describe("Task ID")
2193
2245
  });
2194
2246
  async function getTask(input) {
2195
2247
  const { data } = await capsuleGet(`/tasks/${input.id}`);
2196
2248
  return data;
2197
2249
  }
2198
- var getTasksSchema = z7.object({
2199
- ids: z7.array(z7.number().int().positive()).min(1).max(50).describe(
2250
+ var getTasksSchema = z10.object({
2251
+ ids: z10.array(positiveId).min(1).max(50).describe(
2200
2252
  "Array of task IDs (1\u201350). Capsule's native batch-fetch endpoint caps at 10 per request; the connector transparently splits larger sets into 10-id chunks and fans out the Capsule calls in parallel."
2201
2253
  )
2202
2254
  });
@@ -2212,17 +2264,17 @@ async function getTasks(input) {
2212
2264
  );
2213
2265
  return { tasks: responses.flatMap((r) => r.data.tasks) };
2214
2266
  }
2215
- var createTaskSchema = z7.object({
2216
- description: z7.string().min(1),
2217
- dueOn: z7.string().regex(/^\d{4}-\d{2}-\d{2}$/).describe("YYYY-MM-DD"),
2218
- dueTime: z7.string().regex(/^\d{2}:\d{2}$/).optional().describe("HH:MM in user's timezone"),
2219
- detail: z7.string().optional(),
2220
- ownerId: z7.number().int().positive().optional().describe(
2267
+ var createTaskSchema = z10.object({
2268
+ description: z10.string().min(1),
2269
+ dueOn: z10.string().regex(/^\d{4}-\d{2}-\d{2}$/).describe("YYYY-MM-DD"),
2270
+ dueTime: z10.string().regex(/^\d{2}:\d{2}$/).optional().describe("HH:MM in user's timezone"),
2271
+ detail: z10.string().optional(),
2272
+ ownerId: positiveId.optional().describe(
2221
2273
  "Assign to user ID. Defaults to the API-token owner when omitted. Once set, this connector cannot clear the owner back to null \u2014 use Capsule's web UI for that."
2222
2274
  ),
2223
- partyId: z7.number().int().positive().optional().describe("Link task to a party (mutually exclusive with opportunityId/projectId)"),
2224
- opportunityId: z7.number().int().positive().optional().describe("Link task to an opportunity (mutually exclusive with partyId/projectId)"),
2225
- projectId: z7.number().int().positive().optional().describe("Link task to a project (mutually exclusive with partyId/opportunityId)")
2275
+ partyId: positiveId.optional().describe("Link task to a party (mutually exclusive with opportunityId/projectId)"),
2276
+ opportunityId: positiveId.optional().describe("Link task to an opportunity (mutually exclusive with partyId/projectId)"),
2277
+ projectId: positiveId.optional().describe("Link task to a project (mutually exclusive with partyId/opportunityId)")
2226
2278
  });
2227
2279
  async function createTask(input) {
2228
2280
  const linked = [input.partyId, input.opportunityId, input.projectId].filter(Boolean);
@@ -2231,25 +2283,25 @@ async function createTask(input) {
2231
2283
  }
2232
2284
  const { ownerId, partyId, opportunityId, projectId, ...rest } = input;
2233
2285
  const body = { ...rest };
2234
- if (ownerId) body["owner"] = { id: ownerId };
2235
- if (partyId) body["party"] = { id: partyId };
2236
- if (opportunityId) body["opportunity"] = { id: opportunityId };
2237
- if (projectId) body["kase"] = { id: projectId };
2286
+ setRef(body, "owner", ownerId);
2287
+ setRef(body, "party", partyId);
2288
+ setRef(body, "opportunity", opportunityId);
2289
+ setRef(body, "kase", projectId);
2238
2290
  return capsulePost("/tasks", { task: body });
2239
2291
  }
2240
- var updateTaskSchema = z7.object({
2241
- id: z7.number().int().positive(),
2242
- description: z7.string().min(1).optional(),
2243
- dueOn: z7.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("YYYY-MM-DD"),
2244
- dueTime: z7.string().regex(/^\d{2}:\d{2}$/).optional().describe("HH:MM in user's timezone"),
2245
- detail: z7.string().optional(),
2292
+ var updateTaskSchema = z10.object({
2293
+ id: positiveId,
2294
+ description: z10.string().min(1).optional(),
2295
+ dueOn: z10.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("YYYY-MM-DD"),
2296
+ dueTime: z10.string().regex(/^\d{2}:\d{2}$/).optional().describe("HH:MM in user's timezone"),
2297
+ detail: z10.string().optional(),
2246
2298
  // Capsule rejects direct sets of `PENDING` (which is a track-machinery
2247
2299
  // internal state) with 422 "cannot set task status to PENDING".
2248
2300
  // Only OPEN and COMPLETED are settable here.
2249
- status: z7.enum(["OPEN", "COMPLETED"]).optional().describe(
2301
+ status: z10.enum(["OPEN", "COMPLETED"]).optional().describe(
2250
2302
  "Set to OPEN or COMPLETED. (PENDING exists internally for track-driven tasks but cannot be set directly via this tool \u2014 Capsule rejects it.) Setting status: OPEN on an already-open task is a true no-op (does not advance updatedAt)."
2251
2303
  ),
2252
- ownerId: z7.number().int().positive().optional().describe(
2304
+ ownerId: positiveId.optional().describe(
2253
2305
  "Reassign owner to user ID. Once set, this connector cannot clear an owner back to null \u2014 use Capsule's web UI for that."
2254
2306
  )
2255
2307
  });
@@ -2259,51 +2311,40 @@ async function updateTask(input) {
2259
2311
  for (const [k, v] of Object.entries(rest)) {
2260
2312
  if (v !== void 0) body[k] = v;
2261
2313
  }
2262
- if (ownerId) body["owner"] = { id: ownerId };
2314
+ setRef(body, "owner", ownerId);
2263
2315
  return capsulePut(`/tasks/${id}`, { task: body });
2264
2316
  }
2265
- var completeTaskSchema = z7.object({
2266
- id: z7.number().int().positive()
2317
+ var completeTaskSchema = z10.object({
2318
+ id: positiveId
2267
2319
  });
2268
2320
  async function completeTask(input) {
2269
2321
  return capsulePut(`/tasks/${input.id}`, {
2270
2322
  task: { status: "COMPLETED" }
2271
2323
  });
2272
2324
  }
2273
- var batchCompleteTaskSchema = z7.object({
2274
- ids: z7.array(z7.number().int().positive()).min(1).max(50).describe(
2325
+ var batchCompleteTaskSchema = z10.object({
2326
+ ids: z10.array(positiveId).min(1).max(50).describe(
2275
2327
  "Array of 1\u201350 task ids to mark COMPLETED in parallel. Each id resolves to one PUT /tasks/{id}; failures (e.g. 404 for a deleted task) surface per-item in the result array, the rest still complete. Capped at 50."
2276
2328
  )
2277
2329
  });
2278
2330
  async function batchCompleteTask(input, opts = {}) {
2279
2331
  return batchExecute("batch_complete_task", input.ids, (id) => completeTask({ id }), opts);
2280
2332
  }
2281
- var deleteTaskSchema = z7.object({
2282
- id: z7.number().int().positive(),
2283
- confirm: confirmFlag().describe(
2284
- "Must be set to true. Permanently deletes the task. To mark done without losing history use complete_task. Irreversible."
2285
- )
2333
+ var { schema: deleteTaskSchema, handler: deleteTask } = defineDelete({
2334
+ toolName: "delete_task",
2335
+ pathPrefix: "/tasks",
2336
+ confirmHint: "Must be set to true. Permanently deletes the task. To mark done without losing history use complete_task. Irreversible."
2286
2337
  });
2287
- async function deleteTask(input) {
2288
- if (input.confirm !== true) {
2289
- throw new Error("delete_task requires confirm: true");
2290
- }
2291
- return idempotent(
2292
- () => capsuleDelete(`/tasks/${input.id}`),
2293
- () => ({ deleted: true, alreadyDeleted: false, id: input.id }),
2294
- () => ({ deleted: true, alreadyDeleted: true, id: input.id })
2295
- );
2296
- }
2297
2338
 
2298
2339
  // src/tools/entries.ts
2299
- import { z as z8 } from "zod";
2340
+ import { z as z11 } from "zod";
2300
2341
  var listEntriesPagination = {
2301
- page: z8.number().int().positive().optional().default(1),
2302
- perPage: z8.number().int().min(1).max(100).optional().default(25),
2303
- embed: z8.string().optional().describe(EMBED_ATTACHMENTS_PARTICIPANTS_DESCRIPTION)
2342
+ page: z11.number().int().positive().optional().default(1),
2343
+ perPage: z11.number().int().min(1).max(100).optional().default(25),
2344
+ embed: z11.string().optional().describe(EMBED_ATTACHMENTS_PARTICIPANTS_DESCRIPTION)
2304
2345
  };
2305
- var listPartyEntriesSchema = z8.object({
2306
- partyId: z8.number().int().positive(),
2346
+ var listPartyEntriesSchema = z11.object({
2347
+ partyId: positiveId,
2307
2348
  ...listEntriesPagination
2308
2349
  });
2309
2350
  async function listPartyEntries(input) {
@@ -2313,8 +2354,8 @@ async function listPartyEntries(input) {
2313
2354
  );
2314
2355
  return { ...data, nextPage };
2315
2356
  }
2316
- var listOpportunityEntriesSchema = z8.object({
2317
- opportunityId: z8.number().int().positive(),
2357
+ var listOpportunityEntriesSchema = z11.object({
2358
+ opportunityId: positiveId,
2318
2359
  ...listEntriesPagination
2319
2360
  });
2320
2361
  async function listOpportunityEntries(input) {
@@ -2324,8 +2365,8 @@ async function listOpportunityEntries(input) {
2324
2365
  );
2325
2366
  return { ...data, nextPage };
2326
2367
  }
2327
- var listProjectEntriesSchema = z8.object({
2328
- projectId: z8.number().int().positive(),
2368
+ var listProjectEntriesSchema = z11.object({
2369
+ projectId: positiveId,
2329
2370
  ...listEntriesPagination
2330
2371
  });
2331
2372
  async function listProjectEntries(input) {
@@ -2335,9 +2376,9 @@ async function listProjectEntries(input) {
2335
2376
  );
2336
2377
  return { ...data, nextPage };
2337
2378
  }
2338
- var getEntrySchema = z8.object({
2339
- id: z8.number().int().positive(),
2340
- embed: z8.string().optional().describe(EMBED_ATTACHMENTS_PARTICIPANTS_DESCRIPTION)
2379
+ var getEntrySchema = z11.object({
2380
+ id: positiveId,
2381
+ embed: z11.string().optional().describe(EMBED_ATTACHMENTS_PARTICIPANTS_DESCRIPTION)
2341
2382
  });
2342
2383
  async function getEntry(input) {
2343
2384
  const { data } = await capsuleGet(`/entries/${input.id}`, {
@@ -2345,7 +2386,7 @@ async function getEntry(input) {
2345
2386
  });
2346
2387
  return data;
2347
2388
  }
2348
- var listEntriesSchema = z8.object({
2389
+ var listEntriesSchema = z11.object({
2349
2390
  ...listEntriesPagination
2350
2391
  });
2351
2392
  async function listEntries(input) {
@@ -2356,14 +2397,14 @@ async function listEntries(input) {
2356
2397
  });
2357
2398
  return { ...data, nextPage };
2358
2399
  }
2359
- var addNoteSchema = z8.object({
2360
- content: z8.string().min(1).describe(
2400
+ var addNoteSchema = z11.object({
2401
+ content: z11.string().min(1).describe(
2361
2402
  "Note body text. Stored verbatim and treated as MARKDOWN \u2014 Capsule's web UI renders the markdown when displaying. Pass markdown source ('# Heading', '**bold**', '- bullet'), not HTML."
2362
2403
  ),
2363
- partyId: z8.number().int().positive().optional().describe("Link note to a party (mutually exclusive with opportunityId/projectId)"),
2364
- opportunityId: z8.number().int().positive().optional().describe("Link note to an opportunity (mutually exclusive with partyId/projectId)"),
2365
- projectId: z8.number().int().positive().optional().describe("Link note to a project (mutually exclusive with partyId/opportunityId)"),
2366
- entryAt: z8.string().regex(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:?\d{2})$/).optional().describe(
2404
+ partyId: positiveId.optional().describe("Link note to a party (mutually exclusive with opportunityId/projectId)"),
2405
+ opportunityId: positiveId.optional().describe("Link note to an opportunity (mutually exclusive with partyId/projectId)"),
2406
+ projectId: positiveId.optional().describe("Link note to a project (mutually exclusive with partyId/opportunityId)"),
2407
+ entryAt: z11.string().regex(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:?\d{2})$/).optional().describe(
2367
2408
  "ISO-8601 timestamp for when this note actually happened (e.g. '2024-03-15T14:30:00Z'). Defaults to now. Use this for backdating historical notes when migrating from another system. `entryAt` is preserved across subsequent update_entry calls; only `updatedAt` advances on edits. Note attribution flows to the API-token owner \u2014 there is no way to record a note as authored by a different user via this connector (a `creatorId` parameter would enable audit-attribution spoofing on shared-connector deployments, so it is intentionally not exposed)."
2368
2409
  )
2369
2410
  });
@@ -2374,18 +2415,18 @@ async function addNote(input) {
2374
2415
  throw new Error("Provide exactly one of partyId, opportunityId, or projectId");
2375
2416
  }
2376
2417
  const body = { type: "note", content };
2377
- if (partyId) body["party"] = { id: partyId };
2378
- if (opportunityId) body["opportunity"] = { id: opportunityId };
2379
- if (projectId) body["kase"] = { id: projectId };
2418
+ setRef(body, "party", partyId);
2419
+ setRef(body, "opportunity", opportunityId);
2420
+ setRef(body, "kase", projectId);
2380
2421
  if (entryAt !== void 0) body["entryAt"] = entryAt;
2381
2422
  return capsulePost("/entries", { entry: body });
2382
2423
  }
2383
- var updateEntrySchema = z8.object({
2384
- id: z8.number().int().positive().describe("Entry ID to update"),
2385
- content: z8.string().min(1).optional().describe(
2424
+ var updateEntrySchema = z11.object({
2425
+ id: positiveId.describe("Entry ID to update"),
2426
+ content: z11.string().min(1).optional().describe(
2386
2427
  "New body text for the entry. For notes, this is the markdown content; for emails, the body. Provide only if you want to change it."
2387
2428
  ),
2388
- subject: z8.string().optional().describe(
2429
+ subject: z11.string().optional().describe(
2389
2430
  "New subject line. Mostly meaningful on email-type entries; on plain notes Capsule accepts the call (HTTP 200) but **does not store the subject and does not advance `updatedAt`** \u2014 a true no-op for inapplicable fields. `entryAt` (when the note was authored) is preserved across edits; `updatedAt` advances only when an applicable field actually changes. To sort/filter by 'when did this happen', use `entryAt`; for 'last touched', use `updatedAt`."
2390
2431
  )
2391
2432
  });
@@ -2399,30 +2440,20 @@ async function updateEntry(input) {
2399
2440
  }
2400
2441
  return capsulePut(`/entries/${id}`, { entry: body });
2401
2442
  }
2402
- var deleteEntrySchema = z8.object({
2403
- id: z8.number().int().positive().describe("Entry (note/email/task-record) ID"),
2404
- confirm: confirmFlag().describe(
2405
- "Must be set to true. Permanently deletes the entry \u2014 use this to remove a note from a party/opportunity/project. Irreversible."
2406
- )
2443
+ var { schema: deleteEntrySchema, handler: deleteEntry } = defineDelete({
2444
+ toolName: "delete_entry",
2445
+ pathPrefix: "/entries",
2446
+ confirmHint: "Must be set to true. Permanently deletes the entry \u2014 use this to remove a note from a party/opportunity/project. Irreversible.",
2447
+ idDescription: "Entry (note/email/task-record) ID"
2407
2448
  });
2408
- async function deleteEntry(input) {
2409
- if (input.confirm !== true) {
2410
- throw new Error("delete_entry requires confirm: true");
2411
- }
2412
- return idempotent(
2413
- () => capsuleDelete(`/entries/${input.id}`),
2414
- () => ({ deleted: true, alreadyDeleted: false, id: input.id }),
2415
- () => ({ deleted: true, alreadyDeleted: true, id: input.id })
2416
- );
2417
- }
2418
2449
 
2419
2450
  // src/tools/pipelines.ts
2420
- import { z as z9 } from "zod";
2451
+ import { z as z12 } from "zod";
2421
2452
  var paginationFields = {
2422
- page: z9.number().int().positive().optional(),
2423
- perPage: z9.number().int().min(1).max(100).optional()
2453
+ page: z12.number().int().positive().optional(),
2454
+ perPage: z12.number().int().min(1).max(100).optional()
2424
2455
  };
2425
- var listPipelinesSchema = z9.object({ ...paginationFields });
2456
+ var listPipelinesSchema = z12.object({ ...paginationFields });
2426
2457
  async function listPipelines(input) {
2427
2458
  const { data, nextPage } = await capsuleGetCached("/pipelines", {
2428
2459
  page: input.page ?? 1,
@@ -2430,8 +2461,8 @@ async function listPipelines(input) {
2430
2461
  });
2431
2462
  return { ...data, nextPage };
2432
2463
  }
2433
- var listMilestonesSchema = z9.object({
2434
- pipelineId: z9.number().int().positive(),
2464
+ var listMilestonesSchema = z12.object({
2465
+ pipelineId: positiveId,
2435
2466
  ...paginationFields
2436
2467
  });
2437
2468
  async function listMilestones(input) {
@@ -2443,12 +2474,12 @@ async function listMilestones(input) {
2443
2474
  }
2444
2475
 
2445
2476
  // src/tools/boards.ts
2446
- import { z as z10 } from "zod";
2477
+ import { z as z13 } from "zod";
2447
2478
  var paginationFields2 = {
2448
- page: z10.number().int().positive().optional(),
2449
- perPage: z10.number().int().min(1).max(100).optional()
2479
+ page: z13.number().int().positive().optional(),
2480
+ perPage: z13.number().int().min(1).max(100).optional()
2450
2481
  };
2451
- var listBoardsSchema = z10.object({ ...paginationFields2 });
2482
+ var listBoardsSchema = z13.object({ ...paginationFields2 });
2452
2483
  async function listBoards(input) {
2453
2484
  const { data, nextPage } = await capsuleGetCached("/boards", {
2454
2485
  page: input.page ?? 1,
@@ -2456,8 +2487,8 @@ async function listBoards(input) {
2456
2487
  });
2457
2488
  return { ...data, nextPage };
2458
2489
  }
2459
- var listStagesSchema = z10.object({
2460
- boardId: z10.number().int().positive().optional().describe(
2490
+ var listStagesSchema = z13.object({
2491
+ boardId: positiveId.optional().describe(
2461
2492
  "Optional. If provided, returns only the stages defined on that specific board (uses /boards/{id}/stages). Omit to get all stages across all boards in one call."
2462
2493
  ),
2463
2494
  ...paginationFields2
@@ -2472,7 +2503,7 @@ async function listStages(input) {
2472
2503
  }
2473
2504
 
2474
2505
  // src/tools/tags.ts
2475
- import { z as z11 } from "zod";
2506
+ import { z as z14 } from "zod";
2476
2507
  var TAG_LIST_PATH = {
2477
2508
  parties: "/parties/tags",
2478
2509
  opportunities: "/opportunities/tags",
@@ -2483,11 +2514,11 @@ var ENTITY_TO_WRAPPER = {
2483
2514
  opportunities: "opportunity",
2484
2515
  kases: "kase"
2485
2516
  };
2486
- var TagEntity = z11.enum(["parties", "opportunities", "kases"]).describe("Which entity type. Use 'kases' for projects (Capsule's legacy path name).");
2487
- var listTagsSchema = z11.object({
2488
- entity: z11.enum(["parties", "opportunities", "kases"]).describe("The resource type to list tags for"),
2489
- page: z11.number().int().positive().optional(),
2490
- perPage: z11.number().int().min(1).max(100).optional()
2517
+ var TagEntity = z14.enum(["parties", "opportunities", "kases"]).describe("Which entity type. Use 'kases' for projects (Capsule's legacy path name).");
2518
+ var listTagsSchema = z14.object({
2519
+ entity: z14.enum(["parties", "opportunities", "kases"]).describe("The resource type to list tags for"),
2520
+ page: z14.number().int().positive().optional(),
2521
+ perPage: z14.number().int().min(1).max(100).optional()
2491
2522
  });
2492
2523
  async function listTags(input) {
2493
2524
  const path = TAG_LIST_PATH[input.entity];
@@ -2497,10 +2528,10 @@ async function listTags(input) {
2497
2528
  });
2498
2529
  return { ...data, nextPage };
2499
2530
  }
2500
- var addTagSchema = z11.object({
2531
+ var addTagSchema = z14.object({
2501
2532
  entity: TagEntity,
2502
- entityId: z11.number().int().positive().describe("The party/opportunity/kase id."),
2503
- tagName: z11.string().min(1).describe(
2533
+ entityId: positiveId.describe("The party/opportunity/kase id."),
2534
+ tagName: z14.string().min(1).describe(
2504
2535
  "Name of the tag to attach. Capsule resolves by name: if a tag with this name already exists in the tenant it is attached to the entity; if not, Capsule creates the tag and attaches it. Names are tenant-global. Capsule matches case-INSENSITIVELY when resolving (so 'VIP' and 'vip' attach the same tag), preserving the canonical casing from whichever variant was created first. To ensure consistent casing in your tag list, call list_tags first and reuse the exact name from there. Idempotent \u2014 re-attaching an already-attached tag is harmless."
2505
2536
  )
2506
2537
  });
@@ -2513,10 +2544,10 @@ async function addTag(input) {
2513
2544
  invalidateByPrefix(TAG_LIST_PATH[entity], "add_tag");
2514
2545
  return result;
2515
2546
  }
2516
- var removeTagByIdSchema = z11.object({
2547
+ var removeTagByIdSchema = z14.object({
2517
2548
  entity: TagEntity,
2518
- entityId: z11.number().int().positive().describe("The party/opportunity/kase id."),
2519
- tagId: z11.number().int().positive().describe(
2549
+ entityId: positiveId.describe("The party/opportunity/kase id."),
2550
+ tagId: positiveId.describe(
2520
2551
  "The tag's id. Read via get_party / get_opportunity / get_project with embed='tags' \u2014 each tag entry in the response has an `id` field. list_tags returns the same ids for the same tags, so either source works; reading via embed first is the safer pattern because it confirms the tag is actually attached to this entity before you try to remove it (otherwise Capsule returns 422 'tag not found to delete'). Removing detaches the tag from this entity only; the tag definition itself persists in the tenant for other entities that share it."
2521
2552
  )
2522
2553
  });
@@ -2544,28 +2575,24 @@ async function removeTagById(input) {
2544
2575
  invalidateByPrefix(TAG_LIST_PATH[entity], "remove_tag_by_id");
2545
2576
  return result;
2546
2577
  }
2547
- var batchAddTagSchema = z11.object({
2548
- items: z11.array(addTagSchema).min(1).max(50).describe(
2549
- "Array of 1\u201350 add_tag inputs. Useful for mass-tagging \u2014 e.g. 'tag these 20 contacts as RSAC26'. Each item is the same shape as a single add_tag call. The list_tags cache is invalidated for each affected entity type. Capped at 50."
2550
- )
2578
+ var { schema: batchAddTagSchema, handler: batchAddTag } = defineBatch({
2579
+ toolName: "batch_add_tag",
2580
+ itemSchema: addTagSchema,
2581
+ itemDescription: "Array of 1\u201350 add_tag inputs. Useful for mass-tagging \u2014 e.g. 'tag these 20 contacts as RSAC26'. Each item is the same shape as a single add_tag call. The list_tags cache is invalidated for each affected entity type. Capped at 50.",
2582
+ itemHandler: addTag
2551
2583
  });
2552
- async function batchAddTag(input, opts = {}) {
2553
- return batchExecute("batch_add_tag", input.items, (item) => addTag(item), opts);
2554
- }
2555
- var batchRemoveTagByIdSchema = z11.object({
2556
- items: z11.array(removeTagByIdSchema).min(1).max(50).describe(
2557
- "Array of 1\u201350 remove_tag_by_id inputs. Each item is the same shape as a single remove_tag_by_id call. Detaches the tag from each specified entity; the tag definition itself persists in the tenant. Capped at 50."
2558
- )
2584
+ var { schema: batchRemoveTagByIdSchema, handler: batchRemoveTagById } = defineBatch({
2585
+ toolName: "batch_remove_tag_by_id",
2586
+ itemSchema: removeTagByIdSchema,
2587
+ itemDescription: "Array of 1\u201350 remove_tag_by_id inputs. Each item is the same shape as a single remove_tag_by_id call. Detaches the tag from each specified entity; the tag definition itself persists in the tenant. Capped at 50.",
2588
+ itemHandler: removeTagById
2559
2589
  });
2560
- async function batchRemoveTagById(input, opts = {}) {
2561
- return batchExecute("batch_remove_tag_by_id", input.items, (item) => removeTagById(item), opts);
2562
- }
2563
2590
 
2564
2591
  // src/tools/users.ts
2565
- import { z as z12 } from "zod";
2566
- var listUsersSchema = z12.object({
2567
- page: z12.number().int().positive().optional(),
2568
- perPage: z12.number().int().min(1).max(100).optional()
2592
+ import { z as z15 } from "zod";
2593
+ var listUsersSchema = z15.object({
2594
+ page: z15.number().int().positive().optional(),
2595
+ perPage: z15.number().int().min(1).max(100).optional()
2569
2596
  });
2570
2597
  async function listUsers(input) {
2571
2598
  const { data, nextPage } = await capsuleGetCached("/users", {
@@ -2574,32 +2601,32 @@ async function listUsers(input) {
2574
2601
  });
2575
2602
  return { ...data, nextPage };
2576
2603
  }
2577
- var getCurrentUserSchema = z12.object({});
2604
+ var getCurrentUserSchema = z15.object({});
2578
2605
  async function getCurrentUser(_input) {
2579
2606
  const { data } = await capsuleGet("/users/current");
2580
2607
  return data;
2581
2608
  }
2582
2609
 
2583
2610
  // src/tools/filters.ts
2584
- import { z as z13 } from "zod";
2585
- var FilterConditionSchema = z13.object({
2586
- field: z13.string().describe(
2611
+ import { z as z16 } from "zod";
2612
+ var FilterConditionSchema = z16.object({
2613
+ field: z16.string().describe(
2587
2614
  "The Capsule filter-side field name (these differ from response field names \u2014 e.g. response.createdAt is filter-side 'addedOn', response.lastContactedAt is filter-side 'lastContactedOn'). Common: 'addedOn' (date created), 'updatedOn' (date last modified), 'lastContactedOn' (parties only), 'name', 'tag', 'owner', 'team', 'type' (parties: person|organisation), 'milestone' (opportunities), 'status' (opp/project: OPEN|CLOSED), 'closedOn' (opp/project), 'expectedCloseOn' (opp/project), 'hasTags', 'hasEmailAddress' (parties), 'isOpen', 'isStale' (opportunities), 'custom:{fieldId}'. Full per-entity list: https://developer.capsulecrm.com/v2/reference/filters"
2588
2615
  ),
2589
- operator: z13.string().describe(
2616
+ operator: z16.string().describe(
2590
2617
  "The filter operator. Common: 'is', 'is not' (use value=null to test for null), 'contains', 'does not contain', 'is greater than', 'is less than', 'is within last' (date fields, value=integer days), 'is more than' (date fields, value=integer days ago), 'starts with', 'ends with'. Operator validity depends on the field's type."
2591
2618
  ),
2592
- value: z13.union([z13.string(), z13.number(), z13.boolean(), z13.null()]).describe(
2619
+ value: z16.union([z16.string(), z16.number(), z16.boolean(), z16.null()]).describe(
2593
2620
  "The value to compare against. For 'is within last' on date fields, pass an integer number of days. For tag filters, pass the tag name (string) or tag id (number). For 'is not' null tests, pass null literally."
2594
2621
  )
2595
2622
  });
2596
- var FilterInputSchema = z13.object({
2597
- conditions: z13.array(FilterConditionSchema).min(1).describe(
2623
+ var FilterInputSchema = z16.object({
2624
+ conditions: z16.array(FilterConditionSchema).min(1).describe(
2598
2625
  "Array of filter conditions. All conditions are ANDed together. To get newest records, use a date condition like {field: 'addedOn', operator: 'is within last', value: 7} and pick the highest-id row from the result (Capsule IDs are monotonic)."
2599
2626
  ),
2600
- embed: z13.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
2601
- page: z13.number().int().positive().optional().default(1),
2602
- perPage: z13.number().int().min(1).max(100).optional().default(25)
2627
+ embed: z16.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
2628
+ page: z16.number().int().positive().optional().default(1),
2629
+ perPage: z16.number().int().min(1).max(100).optional().default(25)
2603
2630
  });
2604
2631
  async function runFilter(entityPath, input) {
2605
2632
  const { data, nextPage } = await capsuleSearch(
@@ -2630,12 +2657,12 @@ async function filterProjects(input) {
2630
2657
  }
2631
2658
 
2632
2659
  // src/tools/metadata.ts
2633
- import { z as z14 } from "zod";
2660
+ import { z as z17 } from "zod";
2634
2661
  var paginationFields3 = {
2635
- page: z14.number().int().positive().optional(),
2636
- perPage: z14.number().int().min(1).max(100).optional().describe("Page size, max 100. Defaults to 100 for reference data.")
2662
+ page: z17.number().int().positive().optional(),
2663
+ perPage: z17.number().int().min(1).max(100).optional().describe("Page size, max 100. Defaults to 100 for reference data.")
2637
2664
  };
2638
- var listTeamsSchema = z14.object({ ...paginationFields3 });
2665
+ var listTeamsSchema = z17.object({ ...paginationFields3 });
2639
2666
  async function listTeams(input) {
2640
2667
  const { data, nextPage } = await capsuleGetCached("/teams", {
2641
2668
  page: input.page ?? 1,
@@ -2643,7 +2670,7 @@ async function listTeams(input) {
2643
2670
  });
2644
2671
  return { ...data, nextPage };
2645
2672
  }
2646
- var listLostReasonsSchema = z14.object({ ...paginationFields3 });
2673
+ var listLostReasonsSchema = z17.object({ ...paginationFields3 });
2647
2674
  async function listLostReasons(input) {
2648
2675
  const { data, nextPage } = await capsuleGetCached("/lostreasons", {
2649
2676
  page: input.page ?? 1,
@@ -2651,7 +2678,7 @@ async function listLostReasons(input) {
2651
2678
  });
2652
2679
  return { ...data, nextPage };
2653
2680
  }
2654
- var listActivityTypesSchema = z14.object({ ...paginationFields3 });
2681
+ var listActivityTypesSchema = z17.object({ ...paginationFields3 });
2655
2682
  async function listActivityTypes(input) {
2656
2683
  const { data, nextPage } = await capsuleGetCached(
2657
2684
  "/activitytypes",
@@ -2662,12 +2689,12 @@ async function listActivityTypes(input) {
2662
2689
  );
2663
2690
  return { ...data, nextPage };
2664
2691
  }
2665
- var getSiteSchema = z14.object({});
2692
+ var getSiteSchema = z17.object({});
2666
2693
  async function getSite(_input) {
2667
2694
  const { data } = await capsuleGetCached("/site");
2668
2695
  return data;
2669
2696
  }
2670
- var listTrackDefinitionsSchema = z14.object({ ...paginationFields3 });
2697
+ var listTrackDefinitionsSchema = z17.object({ ...paginationFields3 });
2671
2698
  async function listTrackDefinitions(input) {
2672
2699
  const { data, nextPage } = await capsuleGetCached(
2673
2700
  "/trackdefinitions",
@@ -2675,7 +2702,7 @@ async function listTrackDefinitions(input) {
2675
2702
  );
2676
2703
  return { ...data, nextPage };
2677
2704
  }
2678
- var listCategoriesSchema = z14.object({ ...paginationFields3 });
2705
+ var listCategoriesSchema = z17.object({ ...paginationFields3 });
2679
2706
  async function listCategories(input) {
2680
2707
  const { data, nextPage } = await capsuleGetCached("/categories", {
2681
2708
  page: input.page ?? 1,
@@ -2683,7 +2710,7 @@ async function listCategories(input) {
2683
2710
  });
2684
2711
  return { ...data, nextPage };
2685
2712
  }
2686
- var listGoalsSchema = z14.object({ ...paginationFields3 });
2713
+ var listGoalsSchema = z17.object({ ...paginationFields3 });
2687
2714
  async function listGoals(input) {
2688
2715
  const { data, nextPage } = await capsuleGetCached("/goals", {
2689
2716
  page: input.page ?? 1,
@@ -2693,14 +2720,14 @@ async function listGoals(input) {
2693
2720
  }
2694
2721
 
2695
2722
  // src/tools/audit.ts
2696
- import { z as z15 } from "zod";
2697
- var listEmployeesSchema = z15.object({
2698
- partyId: z15.number().int().positive().describe(
2723
+ import { z as z18 } from "zod";
2724
+ var listEmployeesSchema = z18.object({
2725
+ partyId: positiveId.describe(
2699
2726
  "The organisation's party id. Returns the people whose `organisation` field links to this party."
2700
2727
  ),
2701
- page: z15.number().int().positive().optional().default(1),
2702
- perPage: z15.number().int().min(1).max(100).optional().default(25),
2703
- embed: z15.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
2728
+ page: z18.number().int().positive().optional().default(1),
2729
+ perPage: z18.number().int().min(1).max(100).optional().default(25),
2730
+ embed: z18.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
2704
2731
  });
2705
2732
  async function listEmployees(input) {
2706
2733
  const { data, nextPage } = await capsuleGet(
@@ -2709,15 +2736,15 @@ async function listEmployees(input) {
2709
2736
  );
2710
2737
  return { ...data, nextPage };
2711
2738
  }
2712
- var DeletedSinceSchema = z15.string().describe(
2739
+ var DeletedSinceSchema = z18.string().describe(
2713
2740
  "REQUIRED. ISO-8601 timestamp; only deletions on or after this point are returned. Example: '2026-01-01T00:00:00Z'."
2714
2741
  );
2715
2742
  var DeletedPagination = {
2716
2743
  since: DeletedSinceSchema,
2717
- page: z15.number().int().positive().optional().default(1),
2718
- perPage: z15.number().int().min(1).max(100).optional().default(25)
2744
+ page: z18.number().int().positive().optional().default(1),
2745
+ perPage: z18.number().int().min(1).max(100).optional().default(25)
2719
2746
  };
2720
- var listDeletedPartiesSchema = z15.object(DeletedPagination);
2747
+ var listDeletedPartiesSchema = z18.object(DeletedPagination);
2721
2748
  async function listDeletedParties(input) {
2722
2749
  const { data, nextPage } = await capsuleGet("/parties/deleted", {
2723
2750
  since: input.since,
@@ -2726,7 +2753,7 @@ async function listDeletedParties(input) {
2726
2753
  });
2727
2754
  return { ...data, nextPage };
2728
2755
  }
2729
- var listDeletedOpportunitiesSchema = z15.object(DeletedPagination);
2756
+ var listDeletedOpportunitiesSchema = z18.object(DeletedPagination);
2730
2757
  async function listDeletedOpportunities(input) {
2731
2758
  const { data, nextPage } = await capsuleGet("/opportunities/deleted", {
2732
2759
  since: input.since,
@@ -2735,7 +2762,7 @@ async function listDeletedOpportunities(input) {
2735
2762
  });
2736
2763
  return { ...data, nextPage };
2737
2764
  }
2738
- var listDeletedProjectsSchema = z15.object(DeletedPagination);
2765
+ var listDeletedProjectsSchema = z18.object(DeletedPagination);
2739
2766
  async function listDeletedProjects(input) {
2740
2767
  const { data, nextPage } = await capsuleGet("/kases/deleted", {
2741
2768
  since: input.since,
@@ -2746,14 +2773,14 @@ async function listDeletedProjects(input) {
2746
2773
  }
2747
2774
 
2748
2775
  // src/tools/relationships.ts
2749
- import { z as z16 } from "zod";
2750
- var RelationshipEntity = z16.enum(["opportunities", "kases"]).describe("Which entity has the additional-party links. Use 'kases' for projects.");
2751
- var listAdditionalPartiesSchema = z16.object({
2776
+ import { z as z19 } from "zod";
2777
+ var RelationshipEntity = z19.enum(["opportunities", "kases"]).describe("Which entity has the additional-party links. Use 'kases' for projects.");
2778
+ var listAdditionalPartiesSchema = z19.object({
2752
2779
  entity: RelationshipEntity,
2753
- entityId: z16.number().int().positive().describe("ID of the opportunity or project."),
2754
- embed: z16.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
2755
- page: z16.number().int().positive().optional().default(1),
2756
- perPage: z16.number().int().min(1).max(100).optional().default(25)
2780
+ entityId: positiveId.describe("ID of the opportunity or project."),
2781
+ embed: z19.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
2782
+ page: z19.number().int().positive().optional().default(1),
2783
+ perPage: z19.number().int().min(1).max(100).optional().default(25)
2757
2784
  });
2758
2785
  async function listAdditionalParties(input) {
2759
2786
  const { data, nextPage } = await capsuleGet(
@@ -2762,10 +2789,12 @@ async function listAdditionalParties(input) {
2762
2789
  );
2763
2790
  return { ...data, nextPage };
2764
2791
  }
2765
- var addAdditionalPartySchema = z16.object({
2792
+ var addAdditionalPartySchema = z19.object({
2766
2793
  entity: RelationshipEntity,
2767
- entityId: z16.number().int().positive(),
2768
- partyId: z16.number().int().positive().describe("ID of the party (person or organisation) to link as an additional party.")
2794
+ entityId: positiveId,
2795
+ partyId: positiveId.describe(
2796
+ "ID of the party (person or organisation) to link as an additional party."
2797
+ )
2769
2798
  });
2770
2799
  async function addAdditionalParty(input) {
2771
2800
  try {
@@ -2793,10 +2822,10 @@ async function addAdditionalParty(input) {
2793
2822
  throw err;
2794
2823
  }
2795
2824
  }
2796
- var removeAdditionalPartySchema = z16.object({
2825
+ var removeAdditionalPartySchema = z19.object({
2797
2826
  entity: RelationshipEntity,
2798
- entityId: z16.number().int().positive(),
2799
- partyId: z16.number().int().positive(),
2827
+ entityId: positiveId,
2828
+ partyId: positiveId,
2800
2829
  confirm: confirmFlag().describe(
2801
2830
  "Must be set to true. Removes the link between the entity and the additional party. The party itself is not deleted. Reversible by re-adding the link."
2802
2831
  )
@@ -2823,11 +2852,11 @@ async function removeAdditionalParty(input) {
2823
2852
  })
2824
2853
  );
2825
2854
  }
2826
- var listAssociatedProjectsSchema = z16.object({
2827
- opportunityId: z16.number().int().positive(),
2828
- embed: z16.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
2829
- page: z16.number().int().positive().optional().default(1),
2830
- perPage: z16.number().int().min(1).max(100).optional().default(25)
2855
+ var listAssociatedProjectsSchema = z19.object({
2856
+ opportunityId: positiveId,
2857
+ embed: z19.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
2858
+ page: z19.number().int().positive().optional().default(1),
2859
+ perPage: z19.number().int().min(1).max(100).optional().default(25)
2831
2860
  });
2832
2861
  async function listAssociatedProjects(input) {
2833
2862
  const { data, nextPage } = await capsuleGet(
@@ -2838,9 +2867,9 @@ async function listAssociatedProjects(input) {
2838
2867
  }
2839
2868
 
2840
2869
  // src/tools/custom-fields.ts
2841
- import { z as z17 } from "zod";
2842
- var CustomFieldEntity = z17.enum(["parties", "opportunities", "kases"]).describe("Which entity type's custom field schema to inspect. Use 'kases' for projects.");
2843
- var listCustomFieldsSchema = z17.object({
2870
+ import { z as z20 } from "zod";
2871
+ var CustomFieldEntity = z20.enum(["parties", "opportunities", "kases"]).describe("Which entity type's custom field schema to inspect. Use 'kases' for projects.");
2872
+ var listCustomFieldsSchema = z20.object({
2844
2873
  entity: CustomFieldEntity
2845
2874
  });
2846
2875
  async function listCustomFields(input) {
@@ -2849,9 +2878,9 @@ async function listCustomFields(input) {
2849
2878
  );
2850
2879
  return data;
2851
2880
  }
2852
- var getCustomFieldSchema = z17.object({
2881
+ var getCustomFieldSchema = z20.object({
2853
2882
  entity: CustomFieldEntity,
2854
- fieldId: z17.number().int().positive().describe("Custom field definition id.")
2883
+ fieldId: positiveId.describe("Custom field definition id.")
2855
2884
  });
2856
2885
  async function getCustomField(input) {
2857
2886
  const { data } = await capsuleGetCached(
@@ -2861,11 +2890,11 @@ async function getCustomField(input) {
2861
2890
  }
2862
2891
 
2863
2892
  // src/tools/tracks.ts
2864
- import { z as z18 } from "zod";
2865
- var TrackEntity = z18.enum(["parties", "opportunities", "kases"]).describe("Use 'kases' for projects.");
2866
- var listEntityTracksSchema = z18.object({
2893
+ import { z as z21 } from "zod";
2894
+ var TrackEntity = z21.enum(["parties", "opportunities", "kases"]).describe("Use 'kases' for projects.");
2895
+ var listEntityTracksSchema = z21.object({
2867
2896
  entity: TrackEntity,
2868
- entityId: z18.number().int().positive()
2897
+ entityId: positiveId
2869
2898
  });
2870
2899
  async function listEntityTracks(input) {
2871
2900
  const { data } = await capsuleGet(
@@ -2873,20 +2902,20 @@ async function listEntityTracks(input) {
2873
2902
  );
2874
2903
  return data;
2875
2904
  }
2876
- var showTrackSchema = z18.object({
2877
- trackId: z18.number().int().positive()
2905
+ var showTrackSchema = z21.object({
2906
+ trackId: positiveId
2878
2907
  });
2879
2908
  async function showTrack(input) {
2880
2909
  const { data } = await capsuleGet(`/tracks/${input.trackId}`);
2881
2910
  return data;
2882
2911
  }
2883
- var applyTrackSchema = z18.object({
2884
- entity: z18.enum(["opportunities", "kases"]).describe("Which entity to apply the track to. Use 'kases' for projects."),
2885
- entityId: z18.number().int().positive(),
2886
- trackDefinitionId: z18.number().int().positive().describe(
2912
+ var applyTrackSchema = z21.object({
2913
+ entity: z21.enum(["opportunities", "kases"]).describe("Which entity to apply the track to. Use 'kases' for projects."),
2914
+ entityId: positiveId,
2915
+ trackDefinitionId: positiveId.describe(
2887
2916
  "The trackDefinition to apply (from list_track_definitions). Auto-creates task definitions on the target entity per the track's rules."
2888
2917
  ),
2889
- startDate: z18.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe(
2918
+ startDate: z21.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe(
2890
2919
  "Optional ISO-8601 date (YYYY-MM-DD) the track should start from \u2014 drives task due-date calculations (each task's `dueOn` is computed as startDate + the track-definition's `daysAfter` offset). Defaults to today if omitted. Useful for scheduling a renewal-queue track against a future contract end-date, or backfilling tracks for historical projects."
2891
2920
  )
2892
2921
  });
@@ -2899,9 +2928,9 @@ async function applyTrack(input) {
2899
2928
  if (input.startDate !== void 0) track["trackDateOn"] = input.startDate;
2900
2929
  return capsulePost("/tracks", { track });
2901
2930
  }
2902
- var updateTrackSchema = z18.object({
2903
- trackId: z18.number().int().positive(),
2904
- fields: z18.record(z18.string(), z18.unknown()).describe(
2931
+ var updateTrackSchema = z21.object({
2932
+ trackId: positiveId,
2933
+ fields: z21.record(z21.string(), z21.unknown()).describe(
2905
2934
  "Object of fields to update on the track. Capsule's PUT semantics are partial \u2014 only the fields you provide are changed. Common: { complete: true } to mark a track completed. Capsule rejects unknown keys; consult Capsule's docs for the full updatable set."
2906
2935
  )
2907
2936
  });
@@ -2913,8 +2942,8 @@ async function updateTrack(input) {
2913
2942
  track: input.fields
2914
2943
  });
2915
2944
  }
2916
- var removeTrackSchema = z18.object({
2917
- trackId: z18.number().int().positive(),
2945
+ var removeTrackSchema = z21.object({
2946
+ trackId: positiveId,
2918
2947
  confirm: confirmFlag().describe(
2919
2948
  "Must be set to true. Removes the track instance from its entity. **Capsule also deletes the auto-tasks the track created when it was applied** \u2014 they go with the track and become unreachable (404 on GET /tasks/{id}, gone from list_tasks on the parent entity). If you need any of those tasks to outlive the track, copy their content into fresh tasks (or use the web UI) before calling remove_track."
2920
2949
  )
@@ -2931,13 +2960,13 @@ async function removeTrack(input) {
2931
2960
  }
2932
2961
 
2933
2962
  // src/tools/attachments.ts
2934
- import { z as z19 } from "zod";
2963
+ import { z as z22 } from "zod";
2935
2964
  var DEFAULT_MAX_SIZE_BYTES = 5 * 1024 * 1024;
2936
2965
  var HARD_MAX_SIZE_BYTES = 25 * 1024 * 1024;
2937
2966
  var HARD_MAX_BASE64_CHARS = Math.ceil(HARD_MAX_SIZE_BYTES / 3) * 4;
2938
- var getAttachmentSchema = z19.object({
2939
- id: z19.number().int().positive().describe("Attachment ID."),
2940
- maxSizeBytes: z19.number().int().positive().max(HARD_MAX_SIZE_BYTES).optional().describe(
2967
+ var getAttachmentSchema = z22.object({
2968
+ id: positiveId.describe("Attachment ID."),
2969
+ maxSizeBytes: z22.number().int().positive().max(HARD_MAX_SIZE_BYTES).optional().describe(
2941
2970
  `Refuse to return content over this size (default ${DEFAULT_MAX_SIZE_BYTES} bytes \u2248 5MB; max ${HARD_MAX_SIZE_BYTES} bytes \u2248 25MB). Files exceeding the cap return metadata only with a 'truncated: true' flag.`
2942
2971
  )
2943
2972
  });
@@ -2952,22 +2981,22 @@ async function getAttachment(input) {
2952
2981
  }
2953
2982
  return { contentType, buffer, sizeBytes };
2954
2983
  }
2955
- var uploadAttachmentSchema = z19.object({
2956
- filename: z19.string().min(1).describe(
2984
+ var uploadAttachmentSchema = z22.object({
2985
+ filename: z22.string().min(1).describe(
2957
2986
  "Filename Capsule should record (e.g. 'contract.pdf'). Capsule does NOT validate consistency between filename, contentType, and the actual bytes \u2014 a typo in either is accepted and the file is stored as labelled."
2958
2987
  ),
2959
- contentType: z19.string().min(1).describe(
2988
+ contentType: z22.string().min(1).describe(
2960
2989
  "MIME type of the file (e.g. 'application/pdf', 'image/png', 'text/plain'). Trusted by Capsule verbatim; not cross-checked against `filename` or the actual bytes."
2961
2990
  ),
2962
- dataBase64: z19.string().min(1).max(HARD_MAX_BASE64_CHARS).describe(
2991
+ dataBase64: z22.string().min(1).max(HARD_MAX_BASE64_CHARS).describe(
2963
2992
  "File contents, base64-encoded. Decoded server-side and uploaded as the request body. Maximum 25 MB per attachment (Capsule's documented limit); the connector rejects oversized base64 before uploading. The inbound HTTP body limit is ~35 MB which leaves room for the base64 expansion of a 25 MB binary."
2964
2993
  ),
2965
- content: z19.string().optional().describe(
2994
+ content: z22.string().optional().describe(
2966
2995
  "Body text for the note that will hold the attachment. Defaults to '[attachment]' if omitted."
2967
2996
  ),
2968
- partyId: z19.number().int().positive().optional().describe("Link the new note to a party (mutually exclusive with opportunityId / projectId)."),
2969
- opportunityId: z19.number().int().positive().optional(),
2970
- projectId: z19.number().int().positive().optional()
2997
+ partyId: positiveId.optional().describe("Link the new note to a party (mutually exclusive with opportunityId / projectId)."),
2998
+ opportunityId: positiveId.optional(),
2999
+ projectId: positiveId.optional()
2971
3000
  });
2972
3001
  function isValidBase64(s) {
2973
3002
  if (!/^[A-Za-z0-9+/]*={0,2}$/.test(s)) return false;
@@ -3017,23 +3046,23 @@ async function uploadAttachment(input) {
3017
3046
  }
3018
3047
 
3019
3048
  // src/tools/saved-filters.ts
3020
- import { z as z20 } from "zod";
3021
- var EntitySchema = z20.enum(["parties", "opportunities", "kases"]).describe(
3049
+ import { z as z23 } from "zod";
3050
+ var EntitySchema = z23.enum(["parties", "opportunities", "kases"]).describe(
3022
3051
  "Which entity type the filter operates over. Use 'kases' for projects (Capsule's legacy name)."
3023
3052
  );
3024
- var listSavedFiltersSchema = z20.object({
3053
+ var listSavedFiltersSchema = z23.object({
3025
3054
  entity: EntitySchema
3026
3055
  });
3027
3056
  async function listSavedFilters(input) {
3028
3057
  const { data } = await capsuleGetCached(`/${input.entity}/filters`);
3029
3058
  return data;
3030
3059
  }
3031
- var runSavedFilterSchema = z20.object({
3060
+ var runSavedFilterSchema = z23.object({
3032
3061
  entity: EntitySchema,
3033
- id: z20.number().int().positive().describe("The saved filter id (from list_saved_filters)."),
3034
- embed: z20.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
3035
- page: z20.number().int().positive().optional().default(1),
3036
- perPage: z20.number().int().min(1).max(100).optional().default(25)
3062
+ id: positiveId.describe("The saved filter id (from list_saved_filters)."),
3063
+ embed: z23.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
3064
+ page: z23.number().int().positive().optional().default(1),
3065
+ perPage: z23.number().int().min(1).max(100).optional().default(25)
3037
3066
  });
3038
3067
  async function runSavedFilter(input) {
3039
3068
  const { data, nextPage } = await capsuleGet(
@@ -3051,7 +3080,7 @@ function createCapsuleMcpServer(opts) {
3051
3080
  const server = new McpServer(
3052
3081
  {
3053
3082
  name: "capsulemcp",
3054
- version: "1.6.0",
3083
+ version: "1.6.2",
3055
3084
  description: "Read and (optionally) modify Capsule CRM data \u2014 parties, opportunities, projects, tasks, timeline entries, pipelines, tags.",
3056
3085
  websiteUrl: "https://github.com/soil-dev/capsulemcp",
3057
3086
  icons: ICONS
@@ -3636,7 +3665,7 @@ function createCapsuleMcpServer(opts) {
3636
3665
  registerTool(
3637
3666
  server,
3638
3667
  "list_teams",
3639
- "List all teams configured in the Capsule account. Useful as input for filter_* queries that scope by team, and for reporting. LIMITATION: returns team identity only (id, name, description, timestamps). Capsule's v2 API does not expose team\u2194user membership through any endpoint \u2014 `GET /teams/{id}/users` 404s, `embed=users` is silently ignored, and `GET /users/{id}` doesn't include a `teams` field. To determine whether a given user belongs to a given team, either check Capsule's web UI Team Membership page, or probe via `update_project { ownerId: U, teamId: T }` and read the response \u2014 422 'owner is not a member of the team' means U \u2209 T.",
3668
+ "List all teams configured in the Capsule account. Useful as input for filter_* queries that scope by team, and for reporting. LIMITATION: returns team identity only (id, name, description, timestamps). Capsule's v2 API does not expose team\u2194user membership through any endpoint \u2014 `GET /teams/{id}/users` 404s, `embed=users` is silently ignored, and `GET /users/{id}` doesn't include a `teams` field. To determine whether a given user belongs to a given team, either check Capsule's web UI Team Membership page, or probe via `update_project { ownerId: U, teamId: T }` / `batch_update_opportunity { items: [{ id: <any opp>, ownerId: U, teamId: T }] }` and read the response \u2014 422 'owner is not a member of the team' means U \u2209 T. Both probe paths apply the same membership constraint server-side.",
3640
3669
  listTeamsSchema,
3641
3670
  listTeams
3642
3671
  );