capsulemcp 1.6.1 → 1.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -871,18 +871,19 @@ function registerToolTask(server2, name, description, schema, handler) {
871
871
  }
872
872
 
873
873
  // src/tools/parties.ts
874
- import { z as z3 } from "zod";
874
+ import { z as z6 } from "zod";
875
875
 
876
- // src/tools/descriptions.ts
877
- var EMBED_TAGS_FIELDS_DESCRIPTION = "Comma-separated embeds, e.g. 'tags,fields'";
878
- var EMBED_ATTACHMENTS_PARTICIPANTS_DESCRIPTION = "Comma-separated embeds, e.g. 'attachments,participants'";
876
+ // src/tools/body-helpers.ts
877
+ function setRef(body, key, id) {
878
+ if (id) body[key] = { id };
879
+ }
880
+ function setNullableRef(body, key, id) {
881
+ if (id === null) body[key] = null;
882
+ else if (id !== void 0) body[key] = { id };
883
+ }
879
884
 
880
- // src/tools/confirm-flag.ts
885
+ // src/tools/define-batch.ts
881
886
  import { z } from "zod";
882
- var CONFIRM_REQUIRED_MESSAGE = "confirm: true is required to perform this destructive operation (set the parameter explicitly to acknowledge the destructive intent)";
883
- function confirmFlag() {
884
- return z.literal(true, { error: () => CONFIRM_REQUIRED_MESSAGE });
885
- }
886
887
 
887
888
  // src/capsule/batch.ts
888
889
  function chunk(arr, size) {
@@ -976,6 +977,39 @@ function topFailureReasons(results, n) {
976
977
  return Array.from(counts.values()).sort((a, b) => b.count - a.count).slice(0, n);
977
978
  }
978
979
 
980
+ // src/tools/define-batch.ts
981
+ function defineBatch(args) {
982
+ const schema = z.object({
983
+ items: z.array(args.itemSchema).min(1).max(50).describe(args.itemDescription)
984
+ });
985
+ async function handler(input, opts = {}) {
986
+ return batchExecute(args.toolName, input.items, args.itemHandler, opts);
987
+ }
988
+ return { schema, handler };
989
+ }
990
+
991
+ // src/tools/descriptions.ts
992
+ var EMBED_TAGS_FIELDS_DESCRIPTION = "Comma-separated embeds, e.g. 'tags,fields'";
993
+ var EMBED_ATTACHMENTS_PARTICIPANTS_DESCRIPTION = "Comma-separated embeds, e.g. 'attachments,participants'";
994
+
995
+ // src/tools/define-delete.ts
996
+ import { z as z4 } from "zod";
997
+
998
+ // src/tools/confirm-flag.ts
999
+ import { z as z2 } from "zod";
1000
+ var CONFIRM_REQUIRED_MESSAGE = "confirm: true is required to perform this destructive operation (set the parameter explicitly to acknowledge the destructive intent)";
1001
+ function confirmFlag() {
1002
+ return z2.literal(true, { error: () => CONFIRM_REQUIRED_MESSAGE });
1003
+ }
1004
+
1005
+ // src/tools/shared-schemas.ts
1006
+ import { z as z3 } from "zod";
1007
+ var positiveId = z3.preprocess((input) => {
1008
+ if (typeof input !== "string") return input;
1009
+ const trimmed = input.trim();
1010
+ return /^\d+$/.test(trimmed) ? Number(trimmed) : input;
1011
+ }, z3.number().int().positive());
1012
+
979
1013
  // src/capsule/idempotent.ts
980
1014
  var isCapsule404 = (err) => err instanceof CapsuleApiError && err.status === 404;
981
1015
  var isCapsuleTagNotFound = (err) => err instanceof CapsuleApiError && err.status === 422 && /tag not found/i.test(err.message);
@@ -998,13 +1032,33 @@ async function idempotentWithResult(op, success, alreadyDone, isAlreadyDoneError
998
1032
  }
999
1033
  }
1000
1034
 
1035
+ // src/tools/define-delete.ts
1036
+ function defineDelete(args) {
1037
+ const { toolName, pathPrefix, confirmHint, idDescription } = args;
1038
+ const schema = z4.object({
1039
+ id: idDescription ? positiveId.describe(idDescription) : positiveId,
1040
+ confirm: confirmFlag().describe(confirmHint)
1041
+ });
1042
+ async function handler(input) {
1043
+ if (input.confirm !== true) {
1044
+ throw new Error(`${toolName} requires confirm: true`);
1045
+ }
1046
+ return idempotent(
1047
+ () => capsuleDelete(`${pathPrefix}/${input.id}`),
1048
+ () => ({ deleted: true, alreadyDeleted: false, id: input.id }),
1049
+ () => ({ deleted: true, alreadyDeleted: true, id: input.id })
1050
+ );
1051
+ }
1052
+ return { schema, handler };
1053
+ }
1054
+
1001
1055
  // src/tools/custom-field-helpers.ts
1002
- import { z as z2 } from "zod";
1003
- var CustomFieldWriteSchema = z2.object({
1004
- definitionId: z2.number().int().positive().describe(
1056
+ import { z as z5 } from "zod";
1057
+ var CustomFieldWriteSchema = z5.object({
1058
+ definitionId: positiveId.describe(
1005
1059
  "The custom-field definition id from list_custom_fields. Identifies which field on the entity to set."
1006
1060
  ),
1007
- value: z2.union([z2.string(), z2.number(), z2.boolean(), z2.null()]).describe(
1061
+ value: z5.union([z5.string(), z5.number(), z5.boolean(), z5.null()]).describe(
1008
1062
  "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."
1009
1063
  )
1010
1064
  });
@@ -1020,24 +1074,24 @@ function mapFieldsForBody(fields) {
1020
1074
  }
1021
1075
 
1022
1076
  // src/tools/parties.ts
1023
- var EmailAddressSchema = z3.object({
1024
- address: z3.string().email(),
1025
- type: z3.string().optional()
1077
+ var EmailAddressSchema = z6.object({
1078
+ address: z6.string().email(),
1079
+ type: z6.string().optional()
1026
1080
  });
1027
- var PhoneNumberSchema = z3.object({
1081
+ var PhoneNumberSchema = z6.object({
1028
1082
  // Capsule rejects empty strings with `phoneNumber.number: number is
1029
1083
  // required`. Enforce at the schema layer to catch typos pre-call,
1030
1084
  // matching how EmailAddressSchema's address field behaves.
1031
- number: z3.string().min(1),
1032
- type: z3.string().optional()
1085
+ number: z6.string().min(1),
1086
+ type: z6.string().optional()
1033
1087
  });
1034
1088
  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.";
1035
- var AddressSchema = z3.object({
1036
- street: z3.string().optional(),
1037
- city: z3.string().optional(),
1038
- state: z3.string().optional(),
1039
- country: z3.string().optional().describe(CountryDescription),
1040
- zip: z3.string().optional()
1089
+ var AddressSchema = z6.object({
1090
+ street: z6.string().optional(),
1091
+ city: z6.string().optional(),
1092
+ state: z6.string().optional(),
1093
+ country: z6.string().optional().describe(CountryDescription),
1094
+ zip: z6.string().optional()
1041
1095
  });
1042
1096
  function validateWebsiteAddress(data, ctx) {
1043
1097
  const isUrlService = data.service === void 0 || data.service === "URL";
@@ -1060,7 +1114,7 @@ function validateWebsiteAddress(data, ctx) {
1060
1114
  });
1061
1115
  }
1062
1116
  }
1063
- var WebsiteServiceEnum = z3.enum([
1117
+ var WebsiteServiceEnum = z6.enum([
1064
1118
  "URL",
1065
1119
  "SKYPE",
1066
1120
  "TWITTER",
@@ -1079,19 +1133,19 @@ var WebsiteServiceEnum = z3.enum([
1079
1133
  "BLUESKY",
1080
1134
  "SNAPCHAT"
1081
1135
  ]);
1082
- var WebsiteSchema = z3.object({
1083
- address: z3.string().min(1).describe(
1136
+ var WebsiteSchema = z6.object({
1137
+ address: z6.string().min(1).describe(
1084
1138
  "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."
1085
1139
  ),
1086
1140
  service: WebsiteServiceEnum.optional().describe(
1087
1141
  "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."
1088
1142
  )
1089
1143
  }).superRefine(validateWebsiteAddress);
1090
- var searchPartiesSchema = z3.object({
1091
- q: z3.string().optional().describe("Free-text search query"),
1092
- embed: z3.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
1093
- page: z3.number().int().positive().optional().default(1),
1094
- perPage: z3.number().int().min(1).max(100).optional().default(25)
1144
+ var searchPartiesSchema = z6.object({
1145
+ q: z6.string().optional().describe("Free-text search query"),
1146
+ embed: z6.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
1147
+ page: z6.number().int().positive().optional().default(1),
1148
+ perPage: z6.number().int().min(1).max(100).optional().default(25)
1095
1149
  });
1096
1150
  async function searchParties(input) {
1097
1151
  const path = input.q ? "/parties/search" : "/parties";
@@ -1103,9 +1157,9 @@ async function searchParties(input) {
1103
1157
  });
1104
1158
  return { ...data, nextPage };
1105
1159
  }
1106
- var getPartySchema = z3.object({
1107
- id: z3.number().int().positive().describe("Party ID"),
1108
- embed: z3.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
1160
+ var getPartySchema = z6.object({
1161
+ id: positiveId.describe("Party ID"),
1162
+ embed: z6.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
1109
1163
  });
1110
1164
  async function getParty(input) {
1111
1165
  const { data } = await capsuleGet(`/parties/${input.id}`, {
@@ -1113,11 +1167,11 @@ async function getParty(input) {
1113
1167
  });
1114
1168
  return data;
1115
1169
  }
1116
- var getPartiesSchema = z3.object({
1117
- ids: z3.array(z3.number().int().positive()).min(1).max(50).describe(
1170
+ var getPartiesSchema = z6.object({
1171
+ ids: z6.array(positiveId).min(1).max(50).describe(
1118
1172
  "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."
1119
1173
  ),
1120
- embed: z3.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
1174
+ embed: z6.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
1121
1175
  });
1122
1176
  async function getParties(input) {
1123
1177
  const { ids, embed } = input;
@@ -1135,10 +1189,10 @@ async function getParties(input) {
1135
1189
  );
1136
1190
  return { parties: responses.flatMap((r) => r.data.parties) };
1137
1191
  }
1138
- var listPartyOpportunitiesSchema = z3.object({
1139
- partyId: z3.number().int().positive(),
1140
- page: z3.number().int().positive().optional().default(1),
1141
- perPage: z3.number().int().min(1).max(100).optional().default(25)
1192
+ var listPartyOpportunitiesSchema = z6.object({
1193
+ partyId: positiveId,
1194
+ page: z6.number().int().positive().optional().default(1),
1195
+ perPage: z6.number().int().min(1).max(100).optional().default(25)
1142
1196
  });
1143
1197
  async function listPartyOpportunities(input) {
1144
1198
  const { data, nextPage } = await capsuleGet(
@@ -1147,10 +1201,10 @@ async function listPartyOpportunities(input) {
1147
1201
  );
1148
1202
  return { ...data, nextPage };
1149
1203
  }
1150
- var listPartyProjectsSchema = z3.object({
1151
- partyId: z3.number().int().positive(),
1152
- page: z3.number().int().positive().optional().default(1),
1153
- perPage: z3.number().int().min(1).max(100).optional().default(25)
1204
+ var listPartyProjectsSchema = z6.object({
1205
+ partyId: positiveId,
1206
+ page: z6.number().int().positive().optional().default(1),
1207
+ perPage: z6.number().int().min(1).max(100).optional().default(25)
1154
1208
  });
1155
1209
  async function listPartyProjects(input) {
1156
1210
  const { data, nextPage } = await capsuleGet(
@@ -1160,50 +1214,50 @@ async function listPartyProjects(input) {
1160
1214
  return { ...data, nextPage };
1161
1215
  }
1162
1216
  var PartyWriteBaseSchema = {
1163
- about: z3.string().optional(),
1164
- emailAddresses: z3.array(EmailAddressSchema).optional().describe(
1217
+ about: z6.string().optional(),
1218
+ emailAddresses: z6.array(EmailAddressSchema).optional().describe(
1165
1219
  "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)."
1166
1220
  ),
1167
- phoneNumbers: z3.array(PhoneNumberSchema).optional().describe(
1221
+ phoneNumbers: z6.array(PhoneNumberSchema).optional().describe(
1168
1222
  "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."
1169
1223
  ),
1170
- addresses: z3.array(AddressSchema).optional().describe(
1224
+ addresses: z6.array(AddressSchema).optional().describe(
1171
1225
  "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)."
1172
1226
  ),
1173
- websites: z3.array(WebsiteSchema).optional().describe(
1227
+ websites: z6.array(WebsiteSchema).optional().describe(
1174
1228
  "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."
1175
1229
  ),
1176
- ownerId: z3.number().int().positive().optional().describe(
1230
+ ownerId: positiveId.optional().describe(
1177
1231
  "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."
1178
1232
  )
1179
1233
  };
1180
- var createPartySchema = z3.object({
1181
- type: z3.enum(["person", "organisation"]),
1234
+ var createPartySchema = z6.object({
1235
+ type: z6.enum(["person", "organisation"]),
1182
1236
  // person
1183
- firstName: z3.string().optional(),
1184
- lastName: z3.string().optional(),
1185
- title: z3.string().optional(),
1186
- jobTitle: z3.string().optional(),
1187
- organisationId: z3.number().int().positive().optional().describe("Link person to an existing organisation ID"),
1237
+ firstName: z6.string().optional(),
1238
+ lastName: z6.string().optional(),
1239
+ title: z6.string().optional(),
1240
+ jobTitle: z6.string().optional(),
1241
+ organisationId: positiveId.optional().describe("Link person to an existing organisation ID"),
1188
1242
  // organisation
1189
- name: z3.string().optional(),
1243
+ name: z6.string().optional(),
1190
1244
  ...PartyWriteBaseSchema
1191
1245
  });
1192
1246
  async function createParty(input) {
1193
1247
  const { ownerId, organisationId, ...rest } = input;
1194
1248
  const body = { ...rest };
1195
- if (ownerId) body["owner"] = { id: ownerId };
1196
- if (organisationId) body["organisation"] = { id: organisationId };
1249
+ setRef(body, "owner", ownerId);
1250
+ setRef(body, "organisation", organisationId);
1197
1251
  return capsulePost("/parties", { party: body });
1198
1252
  }
1199
- var updatePartySchema = z3.object({
1200
- id: z3.number().int().positive(),
1201
- firstName: z3.string().optional(),
1202
- lastName: z3.string().optional(),
1203
- title: z3.string().optional(),
1204
- jobTitle: z3.string().optional(),
1205
- name: z3.string().optional(),
1206
- fields: z3.array(CustomFieldWriteSchema).optional().describe(fieldsArrayDescriptor("get_party")),
1253
+ var updatePartySchema = z6.object({
1254
+ id: positiveId,
1255
+ firstName: z6.string().optional(),
1256
+ lastName: z6.string().optional(),
1257
+ title: z6.string().optional(),
1258
+ jobTitle: z6.string().optional(),
1259
+ name: z6.string().optional(),
1260
+ fields: z6.array(CustomFieldWriteSchema).optional().describe(fieldsArrayDescriptor("get_party")),
1207
1261
  ...PartyWriteBaseSchema
1208
1262
  });
1209
1263
  async function updateParty(input) {
@@ -1212,39 +1266,26 @@ async function updateParty(input) {
1212
1266
  for (const [k, v] of Object.entries(rest)) {
1213
1267
  if (v !== void 0) body[k] = v;
1214
1268
  }
1215
- if (ownerId) body["owner"] = { id: ownerId };
1269
+ setRef(body, "owner", ownerId);
1216
1270
  const mappedFields = mapFieldsForBody(fields);
1217
1271
  if (mappedFields !== void 0) body["fields"] = mappedFields;
1218
1272
  return capsulePut(`/parties/${id}`, { party: body });
1219
1273
  }
1220
- var batchUpdatePartySchema = z3.object({
1221
- items: z3.array(updatePartySchema).min(1).max(50).describe(
1222
- "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)."
1223
- )
1274
+ var { schema: batchUpdatePartySchema, handler: batchUpdateParty } = defineBatch({
1275
+ toolName: "batch_update_party",
1276
+ itemSchema: updatePartySchema,
1277
+ 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).",
1278
+ itemHandler: updateParty
1224
1279
  });
1225
- async function batchUpdateParty(input, opts = {}) {
1226
- return batchExecute("batch_update_party", input.items, (item) => updateParty(item), opts);
1227
- }
1228
- var deletePartySchema = z3.object({
1229
- id: z3.number().int().positive(),
1230
- confirm: confirmFlag().describe(
1231
- "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."
1232
- )
1280
+ var { schema: deletePartySchema, handler: deleteParty } = defineDelete({
1281
+ toolName: "delete_party",
1282
+ pathPrefix: "/parties",
1283
+ 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."
1233
1284
  });
1234
- async function deleteParty(input) {
1235
- if (input.confirm !== true) {
1236
- throw new Error("delete_party requires confirm: true");
1237
- }
1238
- return idempotent(
1239
- () => capsuleDelete(`/parties/${input.id}`),
1240
- () => ({ deleted: true, alreadyDeleted: false, id: input.id }),
1241
- () => ({ deleted: true, alreadyDeleted: true, id: input.id })
1242
- );
1243
- }
1244
- var addPartyEmailAddressSchema = z3.object({
1245
- partyId: z3.number().int().positive(),
1246
- address: z3.string().email(),
1247
- type: z3.string().optional().describe("Free-form label, e.g. 'Work', 'Home'.")
1285
+ var addPartyEmailAddressSchema = z6.object({
1286
+ partyId: positiveId,
1287
+ address: z6.string().email(),
1288
+ type: z6.string().optional().describe("Free-form label, e.g. 'Work', 'Home'.")
1248
1289
  });
1249
1290
  async function addPartyEmailAddress(input) {
1250
1291
  const { partyId, address, type } = input;
@@ -1254,9 +1295,9 @@ async function addPartyEmailAddress(input) {
1254
1295
  party: { emailAddresses: [item] }
1255
1296
  });
1256
1297
  }
1257
- var removePartyEmailAddressByIdSchema = z3.object({
1258
- partyId: z3.number().int().positive(),
1259
- emailAddressId: z3.number().int().positive().describe(
1298
+ var removePartyEmailAddressByIdSchema = z6.object({
1299
+ partyId: positiveId,
1300
+ emailAddressId: positiveId.describe(
1260
1301
  "Capsule's id for the email-address row. Read it from get_party (each entry in emailAddresses carries an id)."
1261
1302
  )
1262
1303
  });
@@ -1276,10 +1317,10 @@ async function removePartyEmailAddressById(input) {
1276
1317
  () => ({ removed: true, alreadyRemoved: true, partyId, emailAddressId })
1277
1318
  );
1278
1319
  }
1279
- var addPartyPhoneNumberSchema = z3.object({
1280
- partyId: z3.number().int().positive(),
1281
- number: z3.string().min(1),
1282
- type: z3.string().optional().describe("Free-form label, e.g. 'Work', 'Mobile'.")
1320
+ var addPartyPhoneNumberSchema = z6.object({
1321
+ partyId: positiveId,
1322
+ number: z6.string().min(1),
1323
+ type: z6.string().optional().describe("Free-form label, e.g. 'Work', 'Mobile'.")
1283
1324
  });
1284
1325
  async function addPartyPhoneNumber(input) {
1285
1326
  const { partyId, number, type } = input;
@@ -1289,9 +1330,9 @@ async function addPartyPhoneNumber(input) {
1289
1330
  party: { phoneNumbers: [item] }
1290
1331
  });
1291
1332
  }
1292
- var removePartyPhoneNumberByIdSchema = z3.object({
1293
- partyId: z3.number().int().positive(),
1294
- phoneNumberId: z3.number().int().positive().describe(
1333
+ var removePartyPhoneNumberByIdSchema = z6.object({
1334
+ partyId: positiveId,
1335
+ phoneNumberId: positiveId.describe(
1295
1336
  "Capsule's id for the phone-number row. Read it from get_party (each entry in phoneNumbers carries an id)."
1296
1337
  )
1297
1338
  });
@@ -1311,14 +1352,14 @@ async function removePartyPhoneNumberById(input) {
1311
1352
  () => ({ removed: true, alreadyRemoved: true, partyId, phoneNumberId })
1312
1353
  );
1313
1354
  }
1314
- var addPartyAddressSchema = z3.object({
1315
- partyId: z3.number().int().positive(),
1316
- street: z3.string().optional(),
1317
- city: z3.string().optional(),
1318
- state: z3.string().optional(),
1319
- country: z3.string().optional().describe(CountryDescription),
1320
- zip: z3.string().optional(),
1321
- type: z3.string().optional().describe("Free-form label, e.g. 'Office', 'Home'.")
1355
+ var addPartyAddressSchema = z6.object({
1356
+ partyId: positiveId,
1357
+ street: z6.string().optional(),
1358
+ city: z6.string().optional(),
1359
+ state: z6.string().optional(),
1360
+ country: z6.string().optional().describe(CountryDescription),
1361
+ zip: z6.string().optional(),
1362
+ type: z6.string().optional().describe("Free-form label, e.g. 'Office', 'Home'.")
1322
1363
  });
1323
1364
  async function addPartyAddress(input) {
1324
1365
  const { partyId, ...rest } = input;
@@ -1330,9 +1371,9 @@ async function addPartyAddress(input) {
1330
1371
  party: { addresses: [item] }
1331
1372
  });
1332
1373
  }
1333
- var removePartyAddressByIdSchema = z3.object({
1334
- partyId: z3.number().int().positive(),
1335
- addressId: z3.number().int().positive().describe(
1374
+ var removePartyAddressByIdSchema = z6.object({
1375
+ partyId: positiveId,
1376
+ addressId: positiveId.describe(
1336
1377
  "Capsule's id for the address row. Read it from get_party (each entry in addresses carries an id)."
1337
1378
  )
1338
1379
  });
@@ -1352,9 +1393,9 @@ async function removePartyAddressById(input) {
1352
1393
  () => ({ removed: true, alreadyRemoved: true, partyId, addressId })
1353
1394
  );
1354
1395
  }
1355
- var addPartyWebsiteSchema = z3.object({
1356
- partyId: z3.number().int().positive(),
1357
- address: z3.string().min(1).describe(
1396
+ var addPartyWebsiteSchema = z6.object({
1397
+ partyId: positiveId,
1398
+ address: z6.string().min(1).describe(
1358
1399
  "The website address. A URL when service='URL', or a handle (e.g. '@acmeco') for social services."
1359
1400
  ),
1360
1401
  service: WebsiteServiceEnum.optional().describe("Defaults to 'URL' if omitted.")
@@ -1367,9 +1408,9 @@ async function addPartyWebsite(input) {
1367
1408
  party: { websites: [item] }
1368
1409
  });
1369
1410
  }
1370
- var removePartyWebsiteByIdSchema = z3.object({
1371
- partyId: z3.number().int().positive(),
1372
- websiteId: z3.number().int().positive().describe(
1411
+ var removePartyWebsiteByIdSchema = z6.object({
1412
+ partyId: positiveId,
1413
+ websiteId: positiveId.describe(
1373
1414
  "Capsule's id for the website row. Read it from get_party (each entry in websites carries an id)."
1374
1415
  )
1375
1416
  });
@@ -1391,20 +1432,32 @@ async function removePartyWebsiteById(input) {
1391
1432
  }
1392
1433
 
1393
1434
  // src/tools/opportunities.ts
1394
- import { z as z4 } from "zod";
1395
- var OpportunityValueSchema = z4.object({
1396
- amount: z4.number().nonnegative(),
1397
- currency: z4.string({
1435
+ import { z as z7 } from "zod";
1436
+
1437
+ // src/tools/preserve-refs.ts
1438
+ async function readEntityRefs(path, responseKey) {
1439
+ const { data } = await capsuleGet(path);
1440
+ const entity = data[responseKey];
1441
+ return {
1442
+ teamId: entity?.team?.id ?? void 0,
1443
+ stageId: entity?.stage?.id ?? void 0
1444
+ };
1445
+ }
1446
+
1447
+ // src/tools/opportunities.ts
1448
+ var OpportunityValueSchema = z7.object({
1449
+ amount: z7.number().nonnegative(),
1450
+ currency: z7.string({
1398
1451
  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
1399
1452
  }).length(3).describe(
1400
1453
  "ISO 4217 currency code (3 letters), e.g. 'GBP', 'USD', 'EUR'. Required when amount is set."
1401
1454
  )
1402
1455
  });
1403
- var searchOpportunitiesSchema = z4.object({
1404
- q: z4.string().optional().describe("Free-text search query"),
1405
- embed: z4.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
1406
- page: z4.number().int().positive().optional().default(1),
1407
- perPage: z4.number().int().min(1).max(100).optional().default(25)
1456
+ var searchOpportunitiesSchema = z7.object({
1457
+ q: z7.string().optional().describe("Free-text search query"),
1458
+ embed: z7.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
1459
+ page: z7.number().int().positive().optional().default(1),
1460
+ perPage: z7.number().int().min(1).max(100).optional().default(25)
1408
1461
  });
1409
1462
  async function searchOpportunities(input) {
1410
1463
  const path = input.q ? "/opportunities/search" : "/opportunities";
@@ -1416,9 +1469,9 @@ async function searchOpportunities(input) {
1416
1469
  });
1417
1470
  return { ...data, nextPage };
1418
1471
  }
1419
- var getOpportunitySchema = z4.object({
1420
- id: z4.number().int().positive(),
1421
- embed: z4.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
1472
+ var getOpportunitySchema = z7.object({
1473
+ id: positiveId,
1474
+ embed: z7.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
1422
1475
  });
1423
1476
  async function getOpportunity(input) {
1424
1477
  const { data } = await capsuleGet(`/opportunities/${input.id}`, {
@@ -1426,11 +1479,11 @@ async function getOpportunity(input) {
1426
1479
  });
1427
1480
  return data;
1428
1481
  }
1429
- var getOpportunitiesSchema = z4.object({
1430
- ids: z4.array(z4.number().int().positive()).min(1).max(50).describe(
1482
+ var getOpportunitiesSchema = z7.object({
1483
+ ids: z7.array(positiveId).min(1).max(50).describe(
1431
1484
  "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."
1432
1485
  ),
1433
- embed: z4.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
1486
+ embed: z7.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
1434
1487
  });
1435
1488
  async function getOpportunities(input) {
1436
1489
  const { ids, embed } = input;
@@ -1451,20 +1504,20 @@ async function getOpportunities(input) {
1451
1504
  );
1452
1505
  return { opportunities: responses.flatMap((r) => r.data.opportunities) };
1453
1506
  }
1454
- var createOpportunitySchema = z4.object({
1455
- name: z4.string().min(1),
1456
- partyId: z4.number().int().positive().describe("ID of the party this opportunity belongs to"),
1457
- milestoneId: z4.number().int().positive().describe(
1458
- "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."
1507
+ var createOpportunitySchema = z7.object({
1508
+ name: z7.string().min(1),
1509
+ partyId: positiveId.describe("ID of the party this opportunity belongs to"),
1510
+ milestoneId: positiveId.describe(
1511
+ "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."
1459
1512
  ),
1460
- description: z4.string().optional(),
1513
+ description: z7.string().optional(),
1461
1514
  value: OpportunityValueSchema.optional(),
1462
- expectedCloseOn: z4.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("YYYY-MM-DD"),
1463
- probability: z4.number().int().min(0).max(100).optional(),
1464
- ownerId: z4.number().int().positive().optional().describe(
1465
- "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."
1515
+ expectedCloseOn: z7.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("YYYY-MM-DD"),
1516
+ probability: z7.number().int().min(0).max(100).optional(),
1517
+ ownerId: positiveId.optional().describe(
1518
+ "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."
1466
1519
  ),
1467
- teamId: z4.number().int().positive().optional().describe(
1520
+ teamId: positiveId.optional().describe(
1468
1521
  "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)."
1469
1522
  )
1470
1523
  });
@@ -1475,32 +1528,32 @@ async function createOpportunity(input) {
1475
1528
  party: { id: partyId },
1476
1529
  milestone: { id: milestoneId }
1477
1530
  };
1478
- if (ownerId) body["owner"] = { id: ownerId };
1479
- if (teamId) body["team"] = { id: teamId };
1531
+ setRef(body, "owner", ownerId);
1532
+ setRef(body, "team", teamId);
1480
1533
  return capsulePost("/opportunities", { opportunity: body });
1481
1534
  }
1482
- var updateOpportunitySchema = z4.object({
1483
- id: z4.number().int().positive(),
1484
- name: z4.string().min(1).optional(),
1485
- milestoneId: z4.number().int().positive().optional().describe(
1486
- "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."
1535
+ var updateOpportunitySchema = z7.object({
1536
+ id: positiveId,
1537
+ name: z7.string().min(1).optional(),
1538
+ milestoneId: positiveId.optional().describe(
1539
+ "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."
1487
1540
  ),
1488
- description: z4.string().optional(),
1541
+ description: z7.string().optional(),
1489
1542
  value: OpportunityValueSchema.optional(),
1490
- expectedCloseOn: z4.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional(),
1491
- probability: z4.number().int().min(0).max(100).optional().describe(
1543
+ expectedCloseOn: z7.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional(),
1544
+ probability: z7.number().int().min(0).max(100).optional().describe(
1492
1545
  "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)."
1493
1546
  ),
1494
- lostReasonId: z4.number().int().positive().optional().describe(
1547
+ lostReasonId: positiveId.optional().describe(
1495
1548
  "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."
1496
1549
  ),
1497
- ownerId: z4.number().int().positive().optional().describe(
1550
+ ownerId: positiveId.optional().describe(
1498
1551
  "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."
1499
1552
  ),
1500
- teamId: z4.number().int().positive().nullable().optional().describe(
1553
+ teamId: positiveId.nullable().optional().describe(
1501
1554
  "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."
1502
1555
  ),
1503
- fields: z4.array(CustomFieldWriteSchema).optional().describe(fieldsArrayDescriptor("get_opportunity"))
1556
+ fields: z7.array(CustomFieldWriteSchema).optional().describe(fieldsArrayDescriptor("get_opportunity"))
1504
1557
  });
1505
1558
  async function updateOpportunity(input) {
1506
1559
  const { id, milestoneId, ownerId, teamId, lostReasonId, fields, ...rest } = input;
@@ -1508,59 +1561,39 @@ async function updateOpportunity(input) {
1508
1561
  for (const [k, v] of Object.entries(rest)) {
1509
1562
  if (v !== void 0) body[k] = v;
1510
1563
  }
1511
- if (milestoneId) body["milestone"] = { id: milestoneId };
1564
+ setRef(body, "milestone", milestoneId);
1512
1565
  let resolvedTeamId = teamId;
1513
1566
  if (ownerId !== void 0 && teamId === void 0) {
1514
- const { data } = await capsuleGet(`/opportunities/${id}`);
1515
- resolvedTeamId = data.opportunity?.team?.id ?? void 0;
1567
+ ({ teamId: resolvedTeamId } = await readEntityRefs(`/opportunities/${id}`, "opportunity"));
1516
1568
  }
1517
- if (ownerId) body["owner"] = { id: ownerId };
1518
- if (resolvedTeamId === null) body["team"] = null;
1519
- else if (resolvedTeamId !== void 0) body["team"] = { id: resolvedTeamId };
1520
- if (lostReasonId) body["lostReason"] = { id: lostReasonId };
1569
+ setRef(body, "owner", ownerId);
1570
+ setNullableRef(body, "team", resolvedTeamId);
1571
+ setRef(body, "lostReason", lostReasonId);
1521
1572
  const mappedFields = mapFieldsForBody(fields);
1522
1573
  if (mappedFields !== void 0) body["fields"] = mappedFields;
1523
1574
  return capsulePut(`/opportunities/${id}`, {
1524
1575
  opportunity: body
1525
1576
  });
1526
1577
  }
1527
- var batchUpdateOpportunitySchema = z4.object({
1528
- items: z4.array(updateOpportunitySchema).min(1).max(50).describe(
1529
- "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."
1530
- )
1578
+ var { schema: batchUpdateOpportunitySchema, handler: batchUpdateOpportunity } = defineBatch({
1579
+ toolName: "batch_update_opportunity",
1580
+ itemSchema: updateOpportunitySchema,
1581
+ 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.",
1582
+ itemHandler: updateOpportunity
1531
1583
  });
1532
- async function batchUpdateOpportunity(input, opts = {}) {
1533
- return batchExecute(
1534
- "batch_update_opportunity",
1535
- input.items,
1536
- (item) => updateOpportunity(item),
1537
- opts
1538
- );
1539
- }
1540
- var deleteOpportunitySchema = z4.object({
1541
- id: z4.number().int().positive(),
1542
- confirm: confirmFlag().describe(
1543
- "Must be set to true. Permanently deletes the opportunity. Irreversible."
1544
- )
1584
+ var { schema: deleteOpportunitySchema, handler: deleteOpportunity } = defineDelete({
1585
+ toolName: "delete_opportunity",
1586
+ pathPrefix: "/opportunities",
1587
+ confirmHint: "Must be set to true. Permanently deletes the opportunity. Irreversible."
1545
1588
  });
1546
- async function deleteOpportunity(input) {
1547
- if (input.confirm !== true) {
1548
- throw new Error("delete_opportunity requires confirm: true");
1549
- }
1550
- return idempotent(
1551
- () => capsuleDelete(`/opportunities/${input.id}`),
1552
- () => ({ deleted: true, alreadyDeleted: false, id: input.id }),
1553
- () => ({ deleted: true, alreadyDeleted: true, id: input.id })
1554
- );
1555
- }
1556
1589
 
1557
1590
  // src/tools/projects.ts
1558
- import { z as z5 } from "zod";
1559
- var listProjectsSchema = z5.object({
1560
- status: z5.enum(["OPEN", "CLOSED"]).optional(),
1561
- embed: z5.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
1562
- page: z5.number().int().positive().optional().default(1),
1563
- perPage: z5.number().int().min(1).max(100).optional().default(25)
1591
+ import { z as z8 } from "zod";
1592
+ var listProjectsSchema = z8.object({
1593
+ status: z8.enum(["OPEN", "CLOSED"]).optional(),
1594
+ embed: z8.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
1595
+ page: z8.number().int().positive().optional().default(1),
1596
+ perPage: z8.number().int().min(1).max(100).optional().default(25)
1564
1597
  });
1565
1598
  async function listProjects(input) {
1566
1599
  const { data, nextPage } = await capsuleGet("/kases", {
@@ -1571,9 +1604,9 @@ async function listProjects(input) {
1571
1604
  });
1572
1605
  return { ...data, nextPage };
1573
1606
  }
1574
- var getProjectSchema = z5.object({
1575
- id: z5.number().int().positive(),
1576
- embed: z5.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
1607
+ var getProjectSchema = z8.object({
1608
+ id: positiveId,
1609
+ embed: z8.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
1577
1610
  });
1578
1611
  async function getProject(input) {
1579
1612
  const { data } = await capsuleGet(`/kases/${input.id}`, {
@@ -1581,11 +1614,11 @@ async function getProject(input) {
1581
1614
  });
1582
1615
  return data;
1583
1616
  }
1584
- var getProjectsSchema = z5.object({
1585
- ids: z5.array(z5.number().int().positive()).min(1).max(50).describe(
1617
+ var getProjectsSchema = z8.object({
1618
+ ids: z8.array(positiveId).min(1).max(50).describe(
1586
1619
  "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."
1587
1620
  ),
1588
- embed: z5.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
1621
+ embed: z8.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
1589
1622
  });
1590
1623
  async function getProjects(input) {
1591
1624
  const { ids, embed } = input;
@@ -1603,21 +1636,21 @@ async function getProjects(input) {
1603
1636
  );
1604
1637
  return { kases: responses.flatMap((r) => r.data.kases) };
1605
1638
  }
1606
- var createProjectSchema = z5.object({
1607
- name: z5.string().min(1),
1608
- partyId: z5.number().int().positive().describe("ID of the party linked to this project"),
1609
- description: z5.string().optional(),
1610
- status: z5.enum(["OPEN", "CLOSED"]).optional().describe("Defaults to OPEN when omitted."),
1611
- ownerId: z5.number().int().positive().optional().describe(
1639
+ var createProjectSchema = z8.object({
1640
+ name: z8.string().min(1),
1641
+ partyId: positiveId.describe("ID of the party linked to this project"),
1642
+ description: z8.string().optional(),
1643
+ status: z8.enum(["OPEN", "CLOSED"]).optional().describe("Defaults to OPEN when omitted."),
1644
+ ownerId: positiveId.optional().describe(
1612
1645
  "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."
1613
1646
  ),
1614
- teamId: z5.number().int().positive().optional().describe(
1647
+ teamId: positiveId.optional().describe(
1615
1648
  "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."
1616
1649
  ),
1617
- stageId: z5.number().int().positive().optional().describe(
1650
+ stageId: positiveId.optional().describe(
1618
1651
  "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."
1619
1652
  ),
1620
- expectedCloseOn: z5.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("YYYY-MM-DD")
1653
+ expectedCloseOn: z8.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("YYYY-MM-DD")
1621
1654
  });
1622
1655
  async function createProject(input) {
1623
1656
  const { partyId, ownerId, teamId, status, stageId, ...rest } = input;
@@ -1626,27 +1659,27 @@ async function createProject(input) {
1626
1659
  status: status ?? "OPEN",
1627
1660
  party: { id: partyId }
1628
1661
  };
1629
- if (ownerId) body["owner"] = { id: ownerId };
1630
- if (teamId) body["team"] = { id: teamId };
1662
+ setRef(body, "owner", ownerId);
1663
+ setRef(body, "team", teamId);
1631
1664
  if (stageId) body["stage"] = stageId;
1632
1665
  return capsulePost("/kases", { kase: body });
1633
1666
  }
1634
- var updateProjectSchema = z5.object({
1635
- id: z5.number().int().positive(),
1636
- name: z5.string().min(1).optional(),
1637
- description: z5.string().optional(),
1638
- status: z5.enum(["OPEN", "CLOSED"]).optional(),
1639
- ownerId: z5.number().int().positive().nullable().optional().describe(
1667
+ var updateProjectSchema = z8.object({
1668
+ id: positiveId,
1669
+ name: z8.string().min(1).optional(),
1670
+ description: z8.string().optional(),
1671
+ status: z8.enum(["OPEN", "CLOSED"]).optional(),
1672
+ ownerId: positiveId.nullable().optional().describe(
1640
1673
  "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)."
1641
1674
  ),
1642
- teamId: z5.number().int().positive().nullable().optional().describe(
1675
+ teamId: positiveId.nullable().optional().describe(
1643
1676
  "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'."
1644
1677
  ),
1645
- stageId: z5.number().int().positive().optional().describe(
1678
+ stageId: positiveId.optional().describe(
1646
1679
  "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."
1647
1680
  ),
1648
- expectedCloseOn: z5.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("YYYY-MM-DD"),
1649
- fields: z5.array(CustomFieldWriteSchema).optional().describe(
1681
+ expectedCloseOn: z8.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("YYYY-MM-DD"),
1682
+ fields: z8.array(CustomFieldWriteSchema).optional().describe(
1650
1683
  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."
1651
1684
  )
1652
1685
  });
@@ -1659,55 +1692,38 @@ async function updateProject(input) {
1659
1692
  let resolvedTeamId = teamId;
1660
1693
  let resolvedStageId = stageId;
1661
1694
  if (ownerId !== void 0 && (teamId === void 0 || stageId === void 0)) {
1662
- const { data } = await capsuleGet(`/kases/${id}`);
1663
- if (teamId === void 0) {
1664
- resolvedTeamId = data.kase?.team?.id ?? void 0;
1665
- }
1666
- if (stageId === void 0) {
1667
- resolvedStageId = data.kase?.stage?.id ?? void 0;
1668
- }
1695
+ const current = await readEntityRefs(`/kases/${id}`, "kase");
1696
+ if (teamId === void 0) resolvedTeamId = current.teamId;
1697
+ if (stageId === void 0) resolvedStageId = current.stageId;
1669
1698
  }
1670
- if (ownerId === null) body["owner"] = null;
1671
- else if (ownerId !== void 0) body["owner"] = { id: ownerId };
1672
- if (resolvedTeamId === null) body["team"] = null;
1673
- else if (resolvedTeamId !== void 0) body["team"] = { id: resolvedTeamId };
1699
+ setNullableRef(body, "owner", ownerId);
1700
+ setNullableRef(body, "team", resolvedTeamId);
1674
1701
  if (resolvedStageId) body["stage"] = resolvedStageId;
1675
1702
  const mappedFields = mapFieldsForBody(fields);
1676
1703
  if (mappedFields !== void 0) body["fields"] = mappedFields;
1677
1704
  return capsulePut(`/kases/${id}`, { kase: body });
1678
1705
  }
1679
- var deleteProjectSchema = z5.object({
1680
- id: z5.number().int().positive(),
1681
- confirm: confirmFlag().describe(
1682
- "Must be set to true. Permanently deletes the project (case). Consider update_project status='CLOSED' instead. Irreversible."
1683
- )
1706
+ var { schema: deleteProjectSchema, handler: deleteProject } = defineDelete({
1707
+ toolName: "delete_project",
1708
+ pathPrefix: "/kases",
1709
+ confirmHint: "Must be set to true. Permanently deletes the project (case). Consider update_project status='CLOSED' instead. Irreversible."
1684
1710
  });
1685
- async function deleteProject(input) {
1686
- if (input.confirm !== true) {
1687
- throw new Error("delete_project requires confirm: true");
1688
- }
1689
- return idempotent(
1690
- () => capsuleDelete(`/kases/${input.id}`),
1691
- () => ({ deleted: true, alreadyDeleted: false, id: input.id }),
1692
- () => ({ deleted: true, alreadyDeleted: true, id: input.id })
1693
- );
1694
- }
1695
1711
 
1696
1712
  // src/tools/tasks.ts
1697
- import { z as z6 } from "zod";
1698
- var listTasksSchema = z6.object({
1713
+ import { z as z9 } from "zod";
1714
+ var listTasksSchema = z9.object({
1699
1715
  // Note: Capsule has a third internal status `PENDING` (a task that's
1700
1716
  // part of an active track but not yet "open"), but it can only be
1701
1717
  // reached via track machinery — it is NOT directly settable by
1702
1718
  // /tasks PUT, and a list filter for it returns the same as OPEN
1703
1719
  // anyway. We expose only the two values that are actually filterable
1704
1720
  // by the v2 API.
1705
- status: z6.enum(["OPEN", "COMPLETED"]).optional().describe(
1721
+ status: z9.enum(["OPEN", "COMPLETED"]).optional().describe(
1706
1722
  "Defaults to OPEN when omitted. Pass COMPLETED to filter to completed tasks, or 'OPEN' explicitly."
1707
1723
  ),
1708
- ownerId: z6.number().int().positive().optional().describe("Filter to tasks owned by this user ID"),
1709
- page: z6.number().int().positive().optional().default(1),
1710
- perPage: z6.number().int().min(1).max(100).optional().default(25)
1724
+ ownerId: positiveId.optional().describe("Filter to tasks owned by this user ID"),
1725
+ page: z9.number().int().positive().optional().default(1),
1726
+ perPage: z9.number().int().min(1).max(100).optional().default(25)
1711
1727
  });
1712
1728
  async function listTasks(input) {
1713
1729
  const { data, nextPage } = await capsuleGet("/tasks", {
@@ -1721,15 +1737,15 @@ async function listTasks(input) {
1721
1737
  });
1722
1738
  return { ...data, nextPage };
1723
1739
  }
1724
- var getTaskSchema = z6.object({
1725
- id: z6.number().int().positive().describe("Task ID")
1740
+ var getTaskSchema = z9.object({
1741
+ id: positiveId.describe("Task ID")
1726
1742
  });
1727
1743
  async function getTask(input) {
1728
1744
  const { data } = await capsuleGet(`/tasks/${input.id}`);
1729
1745
  return data;
1730
1746
  }
1731
- var getTasksSchema = z6.object({
1732
- ids: z6.array(z6.number().int().positive()).min(1).max(50).describe(
1747
+ var getTasksSchema = z9.object({
1748
+ ids: z9.array(positiveId).min(1).max(50).describe(
1733
1749
  "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."
1734
1750
  )
1735
1751
  });
@@ -1745,17 +1761,17 @@ async function getTasks(input) {
1745
1761
  );
1746
1762
  return { tasks: responses.flatMap((r) => r.data.tasks) };
1747
1763
  }
1748
- var createTaskSchema = z6.object({
1749
- description: z6.string().min(1),
1750
- dueOn: z6.string().regex(/^\d{4}-\d{2}-\d{2}$/).describe("YYYY-MM-DD"),
1751
- dueTime: z6.string().regex(/^\d{2}:\d{2}$/).optional().describe("HH:MM in user's timezone"),
1752
- detail: z6.string().optional(),
1753
- ownerId: z6.number().int().positive().optional().describe(
1764
+ var createTaskSchema = z9.object({
1765
+ description: z9.string().min(1),
1766
+ dueOn: z9.string().regex(/^\d{4}-\d{2}-\d{2}$/).describe("YYYY-MM-DD"),
1767
+ dueTime: z9.string().regex(/^\d{2}:\d{2}$/).optional().describe("HH:MM in user's timezone"),
1768
+ detail: z9.string().optional(),
1769
+ ownerId: positiveId.optional().describe(
1754
1770
  "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."
1755
1771
  ),
1756
- partyId: z6.number().int().positive().optional().describe("Link task to a party (mutually exclusive with opportunityId/projectId)"),
1757
- opportunityId: z6.number().int().positive().optional().describe("Link task to an opportunity (mutually exclusive with partyId/projectId)"),
1758
- projectId: z6.number().int().positive().optional().describe("Link task to a project (mutually exclusive with partyId/opportunityId)")
1772
+ partyId: positiveId.optional().describe("Link task to a party (mutually exclusive with opportunityId/projectId)"),
1773
+ opportunityId: positiveId.optional().describe("Link task to an opportunity (mutually exclusive with partyId/projectId)"),
1774
+ projectId: positiveId.optional().describe("Link task to a project (mutually exclusive with partyId/opportunityId)")
1759
1775
  });
1760
1776
  async function createTask(input) {
1761
1777
  const linked = [input.partyId, input.opportunityId, input.projectId].filter(Boolean);
@@ -1764,25 +1780,25 @@ async function createTask(input) {
1764
1780
  }
1765
1781
  const { ownerId, partyId, opportunityId, projectId, ...rest } = input;
1766
1782
  const body = { ...rest };
1767
- if (ownerId) body["owner"] = { id: ownerId };
1768
- if (partyId) body["party"] = { id: partyId };
1769
- if (opportunityId) body["opportunity"] = { id: opportunityId };
1770
- if (projectId) body["kase"] = { id: projectId };
1783
+ setRef(body, "owner", ownerId);
1784
+ setRef(body, "party", partyId);
1785
+ setRef(body, "opportunity", opportunityId);
1786
+ setRef(body, "kase", projectId);
1771
1787
  return capsulePost("/tasks", { task: body });
1772
1788
  }
1773
- var updateTaskSchema = z6.object({
1774
- id: z6.number().int().positive(),
1775
- description: z6.string().min(1).optional(),
1776
- dueOn: z6.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("YYYY-MM-DD"),
1777
- dueTime: z6.string().regex(/^\d{2}:\d{2}$/).optional().describe("HH:MM in user's timezone"),
1778
- detail: z6.string().optional(),
1789
+ var updateTaskSchema = z9.object({
1790
+ id: positiveId,
1791
+ description: z9.string().min(1).optional(),
1792
+ dueOn: z9.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("YYYY-MM-DD"),
1793
+ dueTime: z9.string().regex(/^\d{2}:\d{2}$/).optional().describe("HH:MM in user's timezone"),
1794
+ detail: z9.string().optional(),
1779
1795
  // Capsule rejects direct sets of `PENDING` (which is a track-machinery
1780
1796
  // internal state) with 422 "cannot set task status to PENDING".
1781
1797
  // Only OPEN and COMPLETED are settable here.
1782
- status: z6.enum(["OPEN", "COMPLETED"]).optional().describe(
1798
+ status: z9.enum(["OPEN", "COMPLETED"]).optional().describe(
1783
1799
  "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)."
1784
1800
  ),
1785
- ownerId: z6.number().int().positive().optional().describe(
1801
+ ownerId: positiveId.optional().describe(
1786
1802
  "Reassign owner to user ID. Once set, this connector cannot clear an owner back to null \u2014 use Capsule's web UI for that."
1787
1803
  )
1788
1804
  });
@@ -1792,51 +1808,40 @@ async function updateTask(input) {
1792
1808
  for (const [k, v] of Object.entries(rest)) {
1793
1809
  if (v !== void 0) body[k] = v;
1794
1810
  }
1795
- if (ownerId) body["owner"] = { id: ownerId };
1811
+ setRef(body, "owner", ownerId);
1796
1812
  return capsulePut(`/tasks/${id}`, { task: body });
1797
1813
  }
1798
- var completeTaskSchema = z6.object({
1799
- id: z6.number().int().positive()
1814
+ var completeTaskSchema = z9.object({
1815
+ id: positiveId
1800
1816
  });
1801
1817
  async function completeTask(input) {
1802
1818
  return capsulePut(`/tasks/${input.id}`, {
1803
1819
  task: { status: "COMPLETED" }
1804
1820
  });
1805
1821
  }
1806
- var batchCompleteTaskSchema = z6.object({
1807
- ids: z6.array(z6.number().int().positive()).min(1).max(50).describe(
1822
+ var batchCompleteTaskSchema = z9.object({
1823
+ ids: z9.array(positiveId).min(1).max(50).describe(
1808
1824
  "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."
1809
1825
  )
1810
1826
  });
1811
1827
  async function batchCompleteTask(input, opts = {}) {
1812
1828
  return batchExecute("batch_complete_task", input.ids, (id) => completeTask({ id }), opts);
1813
1829
  }
1814
- var deleteTaskSchema = z6.object({
1815
- id: z6.number().int().positive(),
1816
- confirm: confirmFlag().describe(
1817
- "Must be set to true. Permanently deletes the task. To mark done without losing history use complete_task. Irreversible."
1818
- )
1830
+ var { schema: deleteTaskSchema, handler: deleteTask } = defineDelete({
1831
+ toolName: "delete_task",
1832
+ pathPrefix: "/tasks",
1833
+ confirmHint: "Must be set to true. Permanently deletes the task. To mark done without losing history use complete_task. Irreversible."
1819
1834
  });
1820
- async function deleteTask(input) {
1821
- if (input.confirm !== true) {
1822
- throw new Error("delete_task requires confirm: true");
1823
- }
1824
- return idempotent(
1825
- () => capsuleDelete(`/tasks/${input.id}`),
1826
- () => ({ deleted: true, alreadyDeleted: false, id: input.id }),
1827
- () => ({ deleted: true, alreadyDeleted: true, id: input.id })
1828
- );
1829
- }
1830
1835
 
1831
1836
  // src/tools/entries.ts
1832
- import { z as z7 } from "zod";
1837
+ import { z as z10 } from "zod";
1833
1838
  var listEntriesPagination = {
1834
- page: z7.number().int().positive().optional().default(1),
1835
- perPage: z7.number().int().min(1).max(100).optional().default(25),
1836
- embed: z7.string().optional().describe(EMBED_ATTACHMENTS_PARTICIPANTS_DESCRIPTION)
1839
+ page: z10.number().int().positive().optional().default(1),
1840
+ perPage: z10.number().int().min(1).max(100).optional().default(25),
1841
+ embed: z10.string().optional().describe(EMBED_ATTACHMENTS_PARTICIPANTS_DESCRIPTION)
1837
1842
  };
1838
- var listPartyEntriesSchema = z7.object({
1839
- partyId: z7.number().int().positive(),
1843
+ var listPartyEntriesSchema = z10.object({
1844
+ partyId: positiveId,
1840
1845
  ...listEntriesPagination
1841
1846
  });
1842
1847
  async function listPartyEntries(input) {
@@ -1846,8 +1851,8 @@ async function listPartyEntries(input) {
1846
1851
  );
1847
1852
  return { ...data, nextPage };
1848
1853
  }
1849
- var listOpportunityEntriesSchema = z7.object({
1850
- opportunityId: z7.number().int().positive(),
1854
+ var listOpportunityEntriesSchema = z10.object({
1855
+ opportunityId: positiveId,
1851
1856
  ...listEntriesPagination
1852
1857
  });
1853
1858
  async function listOpportunityEntries(input) {
@@ -1857,8 +1862,8 @@ async function listOpportunityEntries(input) {
1857
1862
  );
1858
1863
  return { ...data, nextPage };
1859
1864
  }
1860
- var listProjectEntriesSchema = z7.object({
1861
- projectId: z7.number().int().positive(),
1865
+ var listProjectEntriesSchema = z10.object({
1866
+ projectId: positiveId,
1862
1867
  ...listEntriesPagination
1863
1868
  });
1864
1869
  async function listProjectEntries(input) {
@@ -1868,9 +1873,9 @@ async function listProjectEntries(input) {
1868
1873
  );
1869
1874
  return { ...data, nextPage };
1870
1875
  }
1871
- var getEntrySchema = z7.object({
1872
- id: z7.number().int().positive(),
1873
- embed: z7.string().optional().describe(EMBED_ATTACHMENTS_PARTICIPANTS_DESCRIPTION)
1876
+ var getEntrySchema = z10.object({
1877
+ id: positiveId,
1878
+ embed: z10.string().optional().describe(EMBED_ATTACHMENTS_PARTICIPANTS_DESCRIPTION)
1874
1879
  });
1875
1880
  async function getEntry(input) {
1876
1881
  const { data } = await capsuleGet(`/entries/${input.id}`, {
@@ -1878,7 +1883,7 @@ async function getEntry(input) {
1878
1883
  });
1879
1884
  return data;
1880
1885
  }
1881
- var listEntriesSchema = z7.object({
1886
+ var listEntriesSchema = z10.object({
1882
1887
  ...listEntriesPagination
1883
1888
  });
1884
1889
  async function listEntries(input) {
@@ -1889,14 +1894,14 @@ async function listEntries(input) {
1889
1894
  });
1890
1895
  return { ...data, nextPage };
1891
1896
  }
1892
- var addNoteSchema = z7.object({
1893
- content: z7.string().min(1).describe(
1897
+ var addNoteSchema = z10.object({
1898
+ content: z10.string().min(1).describe(
1894
1899
  "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."
1895
1900
  ),
1896
- partyId: z7.number().int().positive().optional().describe("Link note to a party (mutually exclusive with opportunityId/projectId)"),
1897
- opportunityId: z7.number().int().positive().optional().describe("Link note to an opportunity (mutually exclusive with partyId/projectId)"),
1898
- projectId: z7.number().int().positive().optional().describe("Link note to a project (mutually exclusive with partyId/opportunityId)"),
1899
- entryAt: z7.string().regex(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:?\d{2})$/).optional().describe(
1901
+ partyId: positiveId.optional().describe("Link note to a party (mutually exclusive with opportunityId/projectId)"),
1902
+ opportunityId: positiveId.optional().describe("Link note to an opportunity (mutually exclusive with partyId/projectId)"),
1903
+ projectId: positiveId.optional().describe("Link note to a project (mutually exclusive with partyId/opportunityId)"),
1904
+ entryAt: z10.string().regex(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:?\d{2})$/).optional().describe(
1900
1905
  "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)."
1901
1906
  )
1902
1907
  });
@@ -1907,18 +1912,18 @@ async function addNote(input) {
1907
1912
  throw new Error("Provide exactly one of partyId, opportunityId, or projectId");
1908
1913
  }
1909
1914
  const body = { type: "note", content };
1910
- if (partyId) body["party"] = { id: partyId };
1911
- if (opportunityId) body["opportunity"] = { id: opportunityId };
1912
- if (projectId) body["kase"] = { id: projectId };
1915
+ setRef(body, "party", partyId);
1916
+ setRef(body, "opportunity", opportunityId);
1917
+ setRef(body, "kase", projectId);
1913
1918
  if (entryAt !== void 0) body["entryAt"] = entryAt;
1914
1919
  return capsulePost("/entries", { entry: body });
1915
1920
  }
1916
- var updateEntrySchema = z7.object({
1917
- id: z7.number().int().positive().describe("Entry ID to update"),
1918
- content: z7.string().min(1).optional().describe(
1921
+ var updateEntrySchema = z10.object({
1922
+ id: positiveId.describe("Entry ID to update"),
1923
+ content: z10.string().min(1).optional().describe(
1919
1924
  "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."
1920
1925
  ),
1921
- subject: z7.string().optional().describe(
1926
+ subject: z10.string().optional().describe(
1922
1927
  "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`."
1923
1928
  )
1924
1929
  });
@@ -1932,30 +1937,20 @@ async function updateEntry(input) {
1932
1937
  }
1933
1938
  return capsulePut(`/entries/${id}`, { entry: body });
1934
1939
  }
1935
- var deleteEntrySchema = z7.object({
1936
- id: z7.number().int().positive().describe("Entry (note/email/task-record) ID"),
1937
- confirm: confirmFlag().describe(
1938
- "Must be set to true. Permanently deletes the entry \u2014 use this to remove a note from a party/opportunity/project. Irreversible."
1939
- )
1940
+ var { schema: deleteEntrySchema, handler: deleteEntry } = defineDelete({
1941
+ toolName: "delete_entry",
1942
+ pathPrefix: "/entries",
1943
+ confirmHint: "Must be set to true. Permanently deletes the entry \u2014 use this to remove a note from a party/opportunity/project. Irreversible.",
1944
+ idDescription: "Entry (note/email/task-record) ID"
1940
1945
  });
1941
- async function deleteEntry(input) {
1942
- if (input.confirm !== true) {
1943
- throw new Error("delete_entry requires confirm: true");
1944
- }
1945
- return idempotent(
1946
- () => capsuleDelete(`/entries/${input.id}`),
1947
- () => ({ deleted: true, alreadyDeleted: false, id: input.id }),
1948
- () => ({ deleted: true, alreadyDeleted: true, id: input.id })
1949
- );
1950
- }
1951
1946
 
1952
1947
  // src/tools/pipelines.ts
1953
- import { z as z8 } from "zod";
1948
+ import { z as z11 } from "zod";
1954
1949
  var paginationFields = {
1955
- page: z8.number().int().positive().optional(),
1956
- perPage: z8.number().int().min(1).max(100).optional()
1950
+ page: z11.number().int().positive().optional(),
1951
+ perPage: z11.number().int().min(1).max(100).optional()
1957
1952
  };
1958
- var listPipelinesSchema = z8.object({ ...paginationFields });
1953
+ var listPipelinesSchema = z11.object({ ...paginationFields });
1959
1954
  async function listPipelines(input) {
1960
1955
  const { data, nextPage } = await capsuleGetCached("/pipelines", {
1961
1956
  page: input.page ?? 1,
@@ -1963,8 +1958,8 @@ async function listPipelines(input) {
1963
1958
  });
1964
1959
  return { ...data, nextPage };
1965
1960
  }
1966
- var listMilestonesSchema = z8.object({
1967
- pipelineId: z8.number().int().positive(),
1961
+ var listMilestonesSchema = z11.object({
1962
+ pipelineId: positiveId,
1968
1963
  ...paginationFields
1969
1964
  });
1970
1965
  async function listMilestones(input) {
@@ -1976,12 +1971,12 @@ async function listMilestones(input) {
1976
1971
  }
1977
1972
 
1978
1973
  // src/tools/boards.ts
1979
- import { z as z9 } from "zod";
1974
+ import { z as z12 } from "zod";
1980
1975
  var paginationFields2 = {
1981
- page: z9.number().int().positive().optional(),
1982
- perPage: z9.number().int().min(1).max(100).optional()
1976
+ page: z12.number().int().positive().optional(),
1977
+ perPage: z12.number().int().min(1).max(100).optional()
1983
1978
  };
1984
- var listBoardsSchema = z9.object({ ...paginationFields2 });
1979
+ var listBoardsSchema = z12.object({ ...paginationFields2 });
1985
1980
  async function listBoards(input) {
1986
1981
  const { data, nextPage } = await capsuleGetCached("/boards", {
1987
1982
  page: input.page ?? 1,
@@ -1989,8 +1984,8 @@ async function listBoards(input) {
1989
1984
  });
1990
1985
  return { ...data, nextPage };
1991
1986
  }
1992
- var listStagesSchema = z9.object({
1993
- boardId: z9.number().int().positive().optional().describe(
1987
+ var listStagesSchema = z12.object({
1988
+ boardId: positiveId.optional().describe(
1994
1989
  "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."
1995
1990
  ),
1996
1991
  ...paginationFields2
@@ -2005,7 +2000,7 @@ async function listStages(input) {
2005
2000
  }
2006
2001
 
2007
2002
  // src/tools/tags.ts
2008
- import { z as z10 } from "zod";
2003
+ import { z as z13 } from "zod";
2009
2004
  var TAG_LIST_PATH = {
2010
2005
  parties: "/parties/tags",
2011
2006
  opportunities: "/opportunities/tags",
@@ -2016,11 +2011,11 @@ var ENTITY_TO_WRAPPER = {
2016
2011
  opportunities: "opportunity",
2017
2012
  kases: "kase"
2018
2013
  };
2019
- var TagEntity = z10.enum(["parties", "opportunities", "kases"]).describe("Which entity type. Use 'kases' for projects (Capsule's legacy path name).");
2020
- var listTagsSchema = z10.object({
2021
- entity: z10.enum(["parties", "opportunities", "kases"]).describe("The resource type to list tags for"),
2022
- page: z10.number().int().positive().optional(),
2023
- perPage: z10.number().int().min(1).max(100).optional()
2014
+ var TagEntity = z13.enum(["parties", "opportunities", "kases"]).describe("Which entity type. Use 'kases' for projects (Capsule's legacy path name).");
2015
+ var listTagsSchema = z13.object({
2016
+ entity: z13.enum(["parties", "opportunities", "kases"]).describe("The resource type to list tags for"),
2017
+ page: z13.number().int().positive().optional(),
2018
+ perPage: z13.number().int().min(1).max(100).optional()
2024
2019
  });
2025
2020
  async function listTags(input) {
2026
2021
  const path = TAG_LIST_PATH[input.entity];
@@ -2030,10 +2025,10 @@ async function listTags(input) {
2030
2025
  });
2031
2026
  return { ...data, nextPage };
2032
2027
  }
2033
- var addTagSchema = z10.object({
2028
+ var addTagSchema = z13.object({
2034
2029
  entity: TagEntity,
2035
- entityId: z10.number().int().positive().describe("The party/opportunity/kase id."),
2036
- tagName: z10.string().min(1).describe(
2030
+ entityId: positiveId.describe("The party/opportunity/kase id."),
2031
+ tagName: z13.string().min(1).describe(
2037
2032
  "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."
2038
2033
  )
2039
2034
  });
@@ -2046,10 +2041,10 @@ async function addTag(input) {
2046
2041
  invalidateByPrefix(TAG_LIST_PATH[entity], "add_tag");
2047
2042
  return result;
2048
2043
  }
2049
- var removeTagByIdSchema = z10.object({
2044
+ var removeTagByIdSchema = z13.object({
2050
2045
  entity: TagEntity,
2051
- entityId: z10.number().int().positive().describe("The party/opportunity/kase id."),
2052
- tagId: z10.number().int().positive().describe(
2046
+ entityId: positiveId.describe("The party/opportunity/kase id."),
2047
+ tagId: positiveId.describe(
2053
2048
  "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."
2054
2049
  )
2055
2050
  });
@@ -2077,28 +2072,24 @@ async function removeTagById(input) {
2077
2072
  invalidateByPrefix(TAG_LIST_PATH[entity], "remove_tag_by_id");
2078
2073
  return result;
2079
2074
  }
2080
- var batchAddTagSchema = z10.object({
2081
- items: z10.array(addTagSchema).min(1).max(50).describe(
2082
- "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."
2083
- )
2075
+ var { schema: batchAddTagSchema, handler: batchAddTag } = defineBatch({
2076
+ toolName: "batch_add_tag",
2077
+ itemSchema: addTagSchema,
2078
+ 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.",
2079
+ itemHandler: addTag
2084
2080
  });
2085
- async function batchAddTag(input, opts = {}) {
2086
- return batchExecute("batch_add_tag", input.items, (item) => addTag(item), opts);
2087
- }
2088
- var batchRemoveTagByIdSchema = z10.object({
2089
- items: z10.array(removeTagByIdSchema).min(1).max(50).describe(
2090
- "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."
2091
- )
2081
+ var { schema: batchRemoveTagByIdSchema, handler: batchRemoveTagById } = defineBatch({
2082
+ toolName: "batch_remove_tag_by_id",
2083
+ itemSchema: removeTagByIdSchema,
2084
+ 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.",
2085
+ itemHandler: removeTagById
2092
2086
  });
2093
- async function batchRemoveTagById(input, opts = {}) {
2094
- return batchExecute("batch_remove_tag_by_id", input.items, (item) => removeTagById(item), opts);
2095
- }
2096
2087
 
2097
2088
  // src/tools/users.ts
2098
- import { z as z11 } from "zod";
2099
- var listUsersSchema = z11.object({
2100
- page: z11.number().int().positive().optional(),
2101
- perPage: z11.number().int().min(1).max(100).optional()
2089
+ import { z as z14 } from "zod";
2090
+ var listUsersSchema = z14.object({
2091
+ page: z14.number().int().positive().optional(),
2092
+ perPage: z14.number().int().min(1).max(100).optional()
2102
2093
  });
2103
2094
  async function listUsers(input) {
2104
2095
  const { data, nextPage } = await capsuleGetCached("/users", {
@@ -2107,32 +2098,32 @@ async function listUsers(input) {
2107
2098
  });
2108
2099
  return { ...data, nextPage };
2109
2100
  }
2110
- var getCurrentUserSchema = z11.object({});
2101
+ var getCurrentUserSchema = z14.object({});
2111
2102
  async function getCurrentUser(_input) {
2112
2103
  const { data } = await capsuleGet("/users/current");
2113
2104
  return data;
2114
2105
  }
2115
2106
 
2116
2107
  // src/tools/filters.ts
2117
- import { z as z12 } from "zod";
2118
- var FilterConditionSchema = z12.object({
2119
- field: z12.string().describe(
2108
+ import { z as z15 } from "zod";
2109
+ var FilterConditionSchema = z15.object({
2110
+ field: z15.string().describe(
2120
2111
  "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"
2121
2112
  ),
2122
- operator: z12.string().describe(
2113
+ operator: z15.string().describe(
2123
2114
  "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."
2124
2115
  ),
2125
- value: z12.union([z12.string(), z12.number(), z12.boolean(), z12.null()]).describe(
2116
+ value: z15.union([z15.string(), z15.number(), z15.boolean(), z15.null()]).describe(
2126
2117
  "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."
2127
2118
  )
2128
2119
  });
2129
- var FilterInputSchema = z12.object({
2130
- conditions: z12.array(FilterConditionSchema).min(1).describe(
2120
+ var FilterInputSchema = z15.object({
2121
+ conditions: z15.array(FilterConditionSchema).min(1).describe(
2131
2122
  "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)."
2132
2123
  ),
2133
- embed: z12.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
2134
- page: z12.number().int().positive().optional().default(1),
2135
- perPage: z12.number().int().min(1).max(100).optional().default(25)
2124
+ embed: z15.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
2125
+ page: z15.number().int().positive().optional().default(1),
2126
+ perPage: z15.number().int().min(1).max(100).optional().default(25)
2136
2127
  });
2137
2128
  async function runFilter(entityPath, input) {
2138
2129
  const { data, nextPage } = await capsuleSearch(
@@ -2163,12 +2154,12 @@ async function filterProjects(input) {
2163
2154
  }
2164
2155
 
2165
2156
  // src/tools/metadata.ts
2166
- import { z as z13 } from "zod";
2157
+ import { z as z16 } from "zod";
2167
2158
  var paginationFields3 = {
2168
- page: z13.number().int().positive().optional(),
2169
- perPage: z13.number().int().min(1).max(100).optional().describe("Page size, max 100. Defaults to 100 for reference data.")
2159
+ page: z16.number().int().positive().optional(),
2160
+ perPage: z16.number().int().min(1).max(100).optional().describe("Page size, max 100. Defaults to 100 for reference data.")
2170
2161
  };
2171
- var listTeamsSchema = z13.object({ ...paginationFields3 });
2162
+ var listTeamsSchema = z16.object({ ...paginationFields3 });
2172
2163
  async function listTeams(input) {
2173
2164
  const { data, nextPage } = await capsuleGetCached("/teams", {
2174
2165
  page: input.page ?? 1,
@@ -2176,7 +2167,7 @@ async function listTeams(input) {
2176
2167
  });
2177
2168
  return { ...data, nextPage };
2178
2169
  }
2179
- var listLostReasonsSchema = z13.object({ ...paginationFields3 });
2170
+ var listLostReasonsSchema = z16.object({ ...paginationFields3 });
2180
2171
  async function listLostReasons(input) {
2181
2172
  const { data, nextPage } = await capsuleGetCached("/lostreasons", {
2182
2173
  page: input.page ?? 1,
@@ -2184,7 +2175,7 @@ async function listLostReasons(input) {
2184
2175
  });
2185
2176
  return { ...data, nextPage };
2186
2177
  }
2187
- var listActivityTypesSchema = z13.object({ ...paginationFields3 });
2178
+ var listActivityTypesSchema = z16.object({ ...paginationFields3 });
2188
2179
  async function listActivityTypes(input) {
2189
2180
  const { data, nextPage } = await capsuleGetCached(
2190
2181
  "/activitytypes",
@@ -2195,12 +2186,12 @@ async function listActivityTypes(input) {
2195
2186
  );
2196
2187
  return { ...data, nextPage };
2197
2188
  }
2198
- var getSiteSchema = z13.object({});
2189
+ var getSiteSchema = z16.object({});
2199
2190
  async function getSite(_input) {
2200
2191
  const { data } = await capsuleGetCached("/site");
2201
2192
  return data;
2202
2193
  }
2203
- var listTrackDefinitionsSchema = z13.object({ ...paginationFields3 });
2194
+ var listTrackDefinitionsSchema = z16.object({ ...paginationFields3 });
2204
2195
  async function listTrackDefinitions(input) {
2205
2196
  const { data, nextPage } = await capsuleGetCached(
2206
2197
  "/trackdefinitions",
@@ -2208,7 +2199,7 @@ async function listTrackDefinitions(input) {
2208
2199
  );
2209
2200
  return { ...data, nextPage };
2210
2201
  }
2211
- var listCategoriesSchema = z13.object({ ...paginationFields3 });
2202
+ var listCategoriesSchema = z16.object({ ...paginationFields3 });
2212
2203
  async function listCategories(input) {
2213
2204
  const { data, nextPage } = await capsuleGetCached("/categories", {
2214
2205
  page: input.page ?? 1,
@@ -2216,7 +2207,7 @@ async function listCategories(input) {
2216
2207
  });
2217
2208
  return { ...data, nextPage };
2218
2209
  }
2219
- var listGoalsSchema = z13.object({ ...paginationFields3 });
2210
+ var listGoalsSchema = z16.object({ ...paginationFields3 });
2220
2211
  async function listGoals(input) {
2221
2212
  const { data, nextPage } = await capsuleGetCached("/goals", {
2222
2213
  page: input.page ?? 1,
@@ -2226,14 +2217,14 @@ async function listGoals(input) {
2226
2217
  }
2227
2218
 
2228
2219
  // src/tools/audit.ts
2229
- import { z as z14 } from "zod";
2230
- var listEmployeesSchema = z14.object({
2231
- partyId: z14.number().int().positive().describe(
2220
+ import { z as z17 } from "zod";
2221
+ var listEmployeesSchema = z17.object({
2222
+ partyId: positiveId.describe(
2232
2223
  "The organisation's party id. Returns the people whose `organisation` field links to this party."
2233
2224
  ),
2234
- page: z14.number().int().positive().optional().default(1),
2235
- perPage: z14.number().int().min(1).max(100).optional().default(25),
2236
- embed: z14.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
2225
+ page: z17.number().int().positive().optional().default(1),
2226
+ perPage: z17.number().int().min(1).max(100).optional().default(25),
2227
+ embed: z17.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
2237
2228
  });
2238
2229
  async function listEmployees(input) {
2239
2230
  const { data, nextPage } = await capsuleGet(
@@ -2242,15 +2233,15 @@ async function listEmployees(input) {
2242
2233
  );
2243
2234
  return { ...data, nextPage };
2244
2235
  }
2245
- var DeletedSinceSchema = z14.string().describe(
2236
+ var DeletedSinceSchema = z17.string().describe(
2246
2237
  "REQUIRED. ISO-8601 timestamp; only deletions on or after this point are returned. Example: '2026-01-01T00:00:00Z'."
2247
2238
  );
2248
2239
  var DeletedPagination = {
2249
2240
  since: DeletedSinceSchema,
2250
- page: z14.number().int().positive().optional().default(1),
2251
- perPage: z14.number().int().min(1).max(100).optional().default(25)
2241
+ page: z17.number().int().positive().optional().default(1),
2242
+ perPage: z17.number().int().min(1).max(100).optional().default(25)
2252
2243
  };
2253
- var listDeletedPartiesSchema = z14.object(DeletedPagination);
2244
+ var listDeletedPartiesSchema = z17.object(DeletedPagination);
2254
2245
  async function listDeletedParties(input) {
2255
2246
  const { data, nextPage } = await capsuleGet("/parties/deleted", {
2256
2247
  since: input.since,
@@ -2259,7 +2250,7 @@ async function listDeletedParties(input) {
2259
2250
  });
2260
2251
  return { ...data, nextPage };
2261
2252
  }
2262
- var listDeletedOpportunitiesSchema = z14.object(DeletedPagination);
2253
+ var listDeletedOpportunitiesSchema = z17.object(DeletedPagination);
2263
2254
  async function listDeletedOpportunities(input) {
2264
2255
  const { data, nextPage } = await capsuleGet("/opportunities/deleted", {
2265
2256
  since: input.since,
@@ -2268,7 +2259,7 @@ async function listDeletedOpportunities(input) {
2268
2259
  });
2269
2260
  return { ...data, nextPage };
2270
2261
  }
2271
- var listDeletedProjectsSchema = z14.object(DeletedPagination);
2262
+ var listDeletedProjectsSchema = z17.object(DeletedPagination);
2272
2263
  async function listDeletedProjects(input) {
2273
2264
  const { data, nextPage } = await capsuleGet("/kases/deleted", {
2274
2265
  since: input.since,
@@ -2279,14 +2270,14 @@ async function listDeletedProjects(input) {
2279
2270
  }
2280
2271
 
2281
2272
  // src/tools/relationships.ts
2282
- import { z as z15 } from "zod";
2283
- var RelationshipEntity = z15.enum(["opportunities", "kases"]).describe("Which entity has the additional-party links. Use 'kases' for projects.");
2284
- var listAdditionalPartiesSchema = z15.object({
2273
+ import { z as z18 } from "zod";
2274
+ var RelationshipEntity = z18.enum(["opportunities", "kases"]).describe("Which entity has the additional-party links. Use 'kases' for projects.");
2275
+ var listAdditionalPartiesSchema = z18.object({
2285
2276
  entity: RelationshipEntity,
2286
- entityId: z15.number().int().positive().describe("ID of the opportunity or project."),
2287
- embed: z15.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
2288
- page: z15.number().int().positive().optional().default(1),
2289
- perPage: z15.number().int().min(1).max(100).optional().default(25)
2277
+ entityId: positiveId.describe("ID of the opportunity or project."),
2278
+ embed: z18.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
2279
+ page: z18.number().int().positive().optional().default(1),
2280
+ perPage: z18.number().int().min(1).max(100).optional().default(25)
2290
2281
  });
2291
2282
  async function listAdditionalParties(input) {
2292
2283
  const { data, nextPage } = await capsuleGet(
@@ -2295,10 +2286,12 @@ async function listAdditionalParties(input) {
2295
2286
  );
2296
2287
  return { ...data, nextPage };
2297
2288
  }
2298
- var addAdditionalPartySchema = z15.object({
2289
+ var addAdditionalPartySchema = z18.object({
2299
2290
  entity: RelationshipEntity,
2300
- entityId: z15.number().int().positive(),
2301
- partyId: z15.number().int().positive().describe("ID of the party (person or organisation) to link as an additional party.")
2291
+ entityId: positiveId,
2292
+ partyId: positiveId.describe(
2293
+ "ID of the party (person or organisation) to link as an additional party."
2294
+ )
2302
2295
  });
2303
2296
  async function addAdditionalParty(input) {
2304
2297
  try {
@@ -2326,10 +2319,10 @@ async function addAdditionalParty(input) {
2326
2319
  throw err;
2327
2320
  }
2328
2321
  }
2329
- var removeAdditionalPartySchema = z15.object({
2322
+ var removeAdditionalPartySchema = z18.object({
2330
2323
  entity: RelationshipEntity,
2331
- entityId: z15.number().int().positive(),
2332
- partyId: z15.number().int().positive(),
2324
+ entityId: positiveId,
2325
+ partyId: positiveId,
2333
2326
  confirm: confirmFlag().describe(
2334
2327
  "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."
2335
2328
  )
@@ -2356,11 +2349,11 @@ async function removeAdditionalParty(input) {
2356
2349
  })
2357
2350
  );
2358
2351
  }
2359
- var listAssociatedProjectsSchema = z15.object({
2360
- opportunityId: z15.number().int().positive(),
2361
- embed: z15.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
2362
- page: z15.number().int().positive().optional().default(1),
2363
- perPage: z15.number().int().min(1).max(100).optional().default(25)
2352
+ var listAssociatedProjectsSchema = z18.object({
2353
+ opportunityId: positiveId,
2354
+ embed: z18.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
2355
+ page: z18.number().int().positive().optional().default(1),
2356
+ perPage: z18.number().int().min(1).max(100).optional().default(25)
2364
2357
  });
2365
2358
  async function listAssociatedProjects(input) {
2366
2359
  const { data, nextPage } = await capsuleGet(
@@ -2371,9 +2364,9 @@ async function listAssociatedProjects(input) {
2371
2364
  }
2372
2365
 
2373
2366
  // src/tools/custom-fields.ts
2374
- import { z as z16 } from "zod";
2375
- var CustomFieldEntity = z16.enum(["parties", "opportunities", "kases"]).describe("Which entity type's custom field schema to inspect. Use 'kases' for projects.");
2376
- var listCustomFieldsSchema = z16.object({
2367
+ import { z as z19 } from "zod";
2368
+ var CustomFieldEntity = z19.enum(["parties", "opportunities", "kases"]).describe("Which entity type's custom field schema to inspect. Use 'kases' for projects.");
2369
+ var listCustomFieldsSchema = z19.object({
2377
2370
  entity: CustomFieldEntity
2378
2371
  });
2379
2372
  async function listCustomFields(input) {
@@ -2382,9 +2375,9 @@ async function listCustomFields(input) {
2382
2375
  );
2383
2376
  return data;
2384
2377
  }
2385
- var getCustomFieldSchema = z16.object({
2378
+ var getCustomFieldSchema = z19.object({
2386
2379
  entity: CustomFieldEntity,
2387
- fieldId: z16.number().int().positive().describe("Custom field definition id.")
2380
+ fieldId: positiveId.describe("Custom field definition id.")
2388
2381
  });
2389
2382
  async function getCustomField(input) {
2390
2383
  const { data } = await capsuleGetCached(
@@ -2394,11 +2387,11 @@ async function getCustomField(input) {
2394
2387
  }
2395
2388
 
2396
2389
  // src/tools/tracks.ts
2397
- import { z as z17 } from "zod";
2398
- var TrackEntity = z17.enum(["parties", "opportunities", "kases"]).describe("Use 'kases' for projects.");
2399
- var listEntityTracksSchema = z17.object({
2390
+ import { z as z20 } from "zod";
2391
+ var TrackEntity = z20.enum(["parties", "opportunities", "kases"]).describe("Use 'kases' for projects.");
2392
+ var listEntityTracksSchema = z20.object({
2400
2393
  entity: TrackEntity,
2401
- entityId: z17.number().int().positive()
2394
+ entityId: positiveId
2402
2395
  });
2403
2396
  async function listEntityTracks(input) {
2404
2397
  const { data } = await capsuleGet(
@@ -2406,20 +2399,20 @@ async function listEntityTracks(input) {
2406
2399
  );
2407
2400
  return data;
2408
2401
  }
2409
- var showTrackSchema = z17.object({
2410
- trackId: z17.number().int().positive()
2402
+ var showTrackSchema = z20.object({
2403
+ trackId: positiveId
2411
2404
  });
2412
2405
  async function showTrack(input) {
2413
2406
  const { data } = await capsuleGet(`/tracks/${input.trackId}`);
2414
2407
  return data;
2415
2408
  }
2416
- var applyTrackSchema = z17.object({
2417
- entity: z17.enum(["opportunities", "kases"]).describe("Which entity to apply the track to. Use 'kases' for projects."),
2418
- entityId: z17.number().int().positive(),
2419
- trackDefinitionId: z17.number().int().positive().describe(
2409
+ var applyTrackSchema = z20.object({
2410
+ entity: z20.enum(["opportunities", "kases"]).describe("Which entity to apply the track to. Use 'kases' for projects."),
2411
+ entityId: positiveId,
2412
+ trackDefinitionId: positiveId.describe(
2420
2413
  "The trackDefinition to apply (from list_track_definitions). Auto-creates task definitions on the target entity per the track's rules."
2421
2414
  ),
2422
- startDate: z17.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe(
2415
+ startDate: z20.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe(
2423
2416
  "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."
2424
2417
  )
2425
2418
  });
@@ -2432,9 +2425,9 @@ async function applyTrack(input) {
2432
2425
  if (input.startDate !== void 0) track["trackDateOn"] = input.startDate;
2433
2426
  return capsulePost("/tracks", { track });
2434
2427
  }
2435
- var updateTrackSchema = z17.object({
2436
- trackId: z17.number().int().positive(),
2437
- fields: z17.record(z17.string(), z17.unknown()).describe(
2428
+ var updateTrackSchema = z20.object({
2429
+ trackId: positiveId,
2430
+ fields: z20.record(z20.string(), z20.unknown()).describe(
2438
2431
  "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."
2439
2432
  )
2440
2433
  });
@@ -2446,8 +2439,8 @@ async function updateTrack(input) {
2446
2439
  track: input.fields
2447
2440
  });
2448
2441
  }
2449
- var removeTrackSchema = z17.object({
2450
- trackId: z17.number().int().positive(),
2442
+ var removeTrackSchema = z20.object({
2443
+ trackId: positiveId,
2451
2444
  confirm: confirmFlag().describe(
2452
2445
  "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."
2453
2446
  )
@@ -2464,13 +2457,13 @@ async function removeTrack(input) {
2464
2457
  }
2465
2458
 
2466
2459
  // src/tools/attachments.ts
2467
- import { z as z18 } from "zod";
2460
+ import { z as z21 } from "zod";
2468
2461
  var DEFAULT_MAX_SIZE_BYTES = 5 * 1024 * 1024;
2469
2462
  var HARD_MAX_SIZE_BYTES = 25 * 1024 * 1024;
2470
2463
  var HARD_MAX_BASE64_CHARS = Math.ceil(HARD_MAX_SIZE_BYTES / 3) * 4;
2471
- var getAttachmentSchema = z18.object({
2472
- id: z18.number().int().positive().describe("Attachment ID."),
2473
- maxSizeBytes: z18.number().int().positive().max(HARD_MAX_SIZE_BYTES).optional().describe(
2464
+ var getAttachmentSchema = z21.object({
2465
+ id: positiveId.describe("Attachment ID."),
2466
+ maxSizeBytes: z21.number().int().positive().max(HARD_MAX_SIZE_BYTES).optional().describe(
2474
2467
  `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.`
2475
2468
  )
2476
2469
  });
@@ -2485,22 +2478,22 @@ async function getAttachment(input) {
2485
2478
  }
2486
2479
  return { contentType, buffer, sizeBytes };
2487
2480
  }
2488
- var uploadAttachmentSchema = z18.object({
2489
- filename: z18.string().min(1).describe(
2481
+ var uploadAttachmentSchema = z21.object({
2482
+ filename: z21.string().min(1).describe(
2490
2483
  "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."
2491
2484
  ),
2492
- contentType: z18.string().min(1).describe(
2485
+ contentType: z21.string().min(1).describe(
2493
2486
  "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."
2494
2487
  ),
2495
- dataBase64: z18.string().min(1).max(HARD_MAX_BASE64_CHARS).describe(
2488
+ dataBase64: z21.string().min(1).max(HARD_MAX_BASE64_CHARS).describe(
2496
2489
  "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."
2497
2490
  ),
2498
- content: z18.string().optional().describe(
2491
+ content: z21.string().optional().describe(
2499
2492
  "Body text for the note that will hold the attachment. Defaults to '[attachment]' if omitted."
2500
2493
  ),
2501
- partyId: z18.number().int().positive().optional().describe("Link the new note to a party (mutually exclusive with opportunityId / projectId)."),
2502
- opportunityId: z18.number().int().positive().optional(),
2503
- projectId: z18.number().int().positive().optional()
2494
+ partyId: positiveId.optional().describe("Link the new note to a party (mutually exclusive with opportunityId / projectId)."),
2495
+ opportunityId: positiveId.optional(),
2496
+ projectId: positiveId.optional()
2504
2497
  });
2505
2498
  function isValidBase64(s) {
2506
2499
  if (!/^[A-Za-z0-9+/]*={0,2}$/.test(s)) return false;
@@ -2550,23 +2543,23 @@ async function uploadAttachment(input) {
2550
2543
  }
2551
2544
 
2552
2545
  // src/tools/saved-filters.ts
2553
- import { z as z19 } from "zod";
2554
- var EntitySchema = z19.enum(["parties", "opportunities", "kases"]).describe(
2546
+ import { z as z22 } from "zod";
2547
+ var EntitySchema = z22.enum(["parties", "opportunities", "kases"]).describe(
2555
2548
  "Which entity type the filter operates over. Use 'kases' for projects (Capsule's legacy name)."
2556
2549
  );
2557
- var listSavedFiltersSchema = z19.object({
2550
+ var listSavedFiltersSchema = z22.object({
2558
2551
  entity: EntitySchema
2559
2552
  });
2560
2553
  async function listSavedFilters(input) {
2561
2554
  const { data } = await capsuleGetCached(`/${input.entity}/filters`);
2562
2555
  return data;
2563
2556
  }
2564
- var runSavedFilterSchema = z19.object({
2557
+ var runSavedFilterSchema = z22.object({
2565
2558
  entity: EntitySchema,
2566
- id: z19.number().int().positive().describe("The saved filter id (from list_saved_filters)."),
2567
- embed: z19.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
2568
- page: z19.number().int().positive().optional().default(1),
2569
- perPage: z19.number().int().min(1).max(100).optional().default(25)
2559
+ id: positiveId.describe("The saved filter id (from list_saved_filters)."),
2560
+ embed: z22.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
2561
+ page: z22.number().int().positive().optional().default(1),
2562
+ perPage: z22.number().int().min(1).max(100).optional().default(25)
2570
2563
  });
2571
2564
  async function runSavedFilter(input) {
2572
2565
  const { data, nextPage } = await capsuleGet(
@@ -2584,7 +2577,7 @@ function createCapsuleMcpServer(opts) {
2584
2577
  const server2 = new McpServer(
2585
2578
  {
2586
2579
  name: "capsulemcp",
2587
- version: "1.6.1",
2580
+ version: "1.6.2",
2588
2581
  description: "Read and (optionally) modify Capsule CRM data \u2014 parties, opportunities, projects, tasks, timeline entries, pipelines, tags.",
2589
2582
  websiteUrl: "https://github.com/soil-dev/capsulemcp",
2590
2583
  icons: ICONS
@@ -3169,7 +3162,7 @@ function createCapsuleMcpServer(opts) {
3169
3162
  registerTool(
3170
3163
  server2,
3171
3164
  "list_teams",
3172
- "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.",
3165
+ "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.",
3173
3166
  listTeamsSchema,
3174
3167
  listTeams
3175
3168
  );