capsulemcp 1.6.1 → 1.6.3

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,91 +1717,82 @@ 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
+ organisationId: positiveId.nullable().optional().describe(
1764
+ "For PERSON parties: link to an organisation by id, or `null` to unlink (the person becomes an orphan / standalone record). Discover org IDs via search_parties / filter_parties with type=organisation. For ORGANISATION parties: silently ignored by Capsule's API \u2014 organisations don't have a parent organisation in the data model. Empirically verified in v1.6.3 wire-trace; no client-side type guard since the no-op is harmless."
1765
+ ),
1766
+ fields: z7.array(CustomFieldWriteSchema).optional().describe(fieldsArrayDescriptor("get_party")),
1710
1767
  ...PartyWriteBaseSchema
1711
1768
  });
1712
1769
  async function updateParty(input) {
1713
- const { id, ownerId, fields, ...rest } = input;
1770
+ const { id, ownerId, organisationId, fields, ...rest } = input;
1714
1771
  const body = {};
1715
1772
  for (const [k, v] of Object.entries(rest)) {
1716
1773
  if (v !== void 0) body[k] = v;
1717
1774
  }
1718
- if (ownerId) body["owner"] = { id: ownerId };
1775
+ setRef(body, "owner", ownerId);
1776
+ setNullableRef(body, "organisation", organisationId);
1719
1777
  const mappedFields = mapFieldsForBody(fields);
1720
1778
  if (mappedFields !== void 0) body["fields"] = mappedFields;
1721
1779
  return capsulePut(`/parties/${id}`, { party: body });
1722
1780
  }
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
- )
1781
+ var { schema: batchUpdatePartySchema, handler: batchUpdateParty } = defineBatch({
1782
+ toolName: "batch_update_party",
1783
+ itemSchema: updatePartySchema,
1784
+ 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).",
1785
+ itemHandler: updateParty
1727
1786
  });
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
- )
1787
+ var { schema: deletePartySchema, handler: deleteParty } = defineDelete({
1788
+ toolName: "delete_party",
1789
+ pathPrefix: "/parties",
1790
+ 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
1791
  });
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'.")
1792
+ var addPartyEmailAddressSchema = z7.object({
1793
+ partyId: positiveId,
1794
+ address: z7.string().email(),
1795
+ type: z7.string().optional().describe("Free-form label, e.g. 'Work', 'Home'.")
1751
1796
  });
1752
1797
  async function addPartyEmailAddress(input) {
1753
1798
  const { partyId, address, type } = input;
@@ -1757,9 +1802,9 @@ async function addPartyEmailAddress(input) {
1757
1802
  party: { emailAddresses: [item] }
1758
1803
  });
1759
1804
  }
1760
- var removePartyEmailAddressByIdSchema = z4.object({
1761
- partyId: z4.number().int().positive(),
1762
- emailAddressId: z4.number().int().positive().describe(
1805
+ var removePartyEmailAddressByIdSchema = z7.object({
1806
+ partyId: positiveId,
1807
+ emailAddressId: positiveId.describe(
1763
1808
  "Capsule's id for the email-address row. Read it from get_party (each entry in emailAddresses carries an id)."
1764
1809
  )
1765
1810
  });
@@ -1779,10 +1824,10 @@ async function removePartyEmailAddressById(input) {
1779
1824
  () => ({ removed: true, alreadyRemoved: true, partyId, emailAddressId })
1780
1825
  );
1781
1826
  }
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'.")
1827
+ var addPartyPhoneNumberSchema = z7.object({
1828
+ partyId: positiveId,
1829
+ number: z7.string().min(1),
1830
+ type: z7.string().optional().describe("Free-form label, e.g. 'Work', 'Mobile'.")
1786
1831
  });
1787
1832
  async function addPartyPhoneNumber(input) {
1788
1833
  const { partyId, number, type } = input;
@@ -1792,9 +1837,9 @@ async function addPartyPhoneNumber(input) {
1792
1837
  party: { phoneNumbers: [item] }
1793
1838
  });
1794
1839
  }
1795
- var removePartyPhoneNumberByIdSchema = z4.object({
1796
- partyId: z4.number().int().positive(),
1797
- phoneNumberId: z4.number().int().positive().describe(
1840
+ var removePartyPhoneNumberByIdSchema = z7.object({
1841
+ partyId: positiveId,
1842
+ phoneNumberId: positiveId.describe(
1798
1843
  "Capsule's id for the phone-number row. Read it from get_party (each entry in phoneNumbers carries an id)."
1799
1844
  )
1800
1845
  });
@@ -1814,14 +1859,14 @@ async function removePartyPhoneNumberById(input) {
1814
1859
  () => ({ removed: true, alreadyRemoved: true, partyId, phoneNumberId })
1815
1860
  );
1816
1861
  }
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'.")
1862
+ var addPartyAddressSchema = z7.object({
1863
+ partyId: positiveId,
1864
+ street: z7.string().optional(),
1865
+ city: z7.string().optional(),
1866
+ state: z7.string().optional(),
1867
+ country: z7.string().optional().describe(CountryDescription),
1868
+ zip: z7.string().optional(),
1869
+ type: z7.string().optional().describe("Free-form label, e.g. 'Office', 'Home'.")
1825
1870
  });
1826
1871
  async function addPartyAddress(input) {
1827
1872
  const { partyId, ...rest } = input;
@@ -1833,9 +1878,9 @@ async function addPartyAddress(input) {
1833
1878
  party: { addresses: [item] }
1834
1879
  });
1835
1880
  }
1836
- var removePartyAddressByIdSchema = z4.object({
1837
- partyId: z4.number().int().positive(),
1838
- addressId: z4.number().int().positive().describe(
1881
+ var removePartyAddressByIdSchema = z7.object({
1882
+ partyId: positiveId,
1883
+ addressId: positiveId.describe(
1839
1884
  "Capsule's id for the address row. Read it from get_party (each entry in addresses carries an id)."
1840
1885
  )
1841
1886
  });
@@ -1855,9 +1900,9 @@ async function removePartyAddressById(input) {
1855
1900
  () => ({ removed: true, alreadyRemoved: true, partyId, addressId })
1856
1901
  );
1857
1902
  }
1858
- var addPartyWebsiteSchema = z4.object({
1859
- partyId: z4.number().int().positive(),
1860
- address: z4.string().min(1).describe(
1903
+ var addPartyWebsiteSchema = z7.object({
1904
+ partyId: positiveId,
1905
+ address: z7.string().min(1).describe(
1861
1906
  "The website address. A URL when service='URL', or a handle (e.g. '@acmeco') for social services."
1862
1907
  ),
1863
1908
  service: WebsiteServiceEnum.optional().describe("Defaults to 'URL' if omitted.")
@@ -1870,9 +1915,9 @@ async function addPartyWebsite(input) {
1870
1915
  party: { websites: [item] }
1871
1916
  });
1872
1917
  }
1873
- var removePartyWebsiteByIdSchema = z4.object({
1874
- partyId: z4.number().int().positive(),
1875
- websiteId: z4.number().int().positive().describe(
1918
+ var removePartyWebsiteByIdSchema = z7.object({
1919
+ partyId: positiveId,
1920
+ websiteId: positiveId.describe(
1876
1921
  "Capsule's id for the website row. Read it from get_party (each entry in websites carries an id)."
1877
1922
  )
1878
1923
  });
@@ -1894,20 +1939,32 @@ async function removePartyWebsiteById(input) {
1894
1939
  }
1895
1940
 
1896
1941
  // 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({
1942
+ import { z as z8 } from "zod";
1943
+
1944
+ // src/tools/preserve-refs.ts
1945
+ async function readEntityRefs(path, responseKey) {
1946
+ const { data } = await capsuleGet(path);
1947
+ const entity = data[responseKey];
1948
+ return {
1949
+ teamId: entity?.team?.id ?? void 0,
1950
+ stageId: entity?.stage?.id ?? void 0
1951
+ };
1952
+ }
1953
+
1954
+ // src/tools/opportunities.ts
1955
+ var OpportunityValueSchema = z8.object({
1956
+ amount: z8.number().nonnegative(),
1957
+ currency: z8.string({
1901
1958
  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
1959
  }).length(3).describe(
1903
1960
  "ISO 4217 currency code (3 letters), e.g. 'GBP', 'USD', 'EUR'. Required when amount is set."
1904
1961
  )
1905
1962
  });
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)
1963
+ var searchOpportunitiesSchema = z8.object({
1964
+ q: z8.string().optional().describe("Free-text search query"),
1965
+ embed: z8.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
1966
+ page: z8.number().int().positive().optional().default(1),
1967
+ perPage: z8.number().int().min(1).max(100).optional().default(25)
1911
1968
  });
1912
1969
  async function searchOpportunities(input) {
1913
1970
  const path = input.q ? "/opportunities/search" : "/opportunities";
@@ -1919,9 +1976,9 @@ async function searchOpportunities(input) {
1919
1976
  });
1920
1977
  return { ...data, nextPage };
1921
1978
  }
1922
- var getOpportunitySchema = z5.object({
1923
- id: z5.number().int().positive(),
1924
- embed: z5.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
1979
+ var getOpportunitySchema = z8.object({
1980
+ id: positiveId,
1981
+ embed: z8.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
1925
1982
  });
1926
1983
  async function getOpportunity(input) {
1927
1984
  const { data } = await capsuleGet(`/opportunities/${input.id}`, {
@@ -1929,11 +1986,11 @@ async function getOpportunity(input) {
1929
1986
  });
1930
1987
  return data;
1931
1988
  }
1932
- var getOpportunitiesSchema = z5.object({
1933
- ids: z5.array(z5.number().int().positive()).min(1).max(50).describe(
1989
+ var getOpportunitiesSchema = z8.object({
1990
+ ids: z8.array(positiveId).min(1).max(50).describe(
1934
1991
  "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
1992
  ),
1936
- embed: z5.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
1993
+ embed: z8.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
1937
1994
  });
1938
1995
  async function getOpportunities(input) {
1939
1996
  const { ids, embed } = input;
@@ -1954,20 +2011,20 @@ async function getOpportunities(input) {
1954
2011
  );
1955
2012
  return { opportunities: responses.flatMap((r) => r.data.opportunities) };
1956
2013
  }
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."
2014
+ var createOpportunitySchema = z8.object({
2015
+ name: z8.string().min(1),
2016
+ partyId: positiveId.describe("ID of the party this opportunity belongs to"),
2017
+ milestoneId: positiveId.describe(
2018
+ "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
2019
  ),
1963
- description: z5.string().optional(),
2020
+ description: z8.string().optional(),
1964
2021
  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."
2022
+ expectedCloseOn: z8.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("YYYY-MM-DD"),
2023
+ probability: z8.number().int().min(0).max(100).optional(),
2024
+ ownerId: positiveId.optional().describe(
2025
+ "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
2026
  ),
1970
- teamId: z5.number().int().positive().optional().describe(
2027
+ teamId: positiveId.optional().describe(
1971
2028
  "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
2029
  )
1973
2030
  });
@@ -1978,92 +2035,76 @@ async function createOpportunity(input) {
1978
2035
  party: { id: partyId },
1979
2036
  milestone: { id: milestoneId }
1980
2037
  };
1981
- if (ownerId) body["owner"] = { id: ownerId };
1982
- if (teamId) body["team"] = { id: teamId };
2038
+ setRef(body, "owner", ownerId);
2039
+ setRef(body, "team", teamId);
1983
2040
  return capsulePost("/opportunities", { opportunity: body });
1984
2041
  }
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."
2042
+ var updateOpportunitySchema = z8.object({
2043
+ id: positiveId,
2044
+ name: z8.string().min(1).optional(),
2045
+ partyId: positiveId.optional().describe(
2046
+ "Reassign the opportunity to a different primary party. Capsule requires every opportunity to have a party \u2014 passing `null` is rejected with 422 'party is required' (use Capsule's web UI if you need to dissolve the link entirely). Discover ids via search_parties / filter_parties. No defensive read-modify-write needed: this connector verified empirically (v1.6.3 wire-trace) that `party` is a standalone PUT field on /opportunities and does not interact with the asymmetric owner/team semantic from NOTES-ON-CAPSULE-API.md \xA727."
2047
+ ),
2048
+ milestoneId: positiveId.optional().describe(
2049
+ "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
2050
  ),
1991
- description: z5.string().optional(),
2051
+ description: z8.string().optional(),
1992
2052
  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(
2053
+ expectedCloseOn: z8.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional(),
2054
+ probability: z8.number().int().min(0).max(100).optional().describe(
1995
2055
  "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
2056
  ),
1997
- lostReasonId: z5.number().int().positive().optional().describe(
2057
+ lostReasonId: positiveId.optional().describe(
1998
2058
  "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
2059
  ),
2000
- ownerId: z5.number().int().positive().optional().describe(
2060
+ ownerId: positiveId.optional().describe(
2001
2061
  "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
2062
  ),
2003
- teamId: z5.number().int().positive().nullable().optional().describe(
2063
+ teamId: positiveId.nullable().optional().describe(
2004
2064
  "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
2065
  ),
2006
- fields: z5.array(CustomFieldWriteSchema).optional().describe(fieldsArrayDescriptor("get_opportunity"))
2066
+ fields: z8.array(CustomFieldWriteSchema).optional().describe(fieldsArrayDescriptor("get_opportunity"))
2007
2067
  });
2008
2068
  async function updateOpportunity(input) {
2009
- const { id, milestoneId, ownerId, teamId, lostReasonId, fields, ...rest } = input;
2069
+ const { id, partyId, milestoneId, ownerId, teamId, lostReasonId, fields, ...rest } = input;
2010
2070
  const body = {};
2011
2071
  for (const [k, v] of Object.entries(rest)) {
2012
2072
  if (v !== void 0) body[k] = v;
2013
2073
  }
2014
- if (milestoneId) body["milestone"] = { id: milestoneId };
2074
+ setRef(body, "party", partyId);
2075
+ setRef(body, "milestone", milestoneId);
2015
2076
  let resolvedTeamId = teamId;
2016
2077
  if (ownerId !== void 0 && teamId === void 0) {
2017
- const { data } = await capsuleGet(`/opportunities/${id}`);
2018
- resolvedTeamId = data.opportunity?.team?.id ?? void 0;
2078
+ ({ teamId: resolvedTeamId } = await readEntityRefs(`/opportunities/${id}`, "opportunity"));
2019
2079
  }
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 };
2080
+ setRef(body, "owner", ownerId);
2081
+ setNullableRef(body, "team", resolvedTeamId);
2082
+ setRef(body, "lostReason", lostReasonId);
2024
2083
  const mappedFields = mapFieldsForBody(fields);
2025
2084
  if (mappedFields !== void 0) body["fields"] = mappedFields;
2026
2085
  return capsulePut(`/opportunities/${id}`, {
2027
2086
  opportunity: body
2028
2087
  });
2029
2088
  }
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
- )
2089
+ var { schema: batchUpdateOpportunitySchema, handler: batchUpdateOpportunity } = defineBatch({
2090
+ toolName: "batch_update_opportunity",
2091
+ itemSchema: updateOpportunitySchema,
2092
+ 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.",
2093
+ itemHandler: updateOpportunity
2034
2094
  });
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
- )
2095
+ var { schema: deleteOpportunitySchema, handler: deleteOpportunity } = defineDelete({
2096
+ toolName: "delete_opportunity",
2097
+ pathPrefix: "/opportunities",
2098
+ confirmHint: "Must be set to true. Permanently deletes the opportunity. Irreversible."
2048
2099
  });
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
2100
 
2060
2101
  // 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)
2102
+ import { z as z9 } from "zod";
2103
+ var listProjectsSchema = z9.object({
2104
+ status: z9.enum(["OPEN", "CLOSED"]).optional(),
2105
+ embed: z9.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
2106
+ page: z9.number().int().positive().optional().default(1),
2107
+ perPage: z9.number().int().min(1).max(100).optional().default(25)
2067
2108
  });
2068
2109
  async function listProjects(input) {
2069
2110
  const { data, nextPage } = await capsuleGet("/kases", {
@@ -2074,9 +2115,9 @@ async function listProjects(input) {
2074
2115
  });
2075
2116
  return { ...data, nextPage };
2076
2117
  }
2077
- var getProjectSchema = z6.object({
2078
- id: z6.number().int().positive(),
2079
- embed: z6.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
2118
+ var getProjectSchema = z9.object({
2119
+ id: positiveId,
2120
+ embed: z9.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
2080
2121
  });
2081
2122
  async function getProject(input) {
2082
2123
  const { data } = await capsuleGet(`/kases/${input.id}`, {
@@ -2084,11 +2125,11 @@ async function getProject(input) {
2084
2125
  });
2085
2126
  return data;
2086
2127
  }
2087
- var getProjectsSchema = z6.object({
2088
- ids: z6.array(z6.number().int().positive()).min(1).max(50).describe(
2128
+ var getProjectsSchema = z9.object({
2129
+ ids: z9.array(positiveId).min(1).max(50).describe(
2089
2130
  "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
2131
  ),
2091
- embed: z6.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
2132
+ embed: z9.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
2092
2133
  });
2093
2134
  async function getProjects(input) {
2094
2135
  const { ids, embed } = input;
@@ -2106,21 +2147,21 @@ async function getProjects(input) {
2106
2147
  );
2107
2148
  return { kases: responses.flatMap((r) => r.data.kases) };
2108
2149
  }
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(
2150
+ var createProjectSchema = z9.object({
2151
+ name: z9.string().min(1),
2152
+ partyId: positiveId.describe("ID of the party linked to this project"),
2153
+ description: z9.string().optional(),
2154
+ status: z9.enum(["OPEN", "CLOSED"]).optional().describe("Defaults to OPEN when omitted."),
2155
+ ownerId: positiveId.optional().describe(
2115
2156
  "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
2157
  ),
2117
- teamId: z6.number().int().positive().optional().describe(
2158
+ teamId: positiveId.optional().describe(
2118
2159
  "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
2160
  ),
2120
- stageId: z6.number().int().positive().optional().describe(
2161
+ stageId: positiveId.optional().describe(
2121
2162
  "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
2163
  ),
2123
- expectedCloseOn: z6.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("YYYY-MM-DD")
2164
+ expectedCloseOn: z9.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("YYYY-MM-DD")
2124
2165
  });
2125
2166
  async function createProject(input) {
2126
2167
  const { partyId, ownerId, teamId, status, stageId, ...rest } = input;
@@ -2129,88 +2170,75 @@ async function createProject(input) {
2129
2170
  status: status ?? "OPEN",
2130
2171
  party: { id: partyId }
2131
2172
  };
2132
- if (ownerId) body["owner"] = { id: ownerId };
2133
- if (teamId) body["team"] = { id: teamId };
2173
+ setRef(body, "owner", ownerId);
2174
+ setRef(body, "team", teamId);
2134
2175
  if (stageId) body["stage"] = stageId;
2135
2176
  return capsulePost("/kases", { kase: body });
2136
2177
  }
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(
2178
+ var updateProjectSchema = z9.object({
2179
+ id: positiveId,
2180
+ name: z9.string().min(1).optional(),
2181
+ description: z9.string().optional(),
2182
+ status: z9.enum(["OPEN", "CLOSED"]).optional(),
2183
+ partyId: positiveId.optional().describe(
2184
+ "Reassign the project to a different primary party. Capsule requires every project to have a party \u2014 passing `null` is rejected with 422 'party is required' (verified empirically in v1.6.3 wire-trace). Discover ids via search_parties / filter_parties."
2185
+ ),
2186
+ ownerId: positiveId.nullable().optional().describe(
2143
2187
  "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
2188
  ),
2145
- teamId: z6.number().int().positive().nullable().optional().describe(
2189
+ teamId: positiveId.nullable().optional().describe(
2146
2190
  "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
2191
  ),
2148
- stageId: z6.number().int().positive().optional().describe(
2192
+ stageId: positiveId.optional().describe(
2149
2193
  "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
2194
  ),
2151
- expectedCloseOn: z6.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("YYYY-MM-DD"),
2152
- fields: z6.array(CustomFieldWriteSchema).optional().describe(
2195
+ expectedCloseOn: z9.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("YYYY-MM-DD"),
2196
+ fields: z9.array(CustomFieldWriteSchema).optional().describe(
2153
2197
  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
2198
  )
2155
2199
  });
2156
2200
  async function updateProject(input) {
2157
- const { id, ownerId, teamId, stageId, fields, ...rest } = input;
2201
+ const { id, partyId, ownerId, teamId, stageId, fields, ...rest } = input;
2158
2202
  const body = {};
2159
2203
  for (const [k, v] of Object.entries(rest)) {
2160
2204
  if (v !== void 0) body[k] = v;
2161
2205
  }
2206
+ setRef(body, "party", partyId);
2162
2207
  let resolvedTeamId = teamId;
2163
2208
  let resolvedStageId = stageId;
2164
2209
  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
- }
2210
+ const current = await readEntityRefs(`/kases/${id}`, "kase");
2211
+ if (teamId === void 0) resolvedTeamId = current.teamId;
2212
+ if (stageId === void 0) resolvedStageId = current.stageId;
2172
2213
  }
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 };
2214
+ setNullableRef(body, "owner", ownerId);
2215
+ setNullableRef(body, "team", resolvedTeamId);
2177
2216
  if (resolvedStageId) body["stage"] = resolvedStageId;
2178
2217
  const mappedFields = mapFieldsForBody(fields);
2179
2218
  if (mappedFields !== void 0) body["fields"] = mappedFields;
2180
2219
  return capsulePut(`/kases/${id}`, { kase: body });
2181
2220
  }
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
- )
2221
+ var { schema: deleteProjectSchema, handler: deleteProject } = defineDelete({
2222
+ toolName: "delete_project",
2223
+ pathPrefix: "/kases",
2224
+ confirmHint: "Must be set to true. Permanently deletes the project (case). Consider update_project status='CLOSED' instead. Irreversible."
2187
2225
  });
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
2226
 
2199
2227
  // src/tools/tasks.ts
2200
- import { z as z7 } from "zod";
2201
- var listTasksSchema = z7.object({
2228
+ import { z as z10 } from "zod";
2229
+ var listTasksSchema = z10.object({
2202
2230
  // Note: Capsule has a third internal status `PENDING` (a task that's
2203
2231
  // part of an active track but not yet "open"), but it can only be
2204
2232
  // reached via track machinery — it is NOT directly settable by
2205
2233
  // /tasks PUT, and a list filter for it returns the same as OPEN
2206
2234
  // anyway. We expose only the two values that are actually filterable
2207
2235
  // by the v2 API.
2208
- status: z7.enum(["OPEN", "COMPLETED"]).optional().describe(
2236
+ status: z10.enum(["OPEN", "COMPLETED"]).optional().describe(
2209
2237
  "Defaults to OPEN when omitted. Pass COMPLETED to filter to completed tasks, or 'OPEN' explicitly."
2210
2238
  ),
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)
2239
+ ownerId: positiveId.optional().describe("Filter to tasks owned by this user ID"),
2240
+ page: z10.number().int().positive().optional().default(1),
2241
+ perPage: z10.number().int().min(1).max(100).optional().default(25)
2214
2242
  });
2215
2243
  async function listTasks(input) {
2216
2244
  const { data, nextPage } = await capsuleGet("/tasks", {
@@ -2224,15 +2252,15 @@ async function listTasks(input) {
2224
2252
  });
2225
2253
  return { ...data, nextPage };
2226
2254
  }
2227
- var getTaskSchema = z7.object({
2228
- id: z7.number().int().positive().describe("Task ID")
2255
+ var getTaskSchema = z10.object({
2256
+ id: positiveId.describe("Task ID")
2229
2257
  });
2230
2258
  async function getTask(input) {
2231
2259
  const { data } = await capsuleGet(`/tasks/${input.id}`);
2232
2260
  return data;
2233
2261
  }
2234
- var getTasksSchema = z7.object({
2235
- ids: z7.array(z7.number().int().positive()).min(1).max(50).describe(
2262
+ var getTasksSchema = z10.object({
2263
+ ids: z10.array(positiveId).min(1).max(50).describe(
2236
2264
  "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
2265
  )
2238
2266
  });
@@ -2248,17 +2276,17 @@ async function getTasks(input) {
2248
2276
  );
2249
2277
  return { tasks: responses.flatMap((r) => r.data.tasks) };
2250
2278
  }
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(
2279
+ var createTaskSchema = z10.object({
2280
+ description: z10.string().min(1),
2281
+ dueOn: z10.string().regex(/^\d{4}-\d{2}-\d{2}$/).describe("YYYY-MM-DD"),
2282
+ dueTime: z10.string().regex(/^\d{2}:\d{2}$/).optional().describe("HH:MM in user's timezone"),
2283
+ detail: z10.string().optional(),
2284
+ ownerId: positiveId.optional().describe(
2257
2285
  "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
2286
  ),
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)")
2287
+ partyId: positiveId.optional().describe("Link task to a party (mutually exclusive with opportunityId/projectId)"),
2288
+ opportunityId: positiveId.optional().describe("Link task to an opportunity (mutually exclusive with partyId/projectId)"),
2289
+ projectId: positiveId.optional().describe("Link task to a project (mutually exclusive with partyId/opportunityId)")
2262
2290
  });
2263
2291
  async function createTask(input) {
2264
2292
  const linked = [input.partyId, input.opportunityId, input.projectId].filter(Boolean);
@@ -2267,79 +2295,86 @@ async function createTask(input) {
2267
2295
  }
2268
2296
  const { ownerId, partyId, opportunityId, projectId, ...rest } = input;
2269
2297
  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 };
2298
+ setRef(body, "owner", ownerId);
2299
+ setRef(body, "party", partyId);
2300
+ setRef(body, "opportunity", opportunityId);
2301
+ setRef(body, "kase", projectId);
2274
2302
  return capsulePost("/tasks", { task: body });
2275
2303
  }
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(),
2304
+ var updateTaskSchema = z10.object({
2305
+ id: positiveId,
2306
+ description: z10.string().min(1).optional(),
2307
+ dueOn: z10.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("YYYY-MM-DD"),
2308
+ dueTime: z10.string().regex(/^\d{2}:\d{2}$/).optional().describe("HH:MM in user's timezone"),
2309
+ detail: z10.string().optional(),
2282
2310
  // Capsule rejects direct sets of `PENDING` (which is a track-machinery
2283
2311
  // internal state) with 422 "cannot set task status to PENDING".
2284
2312
  // Only OPEN and COMPLETED are settable here.
2285
- status: z7.enum(["OPEN", "COMPLETED"]).optional().describe(
2313
+ status: z10.enum(["OPEN", "COMPLETED"]).optional().describe(
2286
2314
  "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
2315
  ),
2288
- ownerId: z7.number().int().positive().optional().describe(
2316
+ ownerId: positiveId.optional().describe(
2289
2317
  "Reassign owner to user ID. Once set, this connector cannot clear an owner back to null \u2014 use Capsule's web UI for that."
2318
+ ),
2319
+ partyId: positiveId.nullable().optional().describe(
2320
+ "Re-link the task to a party by id, or `null` to orphan it. Mutually exclusive with `opportunityId` / `projectId` \u2014 Capsule enforces 'task can be related to at most one entity' server-side (422 if two parent-refs are set at once, verified in v1.6.3 wire-trace). To swap parent type atomically, pass the old one as `null` and the new one as an id in the same call."
2321
+ ),
2322
+ opportunityId: positiveId.nullable().optional().describe(
2323
+ "Re-link the task to an opportunity by id, or `null` to orphan it. Mutually exclusive with `partyId` / `projectId` \u2014 see `partyId` for the XOR semantic."
2324
+ ),
2325
+ projectId: positiveId.nullable().optional().describe(
2326
+ "Re-link the task to a project (kase) by id, or `null` to orphan it. Mutually exclusive with `partyId` / `opportunityId` \u2014 see `partyId` for the XOR semantic."
2290
2327
  )
2291
2328
  });
2292
2329
  async function updateTask(input) {
2293
- const { id, ownerId, ...rest } = input;
2330
+ const { id, ownerId, partyId, opportunityId, projectId, ...rest } = input;
2331
+ const setCount = [partyId, opportunityId, projectId].filter((v) => typeof v === "number").length;
2332
+ if (setCount > 1) {
2333
+ throw new Error(
2334
+ "update_task: provide at most one of partyId, opportunityId, or projectId (Capsule rejects multi-parent tasks with 422 'task can be related to at most one entity')"
2335
+ );
2336
+ }
2294
2337
  const body = {};
2295
2338
  for (const [k, v] of Object.entries(rest)) {
2296
2339
  if (v !== void 0) body[k] = v;
2297
2340
  }
2298
- if (ownerId) body["owner"] = { id: ownerId };
2341
+ setRef(body, "owner", ownerId);
2342
+ setNullableRef(body, "party", partyId);
2343
+ setNullableRef(body, "opportunity", opportunityId);
2344
+ setNullableRef(body, "kase", projectId);
2299
2345
  return capsulePut(`/tasks/${id}`, { task: body });
2300
2346
  }
2301
- var completeTaskSchema = z7.object({
2302
- id: z7.number().int().positive()
2347
+ var completeTaskSchema = z10.object({
2348
+ id: positiveId
2303
2349
  });
2304
2350
  async function completeTask(input) {
2305
2351
  return capsulePut(`/tasks/${input.id}`, {
2306
2352
  task: { status: "COMPLETED" }
2307
2353
  });
2308
2354
  }
2309
- var batchCompleteTaskSchema = z7.object({
2310
- ids: z7.array(z7.number().int().positive()).min(1).max(50).describe(
2355
+ var batchCompleteTaskSchema = z10.object({
2356
+ ids: z10.array(positiveId).min(1).max(50).describe(
2311
2357
  "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
2358
  )
2313
2359
  });
2314
2360
  async function batchCompleteTask(input, opts = {}) {
2315
2361
  return batchExecute("batch_complete_task", input.ids, (id) => completeTask({ id }), opts);
2316
2362
  }
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
- )
2363
+ var { schema: deleteTaskSchema, handler: deleteTask } = defineDelete({
2364
+ toolName: "delete_task",
2365
+ pathPrefix: "/tasks",
2366
+ confirmHint: "Must be set to true. Permanently deletes the task. To mark done without losing history use complete_task. Irreversible."
2322
2367
  });
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
2368
 
2334
2369
  // src/tools/entries.ts
2335
- import { z as z8 } from "zod";
2370
+ import { z as z11 } from "zod";
2336
2371
  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)
2372
+ page: z11.number().int().positive().optional().default(1),
2373
+ perPage: z11.number().int().min(1).max(100).optional().default(25),
2374
+ embed: z11.string().optional().describe(EMBED_ATTACHMENTS_PARTICIPANTS_DESCRIPTION)
2340
2375
  };
2341
- var listPartyEntriesSchema = z8.object({
2342
- partyId: z8.number().int().positive(),
2376
+ var listPartyEntriesSchema = z11.object({
2377
+ partyId: positiveId,
2343
2378
  ...listEntriesPagination
2344
2379
  });
2345
2380
  async function listPartyEntries(input) {
@@ -2349,8 +2384,8 @@ async function listPartyEntries(input) {
2349
2384
  );
2350
2385
  return { ...data, nextPage };
2351
2386
  }
2352
- var listOpportunityEntriesSchema = z8.object({
2353
- opportunityId: z8.number().int().positive(),
2387
+ var listOpportunityEntriesSchema = z11.object({
2388
+ opportunityId: positiveId,
2354
2389
  ...listEntriesPagination
2355
2390
  });
2356
2391
  async function listOpportunityEntries(input) {
@@ -2360,8 +2395,8 @@ async function listOpportunityEntries(input) {
2360
2395
  );
2361
2396
  return { ...data, nextPage };
2362
2397
  }
2363
- var listProjectEntriesSchema = z8.object({
2364
- projectId: z8.number().int().positive(),
2398
+ var listProjectEntriesSchema = z11.object({
2399
+ projectId: positiveId,
2365
2400
  ...listEntriesPagination
2366
2401
  });
2367
2402
  async function listProjectEntries(input) {
@@ -2371,9 +2406,9 @@ async function listProjectEntries(input) {
2371
2406
  );
2372
2407
  return { ...data, nextPage };
2373
2408
  }
2374
- var getEntrySchema = z8.object({
2375
- id: z8.number().int().positive(),
2376
- embed: z8.string().optional().describe(EMBED_ATTACHMENTS_PARTICIPANTS_DESCRIPTION)
2409
+ var getEntrySchema = z11.object({
2410
+ id: positiveId,
2411
+ embed: z11.string().optional().describe(EMBED_ATTACHMENTS_PARTICIPANTS_DESCRIPTION)
2377
2412
  });
2378
2413
  async function getEntry(input) {
2379
2414
  const { data } = await capsuleGet(`/entries/${input.id}`, {
@@ -2381,7 +2416,7 @@ async function getEntry(input) {
2381
2416
  });
2382
2417
  return data;
2383
2418
  }
2384
- var listEntriesSchema = z8.object({
2419
+ var listEntriesSchema = z11.object({
2385
2420
  ...listEntriesPagination
2386
2421
  });
2387
2422
  async function listEntries(input) {
@@ -2392,14 +2427,14 @@ async function listEntries(input) {
2392
2427
  });
2393
2428
  return { ...data, nextPage };
2394
2429
  }
2395
- var addNoteSchema = z8.object({
2396
- content: z8.string().min(1).describe(
2430
+ var addNoteSchema = z11.object({
2431
+ content: z11.string().min(1).describe(
2397
2432
  "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
2433
  ),
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(
2434
+ partyId: positiveId.optional().describe("Link note to a party (mutually exclusive with opportunityId/projectId)"),
2435
+ opportunityId: positiveId.optional().describe("Link note to an opportunity (mutually exclusive with partyId/projectId)"),
2436
+ projectId: positiveId.optional().describe("Link note to a project (mutually exclusive with partyId/opportunityId)"),
2437
+ 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
2438
  "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
2439
  )
2405
2440
  });
@@ -2410,18 +2445,18 @@ async function addNote(input) {
2410
2445
  throw new Error("Provide exactly one of partyId, opportunityId, or projectId");
2411
2446
  }
2412
2447
  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 };
2448
+ setRef(body, "party", partyId);
2449
+ setRef(body, "opportunity", opportunityId);
2450
+ setRef(body, "kase", projectId);
2416
2451
  if (entryAt !== void 0) body["entryAt"] = entryAt;
2417
2452
  return capsulePost("/entries", { entry: body });
2418
2453
  }
2419
- var updateEntrySchema = z8.object({
2420
- id: z8.number().int().positive().describe("Entry ID to update"),
2421
- content: z8.string().min(1).optional().describe(
2454
+ var updateEntrySchema = z11.object({
2455
+ id: positiveId.describe("Entry ID to update"),
2456
+ content: z11.string().min(1).optional().describe(
2422
2457
  "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
2458
  ),
2424
- subject: z8.string().optional().describe(
2459
+ subject: z11.string().optional().describe(
2425
2460
  "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
2461
  )
2427
2462
  });
@@ -2435,30 +2470,20 @@ async function updateEntry(input) {
2435
2470
  }
2436
2471
  return capsulePut(`/entries/${id}`, { entry: body });
2437
2472
  }
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
- )
2473
+ var { schema: deleteEntrySchema, handler: deleteEntry } = defineDelete({
2474
+ toolName: "delete_entry",
2475
+ pathPrefix: "/entries",
2476
+ confirmHint: "Must be set to true. Permanently deletes the entry \u2014 use this to remove a note from a party/opportunity/project. Irreversible.",
2477
+ idDescription: "Entry (note/email/task-record) ID"
2443
2478
  });
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
2479
 
2455
2480
  // src/tools/pipelines.ts
2456
- import { z as z9 } from "zod";
2481
+ import { z as z12 } from "zod";
2457
2482
  var paginationFields = {
2458
- page: z9.number().int().positive().optional(),
2459
- perPage: z9.number().int().min(1).max(100).optional()
2483
+ page: z12.number().int().positive().optional(),
2484
+ perPage: z12.number().int().min(1).max(100).optional()
2460
2485
  };
2461
- var listPipelinesSchema = z9.object({ ...paginationFields });
2486
+ var listPipelinesSchema = z12.object({ ...paginationFields });
2462
2487
  async function listPipelines(input) {
2463
2488
  const { data, nextPage } = await capsuleGetCached("/pipelines", {
2464
2489
  page: input.page ?? 1,
@@ -2466,8 +2491,8 @@ async function listPipelines(input) {
2466
2491
  });
2467
2492
  return { ...data, nextPage };
2468
2493
  }
2469
- var listMilestonesSchema = z9.object({
2470
- pipelineId: z9.number().int().positive(),
2494
+ var listMilestonesSchema = z12.object({
2495
+ pipelineId: positiveId,
2471
2496
  ...paginationFields
2472
2497
  });
2473
2498
  async function listMilestones(input) {
@@ -2479,12 +2504,12 @@ async function listMilestones(input) {
2479
2504
  }
2480
2505
 
2481
2506
  // src/tools/boards.ts
2482
- import { z as z10 } from "zod";
2507
+ import { z as z13 } from "zod";
2483
2508
  var paginationFields2 = {
2484
- page: z10.number().int().positive().optional(),
2485
- perPage: z10.number().int().min(1).max(100).optional()
2509
+ page: z13.number().int().positive().optional(),
2510
+ perPage: z13.number().int().min(1).max(100).optional()
2486
2511
  };
2487
- var listBoardsSchema = z10.object({ ...paginationFields2 });
2512
+ var listBoardsSchema = z13.object({ ...paginationFields2 });
2488
2513
  async function listBoards(input) {
2489
2514
  const { data, nextPage } = await capsuleGetCached("/boards", {
2490
2515
  page: input.page ?? 1,
@@ -2492,8 +2517,8 @@ async function listBoards(input) {
2492
2517
  });
2493
2518
  return { ...data, nextPage };
2494
2519
  }
2495
- var listStagesSchema = z10.object({
2496
- boardId: z10.number().int().positive().optional().describe(
2520
+ var listStagesSchema = z13.object({
2521
+ boardId: positiveId.optional().describe(
2497
2522
  "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
2523
  ),
2499
2524
  ...paginationFields2
@@ -2508,7 +2533,7 @@ async function listStages(input) {
2508
2533
  }
2509
2534
 
2510
2535
  // src/tools/tags.ts
2511
- import { z as z11 } from "zod";
2536
+ import { z as z14 } from "zod";
2512
2537
  var TAG_LIST_PATH = {
2513
2538
  parties: "/parties/tags",
2514
2539
  opportunities: "/opportunities/tags",
@@ -2519,11 +2544,11 @@ var ENTITY_TO_WRAPPER = {
2519
2544
  opportunities: "opportunity",
2520
2545
  kases: "kase"
2521
2546
  };
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()
2547
+ var TagEntity = z14.enum(["parties", "opportunities", "kases"]).describe("Which entity type. Use 'kases' for projects (Capsule's legacy path name).");
2548
+ var listTagsSchema = z14.object({
2549
+ entity: z14.enum(["parties", "opportunities", "kases"]).describe("The resource type to list tags for"),
2550
+ page: z14.number().int().positive().optional(),
2551
+ perPage: z14.number().int().min(1).max(100).optional()
2527
2552
  });
2528
2553
  async function listTags(input) {
2529
2554
  const path = TAG_LIST_PATH[input.entity];
@@ -2533,10 +2558,10 @@ async function listTags(input) {
2533
2558
  });
2534
2559
  return { ...data, nextPage };
2535
2560
  }
2536
- var addTagSchema = z11.object({
2561
+ var addTagSchema = z14.object({
2537
2562
  entity: TagEntity,
2538
- entityId: z11.number().int().positive().describe("The party/opportunity/kase id."),
2539
- tagName: z11.string().min(1).describe(
2563
+ entityId: positiveId.describe("The party/opportunity/kase id."),
2564
+ tagName: z14.string().min(1).describe(
2540
2565
  "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
2566
  )
2542
2567
  });
@@ -2549,10 +2574,10 @@ async function addTag(input) {
2549
2574
  invalidateByPrefix(TAG_LIST_PATH[entity], "add_tag");
2550
2575
  return result;
2551
2576
  }
2552
- var removeTagByIdSchema = z11.object({
2577
+ var removeTagByIdSchema = z14.object({
2553
2578
  entity: TagEntity,
2554
- entityId: z11.number().int().positive().describe("The party/opportunity/kase id."),
2555
- tagId: z11.number().int().positive().describe(
2579
+ entityId: positiveId.describe("The party/opportunity/kase id."),
2580
+ tagId: positiveId.describe(
2556
2581
  "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
2582
  )
2558
2583
  });
@@ -2580,28 +2605,24 @@ async function removeTagById(input) {
2580
2605
  invalidateByPrefix(TAG_LIST_PATH[entity], "remove_tag_by_id");
2581
2606
  return result;
2582
2607
  }
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
- )
2608
+ var { schema: batchAddTagSchema, handler: batchAddTag } = defineBatch({
2609
+ toolName: "batch_add_tag",
2610
+ itemSchema: addTagSchema,
2611
+ 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.",
2612
+ itemHandler: addTag
2587
2613
  });
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
- )
2614
+ var { schema: batchRemoveTagByIdSchema, handler: batchRemoveTagById } = defineBatch({
2615
+ toolName: "batch_remove_tag_by_id",
2616
+ itemSchema: removeTagByIdSchema,
2617
+ 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.",
2618
+ itemHandler: removeTagById
2595
2619
  });
2596
- async function batchRemoveTagById(input, opts = {}) {
2597
- return batchExecute("batch_remove_tag_by_id", input.items, (item) => removeTagById(item), opts);
2598
- }
2599
2620
 
2600
2621
  // 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()
2622
+ import { z as z15 } from "zod";
2623
+ var listUsersSchema = z15.object({
2624
+ page: z15.number().int().positive().optional(),
2625
+ perPage: z15.number().int().min(1).max(100).optional()
2605
2626
  });
2606
2627
  async function listUsers(input) {
2607
2628
  const { data, nextPage } = await capsuleGetCached("/users", {
@@ -2610,32 +2631,32 @@ async function listUsers(input) {
2610
2631
  });
2611
2632
  return { ...data, nextPage };
2612
2633
  }
2613
- var getCurrentUserSchema = z12.object({});
2634
+ var getCurrentUserSchema = z15.object({});
2614
2635
  async function getCurrentUser(_input) {
2615
2636
  const { data } = await capsuleGet("/users/current");
2616
2637
  return data;
2617
2638
  }
2618
2639
 
2619
2640
  // src/tools/filters.ts
2620
- import { z as z13 } from "zod";
2621
- var FilterConditionSchema = z13.object({
2622
- field: z13.string().describe(
2641
+ import { z as z16 } from "zod";
2642
+ var FilterConditionSchema = z16.object({
2643
+ field: z16.string().describe(
2623
2644
  "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
2645
  ),
2625
- operator: z13.string().describe(
2646
+ operator: z16.string().describe(
2626
2647
  "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
2648
  ),
2628
- value: z13.union([z13.string(), z13.number(), z13.boolean(), z13.null()]).describe(
2649
+ value: z16.union([z16.string(), z16.number(), z16.boolean(), z16.null()]).describe(
2629
2650
  "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
2651
  )
2631
2652
  });
2632
- var FilterInputSchema = z13.object({
2633
- conditions: z13.array(FilterConditionSchema).min(1).describe(
2653
+ var FilterInputSchema = z16.object({
2654
+ conditions: z16.array(FilterConditionSchema).min(1).describe(
2634
2655
  "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
2656
  ),
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)
2657
+ embed: z16.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
2658
+ page: z16.number().int().positive().optional().default(1),
2659
+ perPage: z16.number().int().min(1).max(100).optional().default(25)
2639
2660
  });
2640
2661
  async function runFilter(entityPath, input) {
2641
2662
  const { data, nextPage } = await capsuleSearch(
@@ -2666,12 +2687,12 @@ async function filterProjects(input) {
2666
2687
  }
2667
2688
 
2668
2689
  // src/tools/metadata.ts
2669
- import { z as z14 } from "zod";
2690
+ import { z as z17 } from "zod";
2670
2691
  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.")
2692
+ page: z17.number().int().positive().optional(),
2693
+ perPage: z17.number().int().min(1).max(100).optional().describe("Page size, max 100. Defaults to 100 for reference data.")
2673
2694
  };
2674
- var listTeamsSchema = z14.object({ ...paginationFields3 });
2695
+ var listTeamsSchema = z17.object({ ...paginationFields3 });
2675
2696
  async function listTeams(input) {
2676
2697
  const { data, nextPage } = await capsuleGetCached("/teams", {
2677
2698
  page: input.page ?? 1,
@@ -2679,7 +2700,7 @@ async function listTeams(input) {
2679
2700
  });
2680
2701
  return { ...data, nextPage };
2681
2702
  }
2682
- var listLostReasonsSchema = z14.object({ ...paginationFields3 });
2703
+ var listLostReasonsSchema = z17.object({ ...paginationFields3 });
2683
2704
  async function listLostReasons(input) {
2684
2705
  const { data, nextPage } = await capsuleGetCached("/lostreasons", {
2685
2706
  page: input.page ?? 1,
@@ -2687,7 +2708,7 @@ async function listLostReasons(input) {
2687
2708
  });
2688
2709
  return { ...data, nextPage };
2689
2710
  }
2690
- var listActivityTypesSchema = z14.object({ ...paginationFields3 });
2711
+ var listActivityTypesSchema = z17.object({ ...paginationFields3 });
2691
2712
  async function listActivityTypes(input) {
2692
2713
  const { data, nextPage } = await capsuleGetCached(
2693
2714
  "/activitytypes",
@@ -2698,12 +2719,12 @@ async function listActivityTypes(input) {
2698
2719
  );
2699
2720
  return { ...data, nextPage };
2700
2721
  }
2701
- var getSiteSchema = z14.object({});
2722
+ var getSiteSchema = z17.object({});
2702
2723
  async function getSite(_input) {
2703
2724
  const { data } = await capsuleGetCached("/site");
2704
2725
  return data;
2705
2726
  }
2706
- var listTrackDefinitionsSchema = z14.object({ ...paginationFields3 });
2727
+ var listTrackDefinitionsSchema = z17.object({ ...paginationFields3 });
2707
2728
  async function listTrackDefinitions(input) {
2708
2729
  const { data, nextPage } = await capsuleGetCached(
2709
2730
  "/trackdefinitions",
@@ -2711,7 +2732,7 @@ async function listTrackDefinitions(input) {
2711
2732
  );
2712
2733
  return { ...data, nextPage };
2713
2734
  }
2714
- var listCategoriesSchema = z14.object({ ...paginationFields3 });
2735
+ var listCategoriesSchema = z17.object({ ...paginationFields3 });
2715
2736
  async function listCategories(input) {
2716
2737
  const { data, nextPage } = await capsuleGetCached("/categories", {
2717
2738
  page: input.page ?? 1,
@@ -2719,7 +2740,7 @@ async function listCategories(input) {
2719
2740
  });
2720
2741
  return { ...data, nextPage };
2721
2742
  }
2722
- var listGoalsSchema = z14.object({ ...paginationFields3 });
2743
+ var listGoalsSchema = z17.object({ ...paginationFields3 });
2723
2744
  async function listGoals(input) {
2724
2745
  const { data, nextPage } = await capsuleGetCached("/goals", {
2725
2746
  page: input.page ?? 1,
@@ -2729,14 +2750,14 @@ async function listGoals(input) {
2729
2750
  }
2730
2751
 
2731
2752
  // src/tools/audit.ts
2732
- import { z as z15 } from "zod";
2733
- var listEmployeesSchema = z15.object({
2734
- partyId: z15.number().int().positive().describe(
2753
+ import { z as z18 } from "zod";
2754
+ var listEmployeesSchema = z18.object({
2755
+ partyId: positiveId.describe(
2735
2756
  "The organisation's party id. Returns the people whose `organisation` field links to this party."
2736
2757
  ),
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)
2758
+ page: z18.number().int().positive().optional().default(1),
2759
+ perPage: z18.number().int().min(1).max(100).optional().default(25),
2760
+ embed: z18.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
2740
2761
  });
2741
2762
  async function listEmployees(input) {
2742
2763
  const { data, nextPage } = await capsuleGet(
@@ -2745,15 +2766,15 @@ async function listEmployees(input) {
2745
2766
  );
2746
2767
  return { ...data, nextPage };
2747
2768
  }
2748
- var DeletedSinceSchema = z15.string().describe(
2769
+ var DeletedSinceSchema = z18.string().describe(
2749
2770
  "REQUIRED. ISO-8601 timestamp; only deletions on or after this point are returned. Example: '2026-01-01T00:00:00Z'."
2750
2771
  );
2751
2772
  var DeletedPagination = {
2752
2773
  since: DeletedSinceSchema,
2753
- page: z15.number().int().positive().optional().default(1),
2754
- perPage: z15.number().int().min(1).max(100).optional().default(25)
2774
+ page: z18.number().int().positive().optional().default(1),
2775
+ perPage: z18.number().int().min(1).max(100).optional().default(25)
2755
2776
  };
2756
- var listDeletedPartiesSchema = z15.object(DeletedPagination);
2777
+ var listDeletedPartiesSchema = z18.object(DeletedPagination);
2757
2778
  async function listDeletedParties(input) {
2758
2779
  const { data, nextPage } = await capsuleGet("/parties/deleted", {
2759
2780
  since: input.since,
@@ -2762,7 +2783,7 @@ async function listDeletedParties(input) {
2762
2783
  });
2763
2784
  return { ...data, nextPage };
2764
2785
  }
2765
- var listDeletedOpportunitiesSchema = z15.object(DeletedPagination);
2786
+ var listDeletedOpportunitiesSchema = z18.object(DeletedPagination);
2766
2787
  async function listDeletedOpportunities(input) {
2767
2788
  const { data, nextPage } = await capsuleGet("/opportunities/deleted", {
2768
2789
  since: input.since,
@@ -2771,7 +2792,7 @@ async function listDeletedOpportunities(input) {
2771
2792
  });
2772
2793
  return { ...data, nextPage };
2773
2794
  }
2774
- var listDeletedProjectsSchema = z15.object(DeletedPagination);
2795
+ var listDeletedProjectsSchema = z18.object(DeletedPagination);
2775
2796
  async function listDeletedProjects(input) {
2776
2797
  const { data, nextPage } = await capsuleGet("/kases/deleted", {
2777
2798
  since: input.since,
@@ -2782,14 +2803,14 @@ async function listDeletedProjects(input) {
2782
2803
  }
2783
2804
 
2784
2805
  // 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({
2806
+ import { z as z19 } from "zod";
2807
+ var RelationshipEntity = z19.enum(["opportunities", "kases"]).describe("Which entity has the additional-party links. Use 'kases' for projects.");
2808
+ var listAdditionalPartiesSchema = z19.object({
2788
2809
  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)
2810
+ entityId: positiveId.describe("ID of the opportunity or project."),
2811
+ embed: z19.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
2812
+ page: z19.number().int().positive().optional().default(1),
2813
+ perPage: z19.number().int().min(1).max(100).optional().default(25)
2793
2814
  });
2794
2815
  async function listAdditionalParties(input) {
2795
2816
  const { data, nextPage } = await capsuleGet(
@@ -2798,10 +2819,12 @@ async function listAdditionalParties(input) {
2798
2819
  );
2799
2820
  return { ...data, nextPage };
2800
2821
  }
2801
- var addAdditionalPartySchema = z16.object({
2822
+ var addAdditionalPartySchema = z19.object({
2802
2823
  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.")
2824
+ entityId: positiveId,
2825
+ partyId: positiveId.describe(
2826
+ "ID of the party (person or organisation) to link as an additional party."
2827
+ )
2805
2828
  });
2806
2829
  async function addAdditionalParty(input) {
2807
2830
  try {
@@ -2829,10 +2852,10 @@ async function addAdditionalParty(input) {
2829
2852
  throw err;
2830
2853
  }
2831
2854
  }
2832
- var removeAdditionalPartySchema = z16.object({
2855
+ var removeAdditionalPartySchema = z19.object({
2833
2856
  entity: RelationshipEntity,
2834
- entityId: z16.number().int().positive(),
2835
- partyId: z16.number().int().positive(),
2857
+ entityId: positiveId,
2858
+ partyId: positiveId,
2836
2859
  confirm: confirmFlag().describe(
2837
2860
  "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
2861
  )
@@ -2859,11 +2882,11 @@ async function removeAdditionalParty(input) {
2859
2882
  })
2860
2883
  );
2861
2884
  }
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)
2885
+ var listAssociatedProjectsSchema = z19.object({
2886
+ opportunityId: positiveId,
2887
+ embed: z19.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
2888
+ page: z19.number().int().positive().optional().default(1),
2889
+ perPage: z19.number().int().min(1).max(100).optional().default(25)
2867
2890
  });
2868
2891
  async function listAssociatedProjects(input) {
2869
2892
  const { data, nextPage } = await capsuleGet(
@@ -2874,9 +2897,9 @@ async function listAssociatedProjects(input) {
2874
2897
  }
2875
2898
 
2876
2899
  // 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({
2900
+ import { z as z20 } from "zod";
2901
+ var CustomFieldEntity = z20.enum(["parties", "opportunities", "kases"]).describe("Which entity type's custom field schema to inspect. Use 'kases' for projects.");
2902
+ var listCustomFieldsSchema = z20.object({
2880
2903
  entity: CustomFieldEntity
2881
2904
  });
2882
2905
  async function listCustomFields(input) {
@@ -2885,9 +2908,9 @@ async function listCustomFields(input) {
2885
2908
  );
2886
2909
  return data;
2887
2910
  }
2888
- var getCustomFieldSchema = z17.object({
2911
+ var getCustomFieldSchema = z20.object({
2889
2912
  entity: CustomFieldEntity,
2890
- fieldId: z17.number().int().positive().describe("Custom field definition id.")
2913
+ fieldId: positiveId.describe("Custom field definition id.")
2891
2914
  });
2892
2915
  async function getCustomField(input) {
2893
2916
  const { data } = await capsuleGetCached(
@@ -2897,11 +2920,11 @@ async function getCustomField(input) {
2897
2920
  }
2898
2921
 
2899
2922
  // 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({
2923
+ import { z as z21 } from "zod";
2924
+ var TrackEntity = z21.enum(["parties", "opportunities", "kases"]).describe("Use 'kases' for projects.");
2925
+ var listEntityTracksSchema = z21.object({
2903
2926
  entity: TrackEntity,
2904
- entityId: z18.number().int().positive()
2927
+ entityId: positiveId
2905
2928
  });
2906
2929
  async function listEntityTracks(input) {
2907
2930
  const { data } = await capsuleGet(
@@ -2909,20 +2932,20 @@ async function listEntityTracks(input) {
2909
2932
  );
2910
2933
  return data;
2911
2934
  }
2912
- var showTrackSchema = z18.object({
2913
- trackId: z18.number().int().positive()
2935
+ var showTrackSchema = z21.object({
2936
+ trackId: positiveId
2914
2937
  });
2915
2938
  async function showTrack(input) {
2916
2939
  const { data } = await capsuleGet(`/tracks/${input.trackId}`);
2917
2940
  return data;
2918
2941
  }
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(
2942
+ var applyTrackSchema = z21.object({
2943
+ entity: z21.enum(["opportunities", "kases"]).describe("Which entity to apply the track to. Use 'kases' for projects."),
2944
+ entityId: positiveId,
2945
+ trackDefinitionId: positiveId.describe(
2923
2946
  "The trackDefinition to apply (from list_track_definitions). Auto-creates task definitions on the target entity per the track's rules."
2924
2947
  ),
2925
- startDate: z18.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe(
2948
+ startDate: z21.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe(
2926
2949
  "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
2950
  )
2928
2951
  });
@@ -2935,9 +2958,9 @@ async function applyTrack(input) {
2935
2958
  if (input.startDate !== void 0) track["trackDateOn"] = input.startDate;
2936
2959
  return capsulePost("/tracks", { track });
2937
2960
  }
2938
- var updateTrackSchema = z18.object({
2939
- trackId: z18.number().int().positive(),
2940
- fields: z18.record(z18.string(), z18.unknown()).describe(
2961
+ var updateTrackSchema = z21.object({
2962
+ trackId: positiveId,
2963
+ fields: z21.record(z21.string(), z21.unknown()).describe(
2941
2964
  "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
2965
  )
2943
2966
  });
@@ -2949,8 +2972,8 @@ async function updateTrack(input) {
2949
2972
  track: input.fields
2950
2973
  });
2951
2974
  }
2952
- var removeTrackSchema = z18.object({
2953
- trackId: z18.number().int().positive(),
2975
+ var removeTrackSchema = z21.object({
2976
+ trackId: positiveId,
2954
2977
  confirm: confirmFlag().describe(
2955
2978
  "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
2979
  )
@@ -2967,13 +2990,13 @@ async function removeTrack(input) {
2967
2990
  }
2968
2991
 
2969
2992
  // src/tools/attachments.ts
2970
- import { z as z19 } from "zod";
2993
+ import { z as z22 } from "zod";
2971
2994
  var DEFAULT_MAX_SIZE_BYTES = 5 * 1024 * 1024;
2972
2995
  var HARD_MAX_SIZE_BYTES = 25 * 1024 * 1024;
2973
2996
  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(
2997
+ var getAttachmentSchema = z22.object({
2998
+ id: positiveId.describe("Attachment ID."),
2999
+ maxSizeBytes: z22.number().int().positive().max(HARD_MAX_SIZE_BYTES).optional().describe(
2977
3000
  `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
3001
  )
2979
3002
  });
@@ -2988,22 +3011,22 @@ async function getAttachment(input) {
2988
3011
  }
2989
3012
  return { contentType, buffer, sizeBytes };
2990
3013
  }
2991
- var uploadAttachmentSchema = z19.object({
2992
- filename: z19.string().min(1).describe(
3014
+ var uploadAttachmentSchema = z22.object({
3015
+ filename: z22.string().min(1).describe(
2993
3016
  "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
3017
  ),
2995
- contentType: z19.string().min(1).describe(
3018
+ contentType: z22.string().min(1).describe(
2996
3019
  "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
3020
  ),
2998
- dataBase64: z19.string().min(1).max(HARD_MAX_BASE64_CHARS).describe(
3021
+ dataBase64: z22.string().min(1).max(HARD_MAX_BASE64_CHARS).describe(
2999
3022
  "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
3023
  ),
3001
- content: z19.string().optional().describe(
3024
+ content: z22.string().optional().describe(
3002
3025
  "Body text for the note that will hold the attachment. Defaults to '[attachment]' if omitted."
3003
3026
  ),
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()
3027
+ partyId: positiveId.optional().describe("Link the new note to a party (mutually exclusive with opportunityId / projectId)."),
3028
+ opportunityId: positiveId.optional(),
3029
+ projectId: positiveId.optional()
3007
3030
  });
3008
3031
  function isValidBase64(s) {
3009
3032
  if (!/^[A-Za-z0-9+/]*={0,2}$/.test(s)) return false;
@@ -3053,23 +3076,23 @@ async function uploadAttachment(input) {
3053
3076
  }
3054
3077
 
3055
3078
  // src/tools/saved-filters.ts
3056
- import { z as z20 } from "zod";
3057
- var EntitySchema = z20.enum(["parties", "opportunities", "kases"]).describe(
3079
+ import { z as z23 } from "zod";
3080
+ var EntitySchema = z23.enum(["parties", "opportunities", "kases"]).describe(
3058
3081
  "Which entity type the filter operates over. Use 'kases' for projects (Capsule's legacy name)."
3059
3082
  );
3060
- var listSavedFiltersSchema = z20.object({
3083
+ var listSavedFiltersSchema = z23.object({
3061
3084
  entity: EntitySchema
3062
3085
  });
3063
3086
  async function listSavedFilters(input) {
3064
3087
  const { data } = await capsuleGetCached(`/${input.entity}/filters`);
3065
3088
  return data;
3066
3089
  }
3067
- var runSavedFilterSchema = z20.object({
3090
+ var runSavedFilterSchema = z23.object({
3068
3091
  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)
3092
+ id: positiveId.describe("The saved filter id (from list_saved_filters)."),
3093
+ embed: z23.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
3094
+ page: z23.number().int().positive().optional().default(1),
3095
+ perPage: z23.number().int().min(1).max(100).optional().default(25)
3073
3096
  });
3074
3097
  async function runSavedFilter(input) {
3075
3098
  const { data, nextPage } = await capsuleGet(
@@ -3087,7 +3110,7 @@ function createCapsuleMcpServer(opts) {
3087
3110
  const server = new McpServer(
3088
3111
  {
3089
3112
  name: "capsulemcp",
3090
- version: "1.6.1",
3113
+ version: "1.6.3",
3091
3114
  description: "Read and (optionally) modify Capsule CRM data \u2014 parties, opportunities, projects, tasks, timeline entries, pipelines, tags.",
3092
3115
  websiteUrl: "https://github.com/soil-dev/capsulemcp",
3093
3116
  icons: ICONS
@@ -3190,7 +3213,7 @@ function createCapsuleMcpServer(opts) {
3190
3213
  registerTool(
3191
3214
  server,
3192
3215
  "update_party",
3193
- "Update top-level fields on an existing party (about, firstName/lastName/name/title/jobTitle, ownerId). Only the fields you provide are changed. Child arrays (emailAddresses / phoneNumbers / addresses / websites) on this tool are APPEND-ONLY: items are merged into the existing list, not replaced. For surgical changes \u2014 replacing one email, removing one phone number, fixing the type on one address \u2014 use the dedicated atomic tools: add_party_email_address / remove_party_email_address_by_id (and the phone/address/website equivalents).",
3216
+ "Update top-level fields on an existing party (about, firstName/lastName/name/title/jobTitle, ownerId, organisationId). For PERSON parties, organisationId links to an organisation or null unlinks the person from its organisation; for ORGANISATION parties Capsule silently ignores organisationId. Only the fields you provide are changed. Child arrays (emailAddresses / phoneNumbers / addresses / websites) on this tool are APPEND-ONLY: items are merged into the existing list, not replaced. For surgical changes \u2014 replacing one email, removing one phone number, fixing the type on one address \u2014 use the dedicated atomic tools: add_party_email_address / remove_party_email_address_by_id (and the phone/address/website equivalents).",
3194
3217
  updatePartySchema,
3195
3218
  updateParty
3196
3219
  );
@@ -3325,7 +3348,7 @@ function createCapsuleMcpServer(opts) {
3325
3348
  registerTool(
3326
3349
  server,
3327
3350
  "update_opportunity",
3328
- "Update fields on an existing opportunity. Only the fields you provide are changed. Closed (Won/Lost) opportunities ARE editable \u2014 Capsule does not enforce closed-record immutability, so `value`, `description`, etc. can be changed on a Won opp without warning. If the workflow needs historical revenue numbers to be stable, enforce that caller-side.",
3351
+ "Update fields on an existing opportunity, including the parent-reference field `partyId` to reassign the opp to a different primary party. Only the fields you provide are changed. Closed (Won/Lost) opportunities ARE editable \u2014 Capsule does not enforce closed-record immutability, so `value`, `description`, etc. can be changed on a Won opp without warning. If the workflow needs historical revenue numbers to be stable, enforce that caller-side. Capsule requires every opportunity to have a party \u2014 passing `partyId: null` is rejected with 422 'party is required'.",
3329
3352
  updateOpportunitySchema,
3330
3353
  updateOpportunity
3331
3354
  );
@@ -3390,7 +3413,7 @@ function createCapsuleMcpServer(opts) {
3390
3413
  registerTool(
3391
3414
  server,
3392
3415
  "update_project",
3393
- "Update fields on an existing project. Only the fields you provide are changed. Use status='CLOSED' to close a project. CLOSED projects remain fully editable \u2014 Capsule does not enforce closed-record immutability. Stage moves and description edits on a CLOSED project are accepted without warning.",
3416
+ "Update fields on an existing project, including the parent-reference field `partyId` to reassign the project to a different primary party. Only the fields you provide are changed. Use status='CLOSED' to close a project. CLOSED projects remain fully editable \u2014 Capsule does not enforce closed-record immutability. Stage moves and description edits on a CLOSED project are accepted without warning. Capsule requires every project to have a party \u2014 passing `partyId: null` is rejected with 422 'party is required'.",
3394
3417
  updateProjectSchema,
3395
3418
  updateProject
3396
3419
  );
@@ -3469,7 +3492,7 @@ function createCapsuleMcpServer(opts) {
3469
3492
  registerTool(
3470
3493
  server,
3471
3494
  "update_task",
3472
- "Update fields on an existing task: `description`, `dueOn`, `dueTime`, `detail`, `status` (OPEN or COMPLETED), and `ownerId`. Only the fields you provide are changed. To mark a task done, prefer the dedicated `complete_task` tool \u2014 it's idempotent (a no-op success on an already-completed task) and semantically clearer than `update_task status=COMPLETED`. Capsule rejects directly setting status=PENDING (which exists only internally for track-driven tasks); use OPEN or COMPLETED. Completed tasks remain fully editable \u2014 Capsule does not enforce closed-record immutability.",
3495
+ "Update fields on an existing task: `description`, `dueOn`, `dueTime`, `detail`, `status` (OPEN or COMPLETED), `ownerId`, and the parent-reference fields `partyId`, `opportunityId`, `projectId`. Pass a parent id to re-link the task, or null on a parent field to orphan/unlink it; at most one parent id may be set in a single call, though null+id swaps are allowed. Only the fields you provide are changed. To mark a task done, prefer the dedicated `complete_task` tool \u2014 it's idempotent (a no-op success on an already-completed task) and semantically clearer than `update_task status=COMPLETED`. Capsule rejects directly setting status=PENDING (which exists only internally for track-driven tasks); use OPEN or COMPLETED. Completed tasks remain fully editable \u2014 Capsule does not enforce closed-record immutability.",
3473
3496
  updateTaskSchema,
3474
3497
  updateTask
3475
3498
  );
@@ -3672,7 +3695,7 @@ function createCapsuleMcpServer(opts) {
3672
3695
  registerTool(
3673
3696
  server,
3674
3697
  "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.",
3698
+ "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
3699
  listTeamsSchema,
3677
3700
  listTeams
3678
3701
  );