capsulemcp 1.6.1 → 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
@@ -1374,18 +1374,19 @@ function registerToolTask(server, name, description, schema, handler) {
1374
1374
  }
1375
1375
 
1376
1376
  // src/tools/parties.ts
1377
- import { z as z4 } from "zod";
1377
+ import { z as z7 } from "zod";
1378
1378
 
1379
- // src/tools/descriptions.ts
1380
- var EMBED_TAGS_FIELDS_DESCRIPTION = "Comma-separated embeds, e.g. 'tags,fields'";
1381
- 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
+ }
1382
1387
 
1383
- // src/tools/confirm-flag.ts
1388
+ // src/tools/define-batch.ts
1384
1389
  import { z as z2 } from "zod";
1385
- var CONFIRM_REQUIRED_MESSAGE = "confirm: true is required to perform this destructive operation (set the parameter explicitly to acknowledge the destructive intent)";
1386
- function confirmFlag() {
1387
- return z2.literal(true, { error: () => CONFIRM_REQUIRED_MESSAGE });
1388
- }
1389
1390
 
1390
1391
  // src/capsule/batch.ts
1391
1392
  function chunk(arr, size) {
@@ -1479,6 +1480,39 @@ function topFailureReasons(results, n) {
1479
1480
  return Array.from(counts.values()).sort((a, b) => b.count - a.count).slice(0, n);
1480
1481
  }
1481
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
+
1482
1516
  // src/capsule/idempotent.ts
1483
1517
  var isCapsule404 = (err) => err instanceof CapsuleApiError && err.status === 404;
1484
1518
  var isCapsuleTagNotFound = (err) => err instanceof CapsuleApiError && err.status === 422 && /tag not found/i.test(err.message);
@@ -1501,13 +1535,33 @@ async function idempotentWithResult(op, success, alreadyDone, isAlreadyDoneError
1501
1535
  }
1502
1536
  }
1503
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
+
1504
1558
  // src/tools/custom-field-helpers.ts
1505
- import { z as z3 } from "zod";
1506
- var CustomFieldWriteSchema = z3.object({
1507
- definitionId: z3.number().int().positive().describe(
1559
+ import { z as z6 } from "zod";
1560
+ var CustomFieldWriteSchema = z6.object({
1561
+ definitionId: positiveId.describe(
1508
1562
  "The custom-field definition id from list_custom_fields. Identifies which field on the entity to set."
1509
1563
  ),
1510
- 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(
1511
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."
1512
1566
  )
1513
1567
  });
@@ -1523,24 +1577,24 @@ function mapFieldsForBody(fields) {
1523
1577
  }
1524
1578
 
1525
1579
  // src/tools/parties.ts
1526
- var EmailAddressSchema = z4.object({
1527
- address: z4.string().email(),
1528
- type: z4.string().optional()
1580
+ var EmailAddressSchema = z7.object({
1581
+ address: z7.string().email(),
1582
+ type: z7.string().optional()
1529
1583
  });
1530
- var PhoneNumberSchema = z4.object({
1584
+ var PhoneNumberSchema = z7.object({
1531
1585
  // Capsule rejects empty strings with `phoneNumber.number: number is
1532
1586
  // required`. Enforce at the schema layer to catch typos pre-call,
1533
1587
  // matching how EmailAddressSchema's address field behaves.
1534
- number: z4.string().min(1),
1535
- type: z4.string().optional()
1588
+ number: z7.string().min(1),
1589
+ type: z7.string().optional()
1536
1590
  });
1537
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.";
1538
- var AddressSchema = z4.object({
1539
- street: z4.string().optional(),
1540
- city: z4.string().optional(),
1541
- state: z4.string().optional(),
1542
- country: z4.string().optional().describe(CountryDescription),
1543
- 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()
1544
1598
  });
1545
1599
  function validateWebsiteAddress(data, ctx) {
1546
1600
  const isUrlService = data.service === void 0 || data.service === "URL";
@@ -1563,7 +1617,7 @@ function validateWebsiteAddress(data, ctx) {
1563
1617
  });
1564
1618
  }
1565
1619
  }
1566
- var WebsiteServiceEnum = z4.enum([
1620
+ var WebsiteServiceEnum = z7.enum([
1567
1621
  "URL",
1568
1622
  "SKYPE",
1569
1623
  "TWITTER",
@@ -1582,19 +1636,19 @@ var WebsiteServiceEnum = z4.enum([
1582
1636
  "BLUESKY",
1583
1637
  "SNAPCHAT"
1584
1638
  ]);
1585
- var WebsiteSchema = z4.object({
1586
- address: z4.string().min(1).describe(
1639
+ var WebsiteSchema = z7.object({
1640
+ address: z7.string().min(1).describe(
1587
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."
1588
1642
  ),
1589
1643
  service: WebsiteServiceEnum.optional().describe(
1590
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."
1591
1645
  )
1592
1646
  }).superRefine(validateWebsiteAddress);
1593
- var searchPartiesSchema = z4.object({
1594
- q: z4.string().optional().describe("Free-text search query"),
1595
- embed: z4.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
1596
- page: z4.number().int().positive().optional().default(1),
1597
- 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)
1598
1652
  });
1599
1653
  async function searchParties(input) {
1600
1654
  const path = input.q ? "/parties/search" : "/parties";
@@ -1606,9 +1660,9 @@ async function searchParties(input) {
1606
1660
  });
1607
1661
  return { ...data, nextPage };
1608
1662
  }
1609
- var getPartySchema = z4.object({
1610
- id: z4.number().int().positive().describe("Party ID"),
1611
- 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)
1612
1666
  });
1613
1667
  async function getParty(input) {
1614
1668
  const { data } = await capsuleGet(`/parties/${input.id}`, {
@@ -1616,11 +1670,11 @@ async function getParty(input) {
1616
1670
  });
1617
1671
  return data;
1618
1672
  }
1619
- var getPartiesSchema = z4.object({
1620
- 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(
1621
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."
1622
1676
  ),
1623
- embed: z4.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
1677
+ embed: z7.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
1624
1678
  });
1625
1679
  async function getParties(input) {
1626
1680
  const { ids, embed } = input;
@@ -1638,10 +1692,10 @@ async function getParties(input) {
1638
1692
  );
1639
1693
  return { parties: responses.flatMap((r) => r.data.parties) };
1640
1694
  }
1641
- var listPartyOpportunitiesSchema = z4.object({
1642
- partyId: z4.number().int().positive(),
1643
- page: z4.number().int().positive().optional().default(1),
1644
- 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)
1645
1699
  });
1646
1700
  async function listPartyOpportunities(input) {
1647
1701
  const { data, nextPage } = await capsuleGet(
@@ -1650,10 +1704,10 @@ async function listPartyOpportunities(input) {
1650
1704
  );
1651
1705
  return { ...data, nextPage };
1652
1706
  }
1653
- var listPartyProjectsSchema = z4.object({
1654
- partyId: z4.number().int().positive(),
1655
- page: z4.number().int().positive().optional().default(1),
1656
- 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)
1657
1711
  });
1658
1712
  async function listPartyProjects(input) {
1659
1713
  const { data, nextPage } = await capsuleGet(
@@ -1663,50 +1717,50 @@ async function listPartyProjects(input) {
1663
1717
  return { ...data, nextPage };
1664
1718
  }
1665
1719
  var PartyWriteBaseSchema = {
1666
- about: z4.string().optional(),
1667
- emailAddresses: z4.array(EmailAddressSchema).optional().describe(
1720
+ about: z7.string().optional(),
1721
+ emailAddresses: z7.array(EmailAddressSchema).optional().describe(
1668
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)."
1669
1723
  ),
1670
- phoneNumbers: z4.array(PhoneNumberSchema).optional().describe(
1724
+ phoneNumbers: z7.array(PhoneNumberSchema).optional().describe(
1671
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."
1672
1726
  ),
1673
- addresses: z4.array(AddressSchema).optional().describe(
1727
+ addresses: z7.array(AddressSchema).optional().describe(
1674
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)."
1675
1729
  ),
1676
- websites: z4.array(WebsiteSchema).optional().describe(
1730
+ websites: z7.array(WebsiteSchema).optional().describe(
1677
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."
1678
1732
  ),
1679
- ownerId: z4.number().int().positive().optional().describe(
1733
+ ownerId: positiveId.optional().describe(
1680
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."
1681
1735
  )
1682
1736
  };
1683
- var createPartySchema = z4.object({
1684
- type: z4.enum(["person", "organisation"]),
1737
+ var createPartySchema = z7.object({
1738
+ type: z7.enum(["person", "organisation"]),
1685
1739
  // person
1686
- firstName: z4.string().optional(),
1687
- lastName: z4.string().optional(),
1688
- title: z4.string().optional(),
1689
- jobTitle: z4.string().optional(),
1690
- 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"),
1691
1745
  // organisation
1692
- name: z4.string().optional(),
1746
+ name: z7.string().optional(),
1693
1747
  ...PartyWriteBaseSchema
1694
1748
  });
1695
1749
  async function createParty(input) {
1696
1750
  const { ownerId, organisationId, ...rest } = input;
1697
1751
  const body = { ...rest };
1698
- if (ownerId) body["owner"] = { id: ownerId };
1699
- if (organisationId) body["organisation"] = { id: organisationId };
1752
+ setRef(body, "owner", ownerId);
1753
+ setRef(body, "organisation", organisationId);
1700
1754
  return capsulePost("/parties", { party: body });
1701
1755
  }
1702
- var updatePartySchema = z4.object({
1703
- id: z4.number().int().positive(),
1704
- firstName: z4.string().optional(),
1705
- lastName: z4.string().optional(),
1706
- title: z4.string().optional(),
1707
- jobTitle: z4.string().optional(),
1708
- name: z4.string().optional(),
1709
- 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")),
1710
1764
  ...PartyWriteBaseSchema
1711
1765
  });
1712
1766
  async function updateParty(input) {
@@ -1715,39 +1769,26 @@ async function updateParty(input) {
1715
1769
  for (const [k, v] of Object.entries(rest)) {
1716
1770
  if (v !== void 0) body[k] = v;
1717
1771
  }
1718
- if (ownerId) body["owner"] = { id: ownerId };
1772
+ setRef(body, "owner", ownerId);
1719
1773
  const mappedFields = mapFieldsForBody(fields);
1720
1774
  if (mappedFields !== void 0) body["fields"] = mappedFields;
1721
1775
  return capsulePut(`/parties/${id}`, { party: body });
1722
1776
  }
1723
- var batchUpdatePartySchema = z4.object({
1724
- items: z4.array(updatePartySchema).min(1).max(50).describe(
1725
- "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)."
1726
- )
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
1727
1782
  });
1728
- async function batchUpdateParty(input, opts = {}) {
1729
- return batchExecute("batch_update_party", input.items, (item) => updateParty(item), opts);
1730
- }
1731
- var deletePartySchema = z4.object({
1732
- id: z4.number().int().positive(),
1733
- confirm: confirmFlag().describe(
1734
- "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."
1735
- )
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."
1736
1787
  });
1737
- async function deleteParty(input) {
1738
- if (input.confirm !== true) {
1739
- throw new Error("delete_party requires confirm: true");
1740
- }
1741
- return idempotent(
1742
- () => capsuleDelete(`/parties/${input.id}`),
1743
- () => ({ deleted: true, alreadyDeleted: false, id: input.id }),
1744
- () => ({ deleted: true, alreadyDeleted: true, id: input.id })
1745
- );
1746
- }
1747
- var addPartyEmailAddressSchema = z4.object({
1748
- partyId: z4.number().int().positive(),
1749
- address: z4.string().email(),
1750
- 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'.")
1751
1792
  });
1752
1793
  async function addPartyEmailAddress(input) {
1753
1794
  const { partyId, address, type } = input;
@@ -1757,9 +1798,9 @@ async function addPartyEmailAddress(input) {
1757
1798
  party: { emailAddresses: [item] }
1758
1799
  });
1759
1800
  }
1760
- var removePartyEmailAddressByIdSchema = z4.object({
1761
- partyId: z4.number().int().positive(),
1762
- emailAddressId: z4.number().int().positive().describe(
1801
+ var removePartyEmailAddressByIdSchema = z7.object({
1802
+ partyId: positiveId,
1803
+ emailAddressId: positiveId.describe(
1763
1804
  "Capsule's id for the email-address row. Read it from get_party (each entry in emailAddresses carries an id)."
1764
1805
  )
1765
1806
  });
@@ -1779,10 +1820,10 @@ async function removePartyEmailAddressById(input) {
1779
1820
  () => ({ removed: true, alreadyRemoved: true, partyId, emailAddressId })
1780
1821
  );
1781
1822
  }
1782
- var addPartyPhoneNumberSchema = z4.object({
1783
- partyId: z4.number().int().positive(),
1784
- number: z4.string().min(1),
1785
- 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'.")
1786
1827
  });
1787
1828
  async function addPartyPhoneNumber(input) {
1788
1829
  const { partyId, number, type } = input;
@@ -1792,9 +1833,9 @@ async function addPartyPhoneNumber(input) {
1792
1833
  party: { phoneNumbers: [item] }
1793
1834
  });
1794
1835
  }
1795
- var removePartyPhoneNumberByIdSchema = z4.object({
1796
- partyId: z4.number().int().positive(),
1797
- phoneNumberId: z4.number().int().positive().describe(
1836
+ var removePartyPhoneNumberByIdSchema = z7.object({
1837
+ partyId: positiveId,
1838
+ phoneNumberId: positiveId.describe(
1798
1839
  "Capsule's id for the phone-number row. Read it from get_party (each entry in phoneNumbers carries an id)."
1799
1840
  )
1800
1841
  });
@@ -1814,14 +1855,14 @@ async function removePartyPhoneNumberById(input) {
1814
1855
  () => ({ removed: true, alreadyRemoved: true, partyId, phoneNumberId })
1815
1856
  );
1816
1857
  }
1817
- var addPartyAddressSchema = z4.object({
1818
- partyId: z4.number().int().positive(),
1819
- street: z4.string().optional(),
1820
- city: z4.string().optional(),
1821
- state: z4.string().optional(),
1822
- country: z4.string().optional().describe(CountryDescription),
1823
- zip: z4.string().optional(),
1824
- 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'.")
1825
1866
  });
1826
1867
  async function addPartyAddress(input) {
1827
1868
  const { partyId, ...rest } = input;
@@ -1833,9 +1874,9 @@ async function addPartyAddress(input) {
1833
1874
  party: { addresses: [item] }
1834
1875
  });
1835
1876
  }
1836
- var removePartyAddressByIdSchema = z4.object({
1837
- partyId: z4.number().int().positive(),
1838
- addressId: z4.number().int().positive().describe(
1877
+ var removePartyAddressByIdSchema = z7.object({
1878
+ partyId: positiveId,
1879
+ addressId: positiveId.describe(
1839
1880
  "Capsule's id for the address row. Read it from get_party (each entry in addresses carries an id)."
1840
1881
  )
1841
1882
  });
@@ -1855,9 +1896,9 @@ async function removePartyAddressById(input) {
1855
1896
  () => ({ removed: true, alreadyRemoved: true, partyId, addressId })
1856
1897
  );
1857
1898
  }
1858
- var addPartyWebsiteSchema = z4.object({
1859
- partyId: z4.number().int().positive(),
1860
- address: z4.string().min(1).describe(
1899
+ var addPartyWebsiteSchema = z7.object({
1900
+ partyId: positiveId,
1901
+ address: z7.string().min(1).describe(
1861
1902
  "The website address. A URL when service='URL', or a handle (e.g. '@acmeco') for social services."
1862
1903
  ),
1863
1904
  service: WebsiteServiceEnum.optional().describe("Defaults to 'URL' if omitted.")
@@ -1870,9 +1911,9 @@ async function addPartyWebsite(input) {
1870
1911
  party: { websites: [item] }
1871
1912
  });
1872
1913
  }
1873
- var removePartyWebsiteByIdSchema = z4.object({
1874
- partyId: z4.number().int().positive(),
1875
- websiteId: z4.number().int().positive().describe(
1914
+ var removePartyWebsiteByIdSchema = z7.object({
1915
+ partyId: positiveId,
1916
+ websiteId: positiveId.describe(
1876
1917
  "Capsule's id for the website row. Read it from get_party (each entry in websites carries an id)."
1877
1918
  )
1878
1919
  });
@@ -1894,20 +1935,32 @@ async function removePartyWebsiteById(input) {
1894
1935
  }
1895
1936
 
1896
1937
  // src/tools/opportunities.ts
1897
- import { z as z5 } from "zod";
1898
- var OpportunityValueSchema = z5.object({
1899
- amount: z5.number().nonnegative(),
1900
- 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({
1901
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
1902
1955
  }).length(3).describe(
1903
1956
  "ISO 4217 currency code (3 letters), e.g. 'GBP', 'USD', 'EUR'. Required when amount is set."
1904
1957
  )
1905
1958
  });
1906
- var searchOpportunitiesSchema = z5.object({
1907
- q: z5.string().optional().describe("Free-text search query"),
1908
- embed: z5.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
1909
- page: z5.number().int().positive().optional().default(1),
1910
- 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)
1911
1964
  });
1912
1965
  async function searchOpportunities(input) {
1913
1966
  const path = input.q ? "/opportunities/search" : "/opportunities";
@@ -1919,9 +1972,9 @@ async function searchOpportunities(input) {
1919
1972
  });
1920
1973
  return { ...data, nextPage };
1921
1974
  }
1922
- var getOpportunitySchema = z5.object({
1923
- id: z5.number().int().positive(),
1924
- 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)
1925
1978
  });
1926
1979
  async function getOpportunity(input) {
1927
1980
  const { data } = await capsuleGet(`/opportunities/${input.id}`, {
@@ -1929,11 +1982,11 @@ async function getOpportunity(input) {
1929
1982
  });
1930
1983
  return data;
1931
1984
  }
1932
- var getOpportunitiesSchema = z5.object({
1933
- 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(
1934
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."
1935
1988
  ),
1936
- embed: z5.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
1989
+ embed: z8.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
1937
1990
  });
1938
1991
  async function getOpportunities(input) {
1939
1992
  const { ids, embed } = input;
@@ -1954,20 +2007,20 @@ async function getOpportunities(input) {
1954
2007
  );
1955
2008
  return { opportunities: responses.flatMap((r) => r.data.opportunities) };
1956
2009
  }
1957
- var createOpportunitySchema = z5.object({
1958
- name: z5.string().min(1),
1959
- partyId: z5.number().int().positive().describe("ID of the party this opportunity belongs to"),
1960
- milestoneId: z5.number().int().positive().describe(
1961
- "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."
1962
2015
  ),
1963
- description: z5.string().optional(),
2016
+ description: z8.string().optional(),
1964
2017
  value: OpportunityValueSchema.optional(),
1965
- expectedCloseOn: z5.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("YYYY-MM-DD"),
1966
- probability: z5.number().int().min(0).max(100).optional(),
1967
- ownerId: z5.number().int().positive().optional().describe(
1968
- "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."
1969
2022
  ),
1970
- teamId: z5.number().int().positive().optional().describe(
2023
+ teamId: positiveId.optional().describe(
1971
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)."
1972
2025
  )
1973
2026
  });
@@ -1978,32 +2031,32 @@ async function createOpportunity(input) {
1978
2031
  party: { id: partyId },
1979
2032
  milestone: { id: milestoneId }
1980
2033
  };
1981
- if (ownerId) body["owner"] = { id: ownerId };
1982
- if (teamId) body["team"] = { id: teamId };
2034
+ setRef(body, "owner", ownerId);
2035
+ setRef(body, "team", teamId);
1983
2036
  return capsulePost("/opportunities", { opportunity: body });
1984
2037
  }
1985
- var updateOpportunitySchema = z5.object({
1986
- id: z5.number().int().positive(),
1987
- name: z5.string().min(1).optional(),
1988
- milestoneId: z5.number().int().positive().optional().describe(
1989
- "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."
1990
2043
  ),
1991
- description: z5.string().optional(),
2044
+ description: z8.string().optional(),
1992
2045
  value: OpportunityValueSchema.optional(),
1993
- expectedCloseOn: z5.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional(),
1994
- 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(
1995
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)."
1996
2049
  ),
1997
- lostReasonId: z5.number().int().positive().optional().describe(
2050
+ lostReasonId: positiveId.optional().describe(
1998
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."
1999
2052
  ),
2000
- ownerId: z5.number().int().positive().optional().describe(
2053
+ ownerId: positiveId.optional().describe(
2001
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."
2002
2055
  ),
2003
- teamId: z5.number().int().positive().nullable().optional().describe(
2056
+ teamId: positiveId.nullable().optional().describe(
2004
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."
2005
2058
  ),
2006
- fields: z5.array(CustomFieldWriteSchema).optional().describe(fieldsArrayDescriptor("get_opportunity"))
2059
+ fields: z8.array(CustomFieldWriteSchema).optional().describe(fieldsArrayDescriptor("get_opportunity"))
2007
2060
  });
2008
2061
  async function updateOpportunity(input) {
2009
2062
  const { id, milestoneId, ownerId, teamId, lostReasonId, fields, ...rest } = input;
@@ -2011,59 +2064,39 @@ async function updateOpportunity(input) {
2011
2064
  for (const [k, v] of Object.entries(rest)) {
2012
2065
  if (v !== void 0) body[k] = v;
2013
2066
  }
2014
- if (milestoneId) body["milestone"] = { id: milestoneId };
2067
+ setRef(body, "milestone", milestoneId);
2015
2068
  let resolvedTeamId = teamId;
2016
2069
  if (ownerId !== void 0 && teamId === void 0) {
2017
- const { data } = await capsuleGet(`/opportunities/${id}`);
2018
- resolvedTeamId = data.opportunity?.team?.id ?? void 0;
2070
+ ({ teamId: resolvedTeamId } = await readEntityRefs(`/opportunities/${id}`, "opportunity"));
2019
2071
  }
2020
- if (ownerId) body["owner"] = { id: ownerId };
2021
- if (resolvedTeamId === null) body["team"] = null;
2022
- else if (resolvedTeamId !== void 0) body["team"] = { id: resolvedTeamId };
2023
- if (lostReasonId) body["lostReason"] = { id: lostReasonId };
2072
+ setRef(body, "owner", ownerId);
2073
+ setNullableRef(body, "team", resolvedTeamId);
2074
+ setRef(body, "lostReason", lostReasonId);
2024
2075
  const mappedFields = mapFieldsForBody(fields);
2025
2076
  if (mappedFields !== void 0) body["fields"] = mappedFields;
2026
2077
  return capsulePut(`/opportunities/${id}`, {
2027
2078
  opportunity: body
2028
2079
  });
2029
2080
  }
2030
- var batchUpdateOpportunitySchema = z5.object({
2031
- items: z5.array(updateOpportunitySchema).min(1).max(50).describe(
2032
- "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."
2033
- )
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
2034
2086
  });
2035
- async function batchUpdateOpportunity(input, opts = {}) {
2036
- return batchExecute(
2037
- "batch_update_opportunity",
2038
- input.items,
2039
- (item) => updateOpportunity(item),
2040
- opts
2041
- );
2042
- }
2043
- var deleteOpportunitySchema = z5.object({
2044
- id: z5.number().int().positive(),
2045
- confirm: confirmFlag().describe(
2046
- "Must be set to true. Permanently deletes the opportunity. Irreversible."
2047
- )
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."
2048
2091
  });
2049
- async function deleteOpportunity(input) {
2050
- if (input.confirm !== true) {
2051
- throw new Error("delete_opportunity requires confirm: true");
2052
- }
2053
- return idempotent(
2054
- () => capsuleDelete(`/opportunities/${input.id}`),
2055
- () => ({ deleted: true, alreadyDeleted: false, id: input.id }),
2056
- () => ({ deleted: true, alreadyDeleted: true, id: input.id })
2057
- );
2058
- }
2059
2092
 
2060
2093
  // src/tools/projects.ts
2061
- import { z as z6 } from "zod";
2062
- var listProjectsSchema = z6.object({
2063
- status: z6.enum(["OPEN", "CLOSED"]).optional(),
2064
- embed: z6.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
2065
- page: z6.number().int().positive().optional().default(1),
2066
- 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)
2067
2100
  });
2068
2101
  async function listProjects(input) {
2069
2102
  const { data, nextPage } = await capsuleGet("/kases", {
@@ -2074,9 +2107,9 @@ async function listProjects(input) {
2074
2107
  });
2075
2108
  return { ...data, nextPage };
2076
2109
  }
2077
- var getProjectSchema = z6.object({
2078
- id: z6.number().int().positive(),
2079
- 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)
2080
2113
  });
2081
2114
  async function getProject(input) {
2082
2115
  const { data } = await capsuleGet(`/kases/${input.id}`, {
@@ -2084,11 +2117,11 @@ async function getProject(input) {
2084
2117
  });
2085
2118
  return data;
2086
2119
  }
2087
- var getProjectsSchema = z6.object({
2088
- 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(
2089
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."
2090
2123
  ),
2091
- embed: z6.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
2124
+ embed: z9.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
2092
2125
  });
2093
2126
  async function getProjects(input) {
2094
2127
  const { ids, embed } = input;
@@ -2106,21 +2139,21 @@ async function getProjects(input) {
2106
2139
  );
2107
2140
  return { kases: responses.flatMap((r) => r.data.kases) };
2108
2141
  }
2109
- var createProjectSchema = z6.object({
2110
- name: z6.string().min(1),
2111
- partyId: z6.number().int().positive().describe("ID of the party linked to this project"),
2112
- description: z6.string().optional(),
2113
- status: z6.enum(["OPEN", "CLOSED"]).optional().describe("Defaults to OPEN when omitted."),
2114
- 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(
2115
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."
2116
2149
  ),
2117
- teamId: z6.number().int().positive().optional().describe(
2150
+ teamId: positiveId.optional().describe(
2118
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."
2119
2152
  ),
2120
- stageId: z6.number().int().positive().optional().describe(
2153
+ stageId: positiveId.optional().describe(
2121
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."
2122
2155
  ),
2123
- 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")
2124
2157
  });
2125
2158
  async function createProject(input) {
2126
2159
  const { partyId, ownerId, teamId, status, stageId, ...rest } = input;
@@ -2129,27 +2162,27 @@ async function createProject(input) {
2129
2162
  status: status ?? "OPEN",
2130
2163
  party: { id: partyId }
2131
2164
  };
2132
- if (ownerId) body["owner"] = { id: ownerId };
2133
- if (teamId) body["team"] = { id: teamId };
2165
+ setRef(body, "owner", ownerId);
2166
+ setRef(body, "team", teamId);
2134
2167
  if (stageId) body["stage"] = stageId;
2135
2168
  return capsulePost("/kases", { kase: body });
2136
2169
  }
2137
- var updateProjectSchema = z6.object({
2138
- id: z6.number().int().positive(),
2139
- name: z6.string().min(1).optional(),
2140
- description: z6.string().optional(),
2141
- status: z6.enum(["OPEN", "CLOSED"]).optional(),
2142
- 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(
2143
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)."
2144
2177
  ),
2145
- teamId: z6.number().int().positive().nullable().optional().describe(
2178
+ teamId: positiveId.nullable().optional().describe(
2146
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'."
2147
2180
  ),
2148
- stageId: z6.number().int().positive().optional().describe(
2181
+ stageId: positiveId.optional().describe(
2149
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."
2150
2183
  ),
2151
- expectedCloseOn: z6.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("YYYY-MM-DD"),
2152
- 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(
2153
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."
2154
2187
  )
2155
2188
  });
@@ -2162,55 +2195,38 @@ async function updateProject(input) {
2162
2195
  let resolvedTeamId = teamId;
2163
2196
  let resolvedStageId = stageId;
2164
2197
  if (ownerId !== void 0 && (teamId === void 0 || stageId === void 0)) {
2165
- const { data } = await capsuleGet(`/kases/${id}`);
2166
- if (teamId === void 0) {
2167
- resolvedTeamId = data.kase?.team?.id ?? void 0;
2168
- }
2169
- if (stageId === void 0) {
2170
- resolvedStageId = data.kase?.stage?.id ?? void 0;
2171
- }
2198
+ const current = await readEntityRefs(`/kases/${id}`, "kase");
2199
+ if (teamId === void 0) resolvedTeamId = current.teamId;
2200
+ if (stageId === void 0) resolvedStageId = current.stageId;
2172
2201
  }
2173
- if (ownerId === null) body["owner"] = null;
2174
- else if (ownerId !== void 0) body["owner"] = { id: ownerId };
2175
- if (resolvedTeamId === null) body["team"] = null;
2176
- else if (resolvedTeamId !== void 0) body["team"] = { id: resolvedTeamId };
2202
+ setNullableRef(body, "owner", ownerId);
2203
+ setNullableRef(body, "team", resolvedTeamId);
2177
2204
  if (resolvedStageId) body["stage"] = resolvedStageId;
2178
2205
  const mappedFields = mapFieldsForBody(fields);
2179
2206
  if (mappedFields !== void 0) body["fields"] = mappedFields;
2180
2207
  return capsulePut(`/kases/${id}`, { kase: body });
2181
2208
  }
2182
- var deleteProjectSchema = z6.object({
2183
- id: z6.number().int().positive(),
2184
- confirm: confirmFlag().describe(
2185
- "Must be set to true. Permanently deletes the project (case). Consider update_project status='CLOSED' instead. Irreversible."
2186
- )
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."
2187
2213
  });
2188
- async function deleteProject(input) {
2189
- if (input.confirm !== true) {
2190
- throw new Error("delete_project requires confirm: true");
2191
- }
2192
- return idempotent(
2193
- () => capsuleDelete(`/kases/${input.id}`),
2194
- () => ({ deleted: true, alreadyDeleted: false, id: input.id }),
2195
- () => ({ deleted: true, alreadyDeleted: true, id: input.id })
2196
- );
2197
- }
2198
2214
 
2199
2215
  // src/tools/tasks.ts
2200
- import { z as z7 } from "zod";
2201
- var listTasksSchema = z7.object({
2216
+ import { z as z10 } from "zod";
2217
+ var listTasksSchema = z10.object({
2202
2218
  // Note: Capsule has a third internal status `PENDING` (a task that's
2203
2219
  // part of an active track but not yet "open"), but it can only be
2204
2220
  // reached via track machinery — it is NOT directly settable by
2205
2221
  // /tasks PUT, and a list filter for it returns the same as OPEN
2206
2222
  // anyway. We expose only the two values that are actually filterable
2207
2223
  // by the v2 API.
2208
- status: z7.enum(["OPEN", "COMPLETED"]).optional().describe(
2224
+ status: z10.enum(["OPEN", "COMPLETED"]).optional().describe(
2209
2225
  "Defaults to OPEN when omitted. Pass COMPLETED to filter to completed tasks, or 'OPEN' explicitly."
2210
2226
  ),
2211
- ownerId: z7.number().int().positive().optional().describe("Filter to tasks owned by this user ID"),
2212
- page: z7.number().int().positive().optional().default(1),
2213
- 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)
2214
2230
  });
2215
2231
  async function listTasks(input) {
2216
2232
  const { data, nextPage } = await capsuleGet("/tasks", {
@@ -2224,15 +2240,15 @@ async function listTasks(input) {
2224
2240
  });
2225
2241
  return { ...data, nextPage };
2226
2242
  }
2227
- var getTaskSchema = z7.object({
2228
- id: z7.number().int().positive().describe("Task ID")
2243
+ var getTaskSchema = z10.object({
2244
+ id: positiveId.describe("Task ID")
2229
2245
  });
2230
2246
  async function getTask(input) {
2231
2247
  const { data } = await capsuleGet(`/tasks/${input.id}`);
2232
2248
  return data;
2233
2249
  }
2234
- var getTasksSchema = z7.object({
2235
- 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(
2236
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."
2237
2253
  )
2238
2254
  });
@@ -2248,17 +2264,17 @@ async function getTasks(input) {
2248
2264
  );
2249
2265
  return { tasks: responses.flatMap((r) => r.data.tasks) };
2250
2266
  }
2251
- var createTaskSchema = z7.object({
2252
- description: z7.string().min(1),
2253
- dueOn: z7.string().regex(/^\d{4}-\d{2}-\d{2}$/).describe("YYYY-MM-DD"),
2254
- dueTime: z7.string().regex(/^\d{2}:\d{2}$/).optional().describe("HH:MM in user's timezone"),
2255
- detail: z7.string().optional(),
2256
- 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(
2257
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."
2258
2274
  ),
2259
- partyId: z7.number().int().positive().optional().describe("Link task to a party (mutually exclusive with opportunityId/projectId)"),
2260
- opportunityId: z7.number().int().positive().optional().describe("Link task to an opportunity (mutually exclusive with partyId/projectId)"),
2261
- 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)")
2262
2278
  });
2263
2279
  async function createTask(input) {
2264
2280
  const linked = [input.partyId, input.opportunityId, input.projectId].filter(Boolean);
@@ -2267,25 +2283,25 @@ async function createTask(input) {
2267
2283
  }
2268
2284
  const { ownerId, partyId, opportunityId, projectId, ...rest } = input;
2269
2285
  const body = { ...rest };
2270
- if (ownerId) body["owner"] = { id: ownerId };
2271
- if (partyId) body["party"] = { id: partyId };
2272
- if (opportunityId) body["opportunity"] = { id: opportunityId };
2273
- 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);
2274
2290
  return capsulePost("/tasks", { task: body });
2275
2291
  }
2276
- var updateTaskSchema = z7.object({
2277
- id: z7.number().int().positive(),
2278
- description: z7.string().min(1).optional(),
2279
- dueOn: z7.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("YYYY-MM-DD"),
2280
- dueTime: z7.string().regex(/^\d{2}:\d{2}$/).optional().describe("HH:MM in user's timezone"),
2281
- 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(),
2282
2298
  // Capsule rejects direct sets of `PENDING` (which is a track-machinery
2283
2299
  // internal state) with 422 "cannot set task status to PENDING".
2284
2300
  // Only OPEN and COMPLETED are settable here.
2285
- status: z7.enum(["OPEN", "COMPLETED"]).optional().describe(
2301
+ status: z10.enum(["OPEN", "COMPLETED"]).optional().describe(
2286
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)."
2287
2303
  ),
2288
- ownerId: z7.number().int().positive().optional().describe(
2304
+ ownerId: positiveId.optional().describe(
2289
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."
2290
2306
  )
2291
2307
  });
@@ -2295,51 +2311,40 @@ async function updateTask(input) {
2295
2311
  for (const [k, v] of Object.entries(rest)) {
2296
2312
  if (v !== void 0) body[k] = v;
2297
2313
  }
2298
- if (ownerId) body["owner"] = { id: ownerId };
2314
+ setRef(body, "owner", ownerId);
2299
2315
  return capsulePut(`/tasks/${id}`, { task: body });
2300
2316
  }
2301
- var completeTaskSchema = z7.object({
2302
- id: z7.number().int().positive()
2317
+ var completeTaskSchema = z10.object({
2318
+ id: positiveId
2303
2319
  });
2304
2320
  async function completeTask(input) {
2305
2321
  return capsulePut(`/tasks/${input.id}`, {
2306
2322
  task: { status: "COMPLETED" }
2307
2323
  });
2308
2324
  }
2309
- var batchCompleteTaskSchema = z7.object({
2310
- 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(
2311
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."
2312
2328
  )
2313
2329
  });
2314
2330
  async function batchCompleteTask(input, opts = {}) {
2315
2331
  return batchExecute("batch_complete_task", input.ids, (id) => completeTask({ id }), opts);
2316
2332
  }
2317
- var deleteTaskSchema = z7.object({
2318
- id: z7.number().int().positive(),
2319
- confirm: confirmFlag().describe(
2320
- "Must be set to true. Permanently deletes the task. To mark done without losing history use complete_task. Irreversible."
2321
- )
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."
2322
2337
  });
2323
- async function deleteTask(input) {
2324
- if (input.confirm !== true) {
2325
- throw new Error("delete_task requires confirm: true");
2326
- }
2327
- return idempotent(
2328
- () => capsuleDelete(`/tasks/${input.id}`),
2329
- () => ({ deleted: true, alreadyDeleted: false, id: input.id }),
2330
- () => ({ deleted: true, alreadyDeleted: true, id: input.id })
2331
- );
2332
- }
2333
2338
 
2334
2339
  // src/tools/entries.ts
2335
- import { z as z8 } from "zod";
2340
+ import { z as z11 } from "zod";
2336
2341
  var listEntriesPagination = {
2337
- page: z8.number().int().positive().optional().default(1),
2338
- perPage: z8.number().int().min(1).max(100).optional().default(25),
2339
- 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)
2340
2345
  };
2341
- var listPartyEntriesSchema = z8.object({
2342
- partyId: z8.number().int().positive(),
2346
+ var listPartyEntriesSchema = z11.object({
2347
+ partyId: positiveId,
2343
2348
  ...listEntriesPagination
2344
2349
  });
2345
2350
  async function listPartyEntries(input) {
@@ -2349,8 +2354,8 @@ async function listPartyEntries(input) {
2349
2354
  );
2350
2355
  return { ...data, nextPage };
2351
2356
  }
2352
- var listOpportunityEntriesSchema = z8.object({
2353
- opportunityId: z8.number().int().positive(),
2357
+ var listOpportunityEntriesSchema = z11.object({
2358
+ opportunityId: positiveId,
2354
2359
  ...listEntriesPagination
2355
2360
  });
2356
2361
  async function listOpportunityEntries(input) {
@@ -2360,8 +2365,8 @@ async function listOpportunityEntries(input) {
2360
2365
  );
2361
2366
  return { ...data, nextPage };
2362
2367
  }
2363
- var listProjectEntriesSchema = z8.object({
2364
- projectId: z8.number().int().positive(),
2368
+ var listProjectEntriesSchema = z11.object({
2369
+ projectId: positiveId,
2365
2370
  ...listEntriesPagination
2366
2371
  });
2367
2372
  async function listProjectEntries(input) {
@@ -2371,9 +2376,9 @@ async function listProjectEntries(input) {
2371
2376
  );
2372
2377
  return { ...data, nextPage };
2373
2378
  }
2374
- var getEntrySchema = z8.object({
2375
- id: z8.number().int().positive(),
2376
- 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)
2377
2382
  });
2378
2383
  async function getEntry(input) {
2379
2384
  const { data } = await capsuleGet(`/entries/${input.id}`, {
@@ -2381,7 +2386,7 @@ async function getEntry(input) {
2381
2386
  });
2382
2387
  return data;
2383
2388
  }
2384
- var listEntriesSchema = z8.object({
2389
+ var listEntriesSchema = z11.object({
2385
2390
  ...listEntriesPagination
2386
2391
  });
2387
2392
  async function listEntries(input) {
@@ -2392,14 +2397,14 @@ async function listEntries(input) {
2392
2397
  });
2393
2398
  return { ...data, nextPage };
2394
2399
  }
2395
- var addNoteSchema = z8.object({
2396
- content: z8.string().min(1).describe(
2400
+ var addNoteSchema = z11.object({
2401
+ content: z11.string().min(1).describe(
2397
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."
2398
2403
  ),
2399
- partyId: z8.number().int().positive().optional().describe("Link note to a party (mutually exclusive with opportunityId/projectId)"),
2400
- opportunityId: z8.number().int().positive().optional().describe("Link note to an opportunity (mutually exclusive with partyId/projectId)"),
2401
- projectId: z8.number().int().positive().optional().describe("Link note to a project (mutually exclusive with partyId/opportunityId)"),
2402
- 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(
2403
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)."
2404
2409
  )
2405
2410
  });
@@ -2410,18 +2415,18 @@ async function addNote(input) {
2410
2415
  throw new Error("Provide exactly one of partyId, opportunityId, or projectId");
2411
2416
  }
2412
2417
  const body = { type: "note", content };
2413
- if (partyId) body["party"] = { id: partyId };
2414
- if (opportunityId) body["opportunity"] = { id: opportunityId };
2415
- if (projectId) body["kase"] = { id: projectId };
2418
+ setRef(body, "party", partyId);
2419
+ setRef(body, "opportunity", opportunityId);
2420
+ setRef(body, "kase", projectId);
2416
2421
  if (entryAt !== void 0) body["entryAt"] = entryAt;
2417
2422
  return capsulePost("/entries", { entry: body });
2418
2423
  }
2419
- var updateEntrySchema = z8.object({
2420
- id: z8.number().int().positive().describe("Entry ID to update"),
2421
- 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(
2422
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."
2423
2428
  ),
2424
- subject: z8.string().optional().describe(
2429
+ subject: z11.string().optional().describe(
2425
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`."
2426
2431
  )
2427
2432
  });
@@ -2435,30 +2440,20 @@ async function updateEntry(input) {
2435
2440
  }
2436
2441
  return capsulePut(`/entries/${id}`, { entry: body });
2437
2442
  }
2438
- var deleteEntrySchema = z8.object({
2439
- id: z8.number().int().positive().describe("Entry (note/email/task-record) ID"),
2440
- confirm: confirmFlag().describe(
2441
- "Must be set to true. Permanently deletes the entry \u2014 use this to remove a note from a party/opportunity/project. Irreversible."
2442
- )
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"
2443
2448
  });
2444
- async function deleteEntry(input) {
2445
- if (input.confirm !== true) {
2446
- throw new Error("delete_entry requires confirm: true");
2447
- }
2448
- return idempotent(
2449
- () => capsuleDelete(`/entries/${input.id}`),
2450
- () => ({ deleted: true, alreadyDeleted: false, id: input.id }),
2451
- () => ({ deleted: true, alreadyDeleted: true, id: input.id })
2452
- );
2453
- }
2454
2449
 
2455
2450
  // src/tools/pipelines.ts
2456
- import { z as z9 } from "zod";
2451
+ import { z as z12 } from "zod";
2457
2452
  var paginationFields = {
2458
- page: z9.number().int().positive().optional(),
2459
- 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()
2460
2455
  };
2461
- var listPipelinesSchema = z9.object({ ...paginationFields });
2456
+ var listPipelinesSchema = z12.object({ ...paginationFields });
2462
2457
  async function listPipelines(input) {
2463
2458
  const { data, nextPage } = await capsuleGetCached("/pipelines", {
2464
2459
  page: input.page ?? 1,
@@ -2466,8 +2461,8 @@ async function listPipelines(input) {
2466
2461
  });
2467
2462
  return { ...data, nextPage };
2468
2463
  }
2469
- var listMilestonesSchema = z9.object({
2470
- pipelineId: z9.number().int().positive(),
2464
+ var listMilestonesSchema = z12.object({
2465
+ pipelineId: positiveId,
2471
2466
  ...paginationFields
2472
2467
  });
2473
2468
  async function listMilestones(input) {
@@ -2479,12 +2474,12 @@ async function listMilestones(input) {
2479
2474
  }
2480
2475
 
2481
2476
  // src/tools/boards.ts
2482
- import { z as z10 } from "zod";
2477
+ import { z as z13 } from "zod";
2483
2478
  var paginationFields2 = {
2484
- page: z10.number().int().positive().optional(),
2485
- 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()
2486
2481
  };
2487
- var listBoardsSchema = z10.object({ ...paginationFields2 });
2482
+ var listBoardsSchema = z13.object({ ...paginationFields2 });
2488
2483
  async function listBoards(input) {
2489
2484
  const { data, nextPage } = await capsuleGetCached("/boards", {
2490
2485
  page: input.page ?? 1,
@@ -2492,8 +2487,8 @@ async function listBoards(input) {
2492
2487
  });
2493
2488
  return { ...data, nextPage };
2494
2489
  }
2495
- var listStagesSchema = z10.object({
2496
- boardId: z10.number().int().positive().optional().describe(
2490
+ var listStagesSchema = z13.object({
2491
+ boardId: positiveId.optional().describe(
2497
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."
2498
2493
  ),
2499
2494
  ...paginationFields2
@@ -2508,7 +2503,7 @@ async function listStages(input) {
2508
2503
  }
2509
2504
 
2510
2505
  // src/tools/tags.ts
2511
- import { z as z11 } from "zod";
2506
+ import { z as z14 } from "zod";
2512
2507
  var TAG_LIST_PATH = {
2513
2508
  parties: "/parties/tags",
2514
2509
  opportunities: "/opportunities/tags",
@@ -2519,11 +2514,11 @@ var ENTITY_TO_WRAPPER = {
2519
2514
  opportunities: "opportunity",
2520
2515
  kases: "kase"
2521
2516
  };
2522
- var TagEntity = z11.enum(["parties", "opportunities", "kases"]).describe("Which entity type. Use 'kases' for projects (Capsule's legacy path name).");
2523
- var listTagsSchema = z11.object({
2524
- entity: z11.enum(["parties", "opportunities", "kases"]).describe("The resource type to list tags for"),
2525
- page: z11.number().int().positive().optional(),
2526
- 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()
2527
2522
  });
2528
2523
  async function listTags(input) {
2529
2524
  const path = TAG_LIST_PATH[input.entity];
@@ -2533,10 +2528,10 @@ async function listTags(input) {
2533
2528
  });
2534
2529
  return { ...data, nextPage };
2535
2530
  }
2536
- var addTagSchema = z11.object({
2531
+ var addTagSchema = z14.object({
2537
2532
  entity: TagEntity,
2538
- entityId: z11.number().int().positive().describe("The party/opportunity/kase id."),
2539
- tagName: z11.string().min(1).describe(
2533
+ entityId: positiveId.describe("The party/opportunity/kase id."),
2534
+ tagName: z14.string().min(1).describe(
2540
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."
2541
2536
  )
2542
2537
  });
@@ -2549,10 +2544,10 @@ async function addTag(input) {
2549
2544
  invalidateByPrefix(TAG_LIST_PATH[entity], "add_tag");
2550
2545
  return result;
2551
2546
  }
2552
- var removeTagByIdSchema = z11.object({
2547
+ var removeTagByIdSchema = z14.object({
2553
2548
  entity: TagEntity,
2554
- entityId: z11.number().int().positive().describe("The party/opportunity/kase id."),
2555
- tagId: z11.number().int().positive().describe(
2549
+ entityId: positiveId.describe("The party/opportunity/kase id."),
2550
+ tagId: positiveId.describe(
2556
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."
2557
2552
  )
2558
2553
  });
@@ -2580,28 +2575,24 @@ async function removeTagById(input) {
2580
2575
  invalidateByPrefix(TAG_LIST_PATH[entity], "remove_tag_by_id");
2581
2576
  return result;
2582
2577
  }
2583
- var batchAddTagSchema = z11.object({
2584
- items: z11.array(addTagSchema).min(1).max(50).describe(
2585
- "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."
2586
- )
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
2587
2583
  });
2588
- async function batchAddTag(input, opts = {}) {
2589
- return batchExecute("batch_add_tag", input.items, (item) => addTag(item), opts);
2590
- }
2591
- var batchRemoveTagByIdSchema = z11.object({
2592
- items: z11.array(removeTagByIdSchema).min(1).max(50).describe(
2593
- "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."
2594
- )
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
2595
2589
  });
2596
- async function batchRemoveTagById(input, opts = {}) {
2597
- return batchExecute("batch_remove_tag_by_id", input.items, (item) => removeTagById(item), opts);
2598
- }
2599
2590
 
2600
2591
  // src/tools/users.ts
2601
- import { z as z12 } from "zod";
2602
- var listUsersSchema = z12.object({
2603
- page: z12.number().int().positive().optional(),
2604
- 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()
2605
2596
  });
2606
2597
  async function listUsers(input) {
2607
2598
  const { data, nextPage } = await capsuleGetCached("/users", {
@@ -2610,32 +2601,32 @@ async function listUsers(input) {
2610
2601
  });
2611
2602
  return { ...data, nextPage };
2612
2603
  }
2613
- var getCurrentUserSchema = z12.object({});
2604
+ var getCurrentUserSchema = z15.object({});
2614
2605
  async function getCurrentUser(_input) {
2615
2606
  const { data } = await capsuleGet("/users/current");
2616
2607
  return data;
2617
2608
  }
2618
2609
 
2619
2610
  // src/tools/filters.ts
2620
- import { z as z13 } from "zod";
2621
- var FilterConditionSchema = z13.object({
2622
- field: z13.string().describe(
2611
+ import { z as z16 } from "zod";
2612
+ var FilterConditionSchema = z16.object({
2613
+ field: z16.string().describe(
2623
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"
2624
2615
  ),
2625
- operator: z13.string().describe(
2616
+ operator: z16.string().describe(
2626
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."
2627
2618
  ),
2628
- 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(
2629
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."
2630
2621
  )
2631
2622
  });
2632
- var FilterInputSchema = z13.object({
2633
- conditions: z13.array(FilterConditionSchema).min(1).describe(
2623
+ var FilterInputSchema = z16.object({
2624
+ conditions: z16.array(FilterConditionSchema).min(1).describe(
2634
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)."
2635
2626
  ),
2636
- embed: z13.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
2637
- page: z13.number().int().positive().optional().default(1),
2638
- 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)
2639
2630
  });
2640
2631
  async function runFilter(entityPath, input) {
2641
2632
  const { data, nextPage } = await capsuleSearch(
@@ -2666,12 +2657,12 @@ async function filterProjects(input) {
2666
2657
  }
2667
2658
 
2668
2659
  // src/tools/metadata.ts
2669
- import { z as z14 } from "zod";
2660
+ import { z as z17 } from "zod";
2670
2661
  var paginationFields3 = {
2671
- page: z14.number().int().positive().optional(),
2672
- 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.")
2673
2664
  };
2674
- var listTeamsSchema = z14.object({ ...paginationFields3 });
2665
+ var listTeamsSchema = z17.object({ ...paginationFields3 });
2675
2666
  async function listTeams(input) {
2676
2667
  const { data, nextPage } = await capsuleGetCached("/teams", {
2677
2668
  page: input.page ?? 1,
@@ -2679,7 +2670,7 @@ async function listTeams(input) {
2679
2670
  });
2680
2671
  return { ...data, nextPage };
2681
2672
  }
2682
- var listLostReasonsSchema = z14.object({ ...paginationFields3 });
2673
+ var listLostReasonsSchema = z17.object({ ...paginationFields3 });
2683
2674
  async function listLostReasons(input) {
2684
2675
  const { data, nextPage } = await capsuleGetCached("/lostreasons", {
2685
2676
  page: input.page ?? 1,
@@ -2687,7 +2678,7 @@ async function listLostReasons(input) {
2687
2678
  });
2688
2679
  return { ...data, nextPage };
2689
2680
  }
2690
- var listActivityTypesSchema = z14.object({ ...paginationFields3 });
2681
+ var listActivityTypesSchema = z17.object({ ...paginationFields3 });
2691
2682
  async function listActivityTypes(input) {
2692
2683
  const { data, nextPage } = await capsuleGetCached(
2693
2684
  "/activitytypes",
@@ -2698,12 +2689,12 @@ async function listActivityTypes(input) {
2698
2689
  );
2699
2690
  return { ...data, nextPage };
2700
2691
  }
2701
- var getSiteSchema = z14.object({});
2692
+ var getSiteSchema = z17.object({});
2702
2693
  async function getSite(_input) {
2703
2694
  const { data } = await capsuleGetCached("/site");
2704
2695
  return data;
2705
2696
  }
2706
- var listTrackDefinitionsSchema = z14.object({ ...paginationFields3 });
2697
+ var listTrackDefinitionsSchema = z17.object({ ...paginationFields3 });
2707
2698
  async function listTrackDefinitions(input) {
2708
2699
  const { data, nextPage } = await capsuleGetCached(
2709
2700
  "/trackdefinitions",
@@ -2711,7 +2702,7 @@ async function listTrackDefinitions(input) {
2711
2702
  );
2712
2703
  return { ...data, nextPage };
2713
2704
  }
2714
- var listCategoriesSchema = z14.object({ ...paginationFields3 });
2705
+ var listCategoriesSchema = z17.object({ ...paginationFields3 });
2715
2706
  async function listCategories(input) {
2716
2707
  const { data, nextPage } = await capsuleGetCached("/categories", {
2717
2708
  page: input.page ?? 1,
@@ -2719,7 +2710,7 @@ async function listCategories(input) {
2719
2710
  });
2720
2711
  return { ...data, nextPage };
2721
2712
  }
2722
- var listGoalsSchema = z14.object({ ...paginationFields3 });
2713
+ var listGoalsSchema = z17.object({ ...paginationFields3 });
2723
2714
  async function listGoals(input) {
2724
2715
  const { data, nextPage } = await capsuleGetCached("/goals", {
2725
2716
  page: input.page ?? 1,
@@ -2729,14 +2720,14 @@ async function listGoals(input) {
2729
2720
  }
2730
2721
 
2731
2722
  // src/tools/audit.ts
2732
- import { z as z15 } from "zod";
2733
- var listEmployeesSchema = z15.object({
2734
- partyId: z15.number().int().positive().describe(
2723
+ import { z as z18 } from "zod";
2724
+ var listEmployeesSchema = z18.object({
2725
+ partyId: positiveId.describe(
2735
2726
  "The organisation's party id. Returns the people whose `organisation` field links to this party."
2736
2727
  ),
2737
- page: z15.number().int().positive().optional().default(1),
2738
- perPage: z15.number().int().min(1).max(100).optional().default(25),
2739
- 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)
2740
2731
  });
2741
2732
  async function listEmployees(input) {
2742
2733
  const { data, nextPage } = await capsuleGet(
@@ -2745,15 +2736,15 @@ async function listEmployees(input) {
2745
2736
  );
2746
2737
  return { ...data, nextPage };
2747
2738
  }
2748
- var DeletedSinceSchema = z15.string().describe(
2739
+ var DeletedSinceSchema = z18.string().describe(
2749
2740
  "REQUIRED. ISO-8601 timestamp; only deletions on or after this point are returned. Example: '2026-01-01T00:00:00Z'."
2750
2741
  );
2751
2742
  var DeletedPagination = {
2752
2743
  since: DeletedSinceSchema,
2753
- page: z15.number().int().positive().optional().default(1),
2754
- 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)
2755
2746
  };
2756
- var listDeletedPartiesSchema = z15.object(DeletedPagination);
2747
+ var listDeletedPartiesSchema = z18.object(DeletedPagination);
2757
2748
  async function listDeletedParties(input) {
2758
2749
  const { data, nextPage } = await capsuleGet("/parties/deleted", {
2759
2750
  since: input.since,
@@ -2762,7 +2753,7 @@ async function listDeletedParties(input) {
2762
2753
  });
2763
2754
  return { ...data, nextPage };
2764
2755
  }
2765
- var listDeletedOpportunitiesSchema = z15.object(DeletedPagination);
2756
+ var listDeletedOpportunitiesSchema = z18.object(DeletedPagination);
2766
2757
  async function listDeletedOpportunities(input) {
2767
2758
  const { data, nextPage } = await capsuleGet("/opportunities/deleted", {
2768
2759
  since: input.since,
@@ -2771,7 +2762,7 @@ async function listDeletedOpportunities(input) {
2771
2762
  });
2772
2763
  return { ...data, nextPage };
2773
2764
  }
2774
- var listDeletedProjectsSchema = z15.object(DeletedPagination);
2765
+ var listDeletedProjectsSchema = z18.object(DeletedPagination);
2775
2766
  async function listDeletedProjects(input) {
2776
2767
  const { data, nextPage } = await capsuleGet("/kases/deleted", {
2777
2768
  since: input.since,
@@ -2782,14 +2773,14 @@ async function listDeletedProjects(input) {
2782
2773
  }
2783
2774
 
2784
2775
  // src/tools/relationships.ts
2785
- import { z as z16 } from "zod";
2786
- var RelationshipEntity = z16.enum(["opportunities", "kases"]).describe("Which entity has the additional-party links. Use 'kases' for projects.");
2787
- 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({
2788
2779
  entity: RelationshipEntity,
2789
- entityId: z16.number().int().positive().describe("ID of the opportunity or project."),
2790
- embed: z16.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
2791
- page: z16.number().int().positive().optional().default(1),
2792
- 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)
2793
2784
  });
2794
2785
  async function listAdditionalParties(input) {
2795
2786
  const { data, nextPage } = await capsuleGet(
@@ -2798,10 +2789,12 @@ async function listAdditionalParties(input) {
2798
2789
  );
2799
2790
  return { ...data, nextPage };
2800
2791
  }
2801
- var addAdditionalPartySchema = z16.object({
2792
+ var addAdditionalPartySchema = z19.object({
2802
2793
  entity: RelationshipEntity,
2803
- entityId: z16.number().int().positive(),
2804
- 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
+ )
2805
2798
  });
2806
2799
  async function addAdditionalParty(input) {
2807
2800
  try {
@@ -2829,10 +2822,10 @@ async function addAdditionalParty(input) {
2829
2822
  throw err;
2830
2823
  }
2831
2824
  }
2832
- var removeAdditionalPartySchema = z16.object({
2825
+ var removeAdditionalPartySchema = z19.object({
2833
2826
  entity: RelationshipEntity,
2834
- entityId: z16.number().int().positive(),
2835
- partyId: z16.number().int().positive(),
2827
+ entityId: positiveId,
2828
+ partyId: positiveId,
2836
2829
  confirm: confirmFlag().describe(
2837
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."
2838
2831
  )
@@ -2859,11 +2852,11 @@ async function removeAdditionalParty(input) {
2859
2852
  })
2860
2853
  );
2861
2854
  }
2862
- var listAssociatedProjectsSchema = z16.object({
2863
- opportunityId: z16.number().int().positive(),
2864
- embed: z16.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
2865
- page: z16.number().int().positive().optional().default(1),
2866
- 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)
2867
2860
  });
2868
2861
  async function listAssociatedProjects(input) {
2869
2862
  const { data, nextPage } = await capsuleGet(
@@ -2874,9 +2867,9 @@ async function listAssociatedProjects(input) {
2874
2867
  }
2875
2868
 
2876
2869
  // src/tools/custom-fields.ts
2877
- import { z as z17 } from "zod";
2878
- var CustomFieldEntity = z17.enum(["parties", "opportunities", "kases"]).describe("Which entity type's custom field schema to inspect. Use 'kases' for projects.");
2879
- 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({
2880
2873
  entity: CustomFieldEntity
2881
2874
  });
2882
2875
  async function listCustomFields(input) {
@@ -2885,9 +2878,9 @@ async function listCustomFields(input) {
2885
2878
  );
2886
2879
  return data;
2887
2880
  }
2888
- var getCustomFieldSchema = z17.object({
2881
+ var getCustomFieldSchema = z20.object({
2889
2882
  entity: CustomFieldEntity,
2890
- fieldId: z17.number().int().positive().describe("Custom field definition id.")
2883
+ fieldId: positiveId.describe("Custom field definition id.")
2891
2884
  });
2892
2885
  async function getCustomField(input) {
2893
2886
  const { data } = await capsuleGetCached(
@@ -2897,11 +2890,11 @@ async function getCustomField(input) {
2897
2890
  }
2898
2891
 
2899
2892
  // src/tools/tracks.ts
2900
- import { z as z18 } from "zod";
2901
- var TrackEntity = z18.enum(["parties", "opportunities", "kases"]).describe("Use 'kases' for projects.");
2902
- 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({
2903
2896
  entity: TrackEntity,
2904
- entityId: z18.number().int().positive()
2897
+ entityId: positiveId
2905
2898
  });
2906
2899
  async function listEntityTracks(input) {
2907
2900
  const { data } = await capsuleGet(
@@ -2909,20 +2902,20 @@ async function listEntityTracks(input) {
2909
2902
  );
2910
2903
  return data;
2911
2904
  }
2912
- var showTrackSchema = z18.object({
2913
- trackId: z18.number().int().positive()
2905
+ var showTrackSchema = z21.object({
2906
+ trackId: positiveId
2914
2907
  });
2915
2908
  async function showTrack(input) {
2916
2909
  const { data } = await capsuleGet(`/tracks/${input.trackId}`);
2917
2910
  return data;
2918
2911
  }
2919
- var applyTrackSchema = z18.object({
2920
- entity: z18.enum(["opportunities", "kases"]).describe("Which entity to apply the track to. Use 'kases' for projects."),
2921
- entityId: z18.number().int().positive(),
2922
- 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(
2923
2916
  "The trackDefinition to apply (from list_track_definitions). Auto-creates task definitions on the target entity per the track's rules."
2924
2917
  ),
2925
- 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(
2926
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."
2927
2920
  )
2928
2921
  });
@@ -2935,9 +2928,9 @@ async function applyTrack(input) {
2935
2928
  if (input.startDate !== void 0) track["trackDateOn"] = input.startDate;
2936
2929
  return capsulePost("/tracks", { track });
2937
2930
  }
2938
- var updateTrackSchema = z18.object({
2939
- trackId: z18.number().int().positive(),
2940
- 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(
2941
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."
2942
2935
  )
2943
2936
  });
@@ -2949,8 +2942,8 @@ async function updateTrack(input) {
2949
2942
  track: input.fields
2950
2943
  });
2951
2944
  }
2952
- var removeTrackSchema = z18.object({
2953
- trackId: z18.number().int().positive(),
2945
+ var removeTrackSchema = z21.object({
2946
+ trackId: positiveId,
2954
2947
  confirm: confirmFlag().describe(
2955
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."
2956
2949
  )
@@ -2967,13 +2960,13 @@ async function removeTrack(input) {
2967
2960
  }
2968
2961
 
2969
2962
  // src/tools/attachments.ts
2970
- import { z as z19 } from "zod";
2963
+ import { z as z22 } from "zod";
2971
2964
  var DEFAULT_MAX_SIZE_BYTES = 5 * 1024 * 1024;
2972
2965
  var HARD_MAX_SIZE_BYTES = 25 * 1024 * 1024;
2973
2966
  var HARD_MAX_BASE64_CHARS = Math.ceil(HARD_MAX_SIZE_BYTES / 3) * 4;
2974
- var getAttachmentSchema = z19.object({
2975
- id: z19.number().int().positive().describe("Attachment ID."),
2976
- 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(
2977
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.`
2978
2971
  )
2979
2972
  });
@@ -2988,22 +2981,22 @@ async function getAttachment(input) {
2988
2981
  }
2989
2982
  return { contentType, buffer, sizeBytes };
2990
2983
  }
2991
- var uploadAttachmentSchema = z19.object({
2992
- filename: z19.string().min(1).describe(
2984
+ var uploadAttachmentSchema = z22.object({
2985
+ filename: z22.string().min(1).describe(
2993
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."
2994
2987
  ),
2995
- contentType: z19.string().min(1).describe(
2988
+ contentType: z22.string().min(1).describe(
2996
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."
2997
2990
  ),
2998
- dataBase64: z19.string().min(1).max(HARD_MAX_BASE64_CHARS).describe(
2991
+ dataBase64: z22.string().min(1).max(HARD_MAX_BASE64_CHARS).describe(
2999
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."
3000
2993
  ),
3001
- content: z19.string().optional().describe(
2994
+ content: z22.string().optional().describe(
3002
2995
  "Body text for the note that will hold the attachment. Defaults to '[attachment]' if omitted."
3003
2996
  ),
3004
- partyId: z19.number().int().positive().optional().describe("Link the new note to a party (mutually exclusive with opportunityId / projectId)."),
3005
- opportunityId: z19.number().int().positive().optional(),
3006
- 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()
3007
3000
  });
3008
3001
  function isValidBase64(s) {
3009
3002
  if (!/^[A-Za-z0-9+/]*={0,2}$/.test(s)) return false;
@@ -3053,23 +3046,23 @@ async function uploadAttachment(input) {
3053
3046
  }
3054
3047
 
3055
3048
  // src/tools/saved-filters.ts
3056
- import { z as z20 } from "zod";
3057
- var EntitySchema = z20.enum(["parties", "opportunities", "kases"]).describe(
3049
+ import { z as z23 } from "zod";
3050
+ var EntitySchema = z23.enum(["parties", "opportunities", "kases"]).describe(
3058
3051
  "Which entity type the filter operates over. Use 'kases' for projects (Capsule's legacy name)."
3059
3052
  );
3060
- var listSavedFiltersSchema = z20.object({
3053
+ var listSavedFiltersSchema = z23.object({
3061
3054
  entity: EntitySchema
3062
3055
  });
3063
3056
  async function listSavedFilters(input) {
3064
3057
  const { data } = await capsuleGetCached(`/${input.entity}/filters`);
3065
3058
  return data;
3066
3059
  }
3067
- var runSavedFilterSchema = z20.object({
3060
+ var runSavedFilterSchema = z23.object({
3068
3061
  entity: EntitySchema,
3069
- id: z20.number().int().positive().describe("The saved filter id (from list_saved_filters)."),
3070
- embed: z20.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
3071
- page: z20.number().int().positive().optional().default(1),
3072
- 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)
3073
3066
  });
3074
3067
  async function runSavedFilter(input) {
3075
3068
  const { data, nextPage } = await capsuleGet(
@@ -3087,7 +3080,7 @@ function createCapsuleMcpServer(opts) {
3087
3080
  const server = new McpServer(
3088
3081
  {
3089
3082
  name: "capsulemcp",
3090
- version: "1.6.1",
3083
+ version: "1.6.2",
3091
3084
  description: "Read and (optionally) modify Capsule CRM data \u2014 parties, opportunities, projects, tasks, timeline entries, pipelines, tags.",
3092
3085
  websiteUrl: "https://github.com/soil-dev/capsulemcp",
3093
3086
  icons: ICONS
@@ -3672,7 +3665,7 @@ function createCapsuleMcpServer(opts) {
3672
3665
  registerTool(
3673
3666
  server,
3674
3667
  "list_teams",
3675
- "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.",
3676
3669
  listTeamsSchema,
3677
3670
  listTeams
3678
3671
  );