capsulemcp 1.6.1 → 1.6.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/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,91 +1214,82 @@ 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
+ organisationId: positiveId.nullable().optional().describe(
1261
+ "For PERSON parties: link to an organisation by id, or `null` to unlink (the person becomes an orphan / standalone record). Discover org IDs via search_parties / filter_parties with type=organisation. For ORGANISATION parties: silently ignored by Capsule's API \u2014 organisations don't have a parent organisation in the data model. Empirically verified in v1.6.3 wire-trace; no client-side type guard since the no-op is harmless."
1262
+ ),
1263
+ fields: z6.array(CustomFieldWriteSchema).optional().describe(fieldsArrayDescriptor("get_party")),
1207
1264
  ...PartyWriteBaseSchema
1208
1265
  });
1209
1266
  async function updateParty(input) {
1210
- const { id, ownerId, fields, ...rest } = input;
1267
+ const { id, ownerId, organisationId, fields, ...rest } = input;
1211
1268
  const body = {};
1212
1269
  for (const [k, v] of Object.entries(rest)) {
1213
1270
  if (v !== void 0) body[k] = v;
1214
1271
  }
1215
- if (ownerId) body["owner"] = { id: ownerId };
1272
+ setRef(body, "owner", ownerId);
1273
+ setNullableRef(body, "organisation", organisationId);
1216
1274
  const mappedFields = mapFieldsForBody(fields);
1217
1275
  if (mappedFields !== void 0) body["fields"] = mappedFields;
1218
1276
  return capsulePut(`/parties/${id}`, { party: body });
1219
1277
  }
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
- )
1278
+ var { schema: batchUpdatePartySchema, handler: batchUpdateParty } = defineBatch({
1279
+ toolName: "batch_update_party",
1280
+ itemSchema: updatePartySchema,
1281
+ 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).",
1282
+ itemHandler: updateParty
1224
1283
  });
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
- )
1284
+ var { schema: deletePartySchema, handler: deleteParty } = defineDelete({
1285
+ toolName: "delete_party",
1286
+ pathPrefix: "/parties",
1287
+ 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
1288
  });
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'.")
1289
+ var addPartyEmailAddressSchema = z6.object({
1290
+ partyId: positiveId,
1291
+ address: z6.string().email(),
1292
+ type: z6.string().optional().describe("Free-form label, e.g. 'Work', 'Home'.")
1248
1293
  });
1249
1294
  async function addPartyEmailAddress(input) {
1250
1295
  const { partyId, address, type } = input;
@@ -1254,9 +1299,9 @@ async function addPartyEmailAddress(input) {
1254
1299
  party: { emailAddresses: [item] }
1255
1300
  });
1256
1301
  }
1257
- var removePartyEmailAddressByIdSchema = z3.object({
1258
- partyId: z3.number().int().positive(),
1259
- emailAddressId: z3.number().int().positive().describe(
1302
+ var removePartyEmailAddressByIdSchema = z6.object({
1303
+ partyId: positiveId,
1304
+ emailAddressId: positiveId.describe(
1260
1305
  "Capsule's id for the email-address row. Read it from get_party (each entry in emailAddresses carries an id)."
1261
1306
  )
1262
1307
  });
@@ -1276,10 +1321,10 @@ async function removePartyEmailAddressById(input) {
1276
1321
  () => ({ removed: true, alreadyRemoved: true, partyId, emailAddressId })
1277
1322
  );
1278
1323
  }
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'.")
1324
+ var addPartyPhoneNumberSchema = z6.object({
1325
+ partyId: positiveId,
1326
+ number: z6.string().min(1),
1327
+ type: z6.string().optional().describe("Free-form label, e.g. 'Work', 'Mobile'.")
1283
1328
  });
1284
1329
  async function addPartyPhoneNumber(input) {
1285
1330
  const { partyId, number, type } = input;
@@ -1289,9 +1334,9 @@ async function addPartyPhoneNumber(input) {
1289
1334
  party: { phoneNumbers: [item] }
1290
1335
  });
1291
1336
  }
1292
- var removePartyPhoneNumberByIdSchema = z3.object({
1293
- partyId: z3.number().int().positive(),
1294
- phoneNumberId: z3.number().int().positive().describe(
1337
+ var removePartyPhoneNumberByIdSchema = z6.object({
1338
+ partyId: positiveId,
1339
+ phoneNumberId: positiveId.describe(
1295
1340
  "Capsule's id for the phone-number row. Read it from get_party (each entry in phoneNumbers carries an id)."
1296
1341
  )
1297
1342
  });
@@ -1311,14 +1356,14 @@ async function removePartyPhoneNumberById(input) {
1311
1356
  () => ({ removed: true, alreadyRemoved: true, partyId, phoneNumberId })
1312
1357
  );
1313
1358
  }
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'.")
1359
+ var addPartyAddressSchema = z6.object({
1360
+ partyId: positiveId,
1361
+ street: z6.string().optional(),
1362
+ city: z6.string().optional(),
1363
+ state: z6.string().optional(),
1364
+ country: z6.string().optional().describe(CountryDescription),
1365
+ zip: z6.string().optional(),
1366
+ type: z6.string().optional().describe("Free-form label, e.g. 'Office', 'Home'.")
1322
1367
  });
1323
1368
  async function addPartyAddress(input) {
1324
1369
  const { partyId, ...rest } = input;
@@ -1330,9 +1375,9 @@ async function addPartyAddress(input) {
1330
1375
  party: { addresses: [item] }
1331
1376
  });
1332
1377
  }
1333
- var removePartyAddressByIdSchema = z3.object({
1334
- partyId: z3.number().int().positive(),
1335
- addressId: z3.number().int().positive().describe(
1378
+ var removePartyAddressByIdSchema = z6.object({
1379
+ partyId: positiveId,
1380
+ addressId: positiveId.describe(
1336
1381
  "Capsule's id for the address row. Read it from get_party (each entry in addresses carries an id)."
1337
1382
  )
1338
1383
  });
@@ -1352,9 +1397,9 @@ async function removePartyAddressById(input) {
1352
1397
  () => ({ removed: true, alreadyRemoved: true, partyId, addressId })
1353
1398
  );
1354
1399
  }
1355
- var addPartyWebsiteSchema = z3.object({
1356
- partyId: z3.number().int().positive(),
1357
- address: z3.string().min(1).describe(
1400
+ var addPartyWebsiteSchema = z6.object({
1401
+ partyId: positiveId,
1402
+ address: z6.string().min(1).describe(
1358
1403
  "The website address. A URL when service='URL', or a handle (e.g. '@acmeco') for social services."
1359
1404
  ),
1360
1405
  service: WebsiteServiceEnum.optional().describe("Defaults to 'URL' if omitted.")
@@ -1367,9 +1412,9 @@ async function addPartyWebsite(input) {
1367
1412
  party: { websites: [item] }
1368
1413
  });
1369
1414
  }
1370
- var removePartyWebsiteByIdSchema = z3.object({
1371
- partyId: z3.number().int().positive(),
1372
- websiteId: z3.number().int().positive().describe(
1415
+ var removePartyWebsiteByIdSchema = z6.object({
1416
+ partyId: positiveId,
1417
+ websiteId: positiveId.describe(
1373
1418
  "Capsule's id for the website row. Read it from get_party (each entry in websites carries an id)."
1374
1419
  )
1375
1420
  });
@@ -1391,20 +1436,32 @@ async function removePartyWebsiteById(input) {
1391
1436
  }
1392
1437
 
1393
1438
  // 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({
1439
+ import { z as z7 } from "zod";
1440
+
1441
+ // src/tools/preserve-refs.ts
1442
+ async function readEntityRefs(path, responseKey) {
1443
+ const { data } = await capsuleGet(path);
1444
+ const entity = data[responseKey];
1445
+ return {
1446
+ teamId: entity?.team?.id ?? void 0,
1447
+ stageId: entity?.stage?.id ?? void 0
1448
+ };
1449
+ }
1450
+
1451
+ // src/tools/opportunities.ts
1452
+ var OpportunityValueSchema = z7.object({
1453
+ amount: z7.number().nonnegative(),
1454
+ currency: z7.string({
1398
1455
  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
1456
  }).length(3).describe(
1400
1457
  "ISO 4217 currency code (3 letters), e.g. 'GBP', 'USD', 'EUR'. Required when amount is set."
1401
1458
  )
1402
1459
  });
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)
1460
+ var searchOpportunitiesSchema = z7.object({
1461
+ q: z7.string().optional().describe("Free-text search query"),
1462
+ embed: z7.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
1463
+ page: z7.number().int().positive().optional().default(1),
1464
+ perPage: z7.number().int().min(1).max(100).optional().default(25)
1408
1465
  });
1409
1466
  async function searchOpportunities(input) {
1410
1467
  const path = input.q ? "/opportunities/search" : "/opportunities";
@@ -1416,9 +1473,9 @@ async function searchOpportunities(input) {
1416
1473
  });
1417
1474
  return { ...data, nextPage };
1418
1475
  }
1419
- var getOpportunitySchema = z4.object({
1420
- id: z4.number().int().positive(),
1421
- embed: z4.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
1476
+ var getOpportunitySchema = z7.object({
1477
+ id: positiveId,
1478
+ embed: z7.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
1422
1479
  });
1423
1480
  async function getOpportunity(input) {
1424
1481
  const { data } = await capsuleGet(`/opportunities/${input.id}`, {
@@ -1426,11 +1483,11 @@ async function getOpportunity(input) {
1426
1483
  });
1427
1484
  return data;
1428
1485
  }
1429
- var getOpportunitiesSchema = z4.object({
1430
- ids: z4.array(z4.number().int().positive()).min(1).max(50).describe(
1486
+ var getOpportunitiesSchema = z7.object({
1487
+ ids: z7.array(positiveId).min(1).max(50).describe(
1431
1488
  "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
1489
  ),
1433
- embed: z4.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
1490
+ embed: z7.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
1434
1491
  });
1435
1492
  async function getOpportunities(input) {
1436
1493
  const { ids, embed } = input;
@@ -1451,20 +1508,20 @@ async function getOpportunities(input) {
1451
1508
  );
1452
1509
  return { opportunities: responses.flatMap((r) => r.data.opportunities) };
1453
1510
  }
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."
1511
+ var createOpportunitySchema = z7.object({
1512
+ name: z7.string().min(1),
1513
+ partyId: positiveId.describe("ID of the party this opportunity belongs to"),
1514
+ milestoneId: positiveId.describe(
1515
+ "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
1516
  ),
1460
- description: z4.string().optional(),
1517
+ description: z7.string().optional(),
1461
1518
  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."
1519
+ expectedCloseOn: z7.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("YYYY-MM-DD"),
1520
+ probability: z7.number().int().min(0).max(100).optional(),
1521
+ ownerId: positiveId.optional().describe(
1522
+ "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
1523
  ),
1467
- teamId: z4.number().int().positive().optional().describe(
1524
+ teamId: positiveId.optional().describe(
1468
1525
  "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
1526
  )
1470
1527
  });
@@ -1475,92 +1532,76 @@ async function createOpportunity(input) {
1475
1532
  party: { id: partyId },
1476
1533
  milestone: { id: milestoneId }
1477
1534
  };
1478
- if (ownerId) body["owner"] = { id: ownerId };
1479
- if (teamId) body["team"] = { id: teamId };
1535
+ setRef(body, "owner", ownerId);
1536
+ setRef(body, "team", teamId);
1480
1537
  return capsulePost("/opportunities", { opportunity: body });
1481
1538
  }
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."
1539
+ var updateOpportunitySchema = z7.object({
1540
+ id: positiveId,
1541
+ name: z7.string().min(1).optional(),
1542
+ partyId: positiveId.optional().describe(
1543
+ "Reassign the opportunity to a different primary party. Capsule requires every opportunity to have a party \u2014 passing `null` is rejected with 422 'party is required' (use Capsule's web UI if you need to dissolve the link entirely). Discover ids via search_parties / filter_parties. No defensive read-modify-write needed: this connector verified empirically (v1.6.3 wire-trace) that `party` is a standalone PUT field on /opportunities and does not interact with the asymmetric owner/team semantic from NOTES-ON-CAPSULE-API.md \xA727."
1544
+ ),
1545
+ milestoneId: positiveId.optional().describe(
1546
+ "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
1547
  ),
1488
- description: z4.string().optional(),
1548
+ description: z7.string().optional(),
1489
1549
  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(
1550
+ expectedCloseOn: z7.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional(),
1551
+ probability: z7.number().int().min(0).max(100).optional().describe(
1492
1552
  "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
1553
  ),
1494
- lostReasonId: z4.number().int().positive().optional().describe(
1554
+ lostReasonId: positiveId.optional().describe(
1495
1555
  "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
1556
  ),
1497
- ownerId: z4.number().int().positive().optional().describe(
1557
+ ownerId: positiveId.optional().describe(
1498
1558
  "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
1559
  ),
1500
- teamId: z4.number().int().positive().nullable().optional().describe(
1560
+ teamId: positiveId.nullable().optional().describe(
1501
1561
  "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
1562
  ),
1503
- fields: z4.array(CustomFieldWriteSchema).optional().describe(fieldsArrayDescriptor("get_opportunity"))
1563
+ fields: z7.array(CustomFieldWriteSchema).optional().describe(fieldsArrayDescriptor("get_opportunity"))
1504
1564
  });
1505
1565
  async function updateOpportunity(input) {
1506
- const { id, milestoneId, ownerId, teamId, lostReasonId, fields, ...rest } = input;
1566
+ const { id, partyId, milestoneId, ownerId, teamId, lostReasonId, fields, ...rest } = input;
1507
1567
  const body = {};
1508
1568
  for (const [k, v] of Object.entries(rest)) {
1509
1569
  if (v !== void 0) body[k] = v;
1510
1570
  }
1511
- if (milestoneId) body["milestone"] = { id: milestoneId };
1571
+ setRef(body, "party", partyId);
1572
+ setRef(body, "milestone", milestoneId);
1512
1573
  let resolvedTeamId = teamId;
1513
1574
  if (ownerId !== void 0 && teamId === void 0) {
1514
- const { data } = await capsuleGet(`/opportunities/${id}`);
1515
- resolvedTeamId = data.opportunity?.team?.id ?? void 0;
1575
+ ({ teamId: resolvedTeamId } = await readEntityRefs(`/opportunities/${id}`, "opportunity"));
1516
1576
  }
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 };
1577
+ setRef(body, "owner", ownerId);
1578
+ setNullableRef(body, "team", resolvedTeamId);
1579
+ setRef(body, "lostReason", lostReasonId);
1521
1580
  const mappedFields = mapFieldsForBody(fields);
1522
1581
  if (mappedFields !== void 0) body["fields"] = mappedFields;
1523
1582
  return capsulePut(`/opportunities/${id}`, {
1524
1583
  opportunity: body
1525
1584
  });
1526
1585
  }
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
- )
1586
+ var { schema: batchUpdateOpportunitySchema, handler: batchUpdateOpportunity } = defineBatch({
1587
+ toolName: "batch_update_opportunity",
1588
+ itemSchema: updateOpportunitySchema,
1589
+ 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.",
1590
+ itemHandler: updateOpportunity
1531
1591
  });
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
- )
1592
+ var { schema: deleteOpportunitySchema, handler: deleteOpportunity } = defineDelete({
1593
+ toolName: "delete_opportunity",
1594
+ pathPrefix: "/opportunities",
1595
+ confirmHint: "Must be set to true. Permanently deletes the opportunity. Irreversible."
1545
1596
  });
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
1597
 
1557
1598
  // 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)
1599
+ import { z as z8 } from "zod";
1600
+ var listProjectsSchema = z8.object({
1601
+ status: z8.enum(["OPEN", "CLOSED"]).optional(),
1602
+ embed: z8.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
1603
+ page: z8.number().int().positive().optional().default(1),
1604
+ perPage: z8.number().int().min(1).max(100).optional().default(25)
1564
1605
  });
1565
1606
  async function listProjects(input) {
1566
1607
  const { data, nextPage } = await capsuleGet("/kases", {
@@ -1571,9 +1612,9 @@ async function listProjects(input) {
1571
1612
  });
1572
1613
  return { ...data, nextPage };
1573
1614
  }
1574
- var getProjectSchema = z5.object({
1575
- id: z5.number().int().positive(),
1576
- embed: z5.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
1615
+ var getProjectSchema = z8.object({
1616
+ id: positiveId,
1617
+ embed: z8.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
1577
1618
  });
1578
1619
  async function getProject(input) {
1579
1620
  const { data } = await capsuleGet(`/kases/${input.id}`, {
@@ -1581,11 +1622,11 @@ async function getProject(input) {
1581
1622
  });
1582
1623
  return data;
1583
1624
  }
1584
- var getProjectsSchema = z5.object({
1585
- ids: z5.array(z5.number().int().positive()).min(1).max(50).describe(
1625
+ var getProjectsSchema = z8.object({
1626
+ ids: z8.array(positiveId).min(1).max(50).describe(
1586
1627
  "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
1628
  ),
1588
- embed: z5.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
1629
+ embed: z8.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
1589
1630
  });
1590
1631
  async function getProjects(input) {
1591
1632
  const { ids, embed } = input;
@@ -1603,21 +1644,21 @@ async function getProjects(input) {
1603
1644
  );
1604
1645
  return { kases: responses.flatMap((r) => r.data.kases) };
1605
1646
  }
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(
1647
+ var createProjectSchema = z8.object({
1648
+ name: z8.string().min(1),
1649
+ partyId: positiveId.describe("ID of the party linked to this project"),
1650
+ description: z8.string().optional(),
1651
+ status: z8.enum(["OPEN", "CLOSED"]).optional().describe("Defaults to OPEN when omitted."),
1652
+ ownerId: positiveId.optional().describe(
1612
1653
  "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
1654
  ),
1614
- teamId: z5.number().int().positive().optional().describe(
1655
+ teamId: positiveId.optional().describe(
1615
1656
  "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
1657
  ),
1617
- stageId: z5.number().int().positive().optional().describe(
1658
+ stageId: positiveId.optional().describe(
1618
1659
  "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
1660
  ),
1620
- expectedCloseOn: z5.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("YYYY-MM-DD")
1661
+ expectedCloseOn: z8.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("YYYY-MM-DD")
1621
1662
  });
1622
1663
  async function createProject(input) {
1623
1664
  const { partyId, ownerId, teamId, status, stageId, ...rest } = input;
@@ -1626,88 +1667,75 @@ async function createProject(input) {
1626
1667
  status: status ?? "OPEN",
1627
1668
  party: { id: partyId }
1628
1669
  };
1629
- if (ownerId) body["owner"] = { id: ownerId };
1630
- if (teamId) body["team"] = { id: teamId };
1670
+ setRef(body, "owner", ownerId);
1671
+ setRef(body, "team", teamId);
1631
1672
  if (stageId) body["stage"] = stageId;
1632
1673
  return capsulePost("/kases", { kase: body });
1633
1674
  }
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(
1675
+ var updateProjectSchema = z8.object({
1676
+ id: positiveId,
1677
+ name: z8.string().min(1).optional(),
1678
+ description: z8.string().optional(),
1679
+ status: z8.enum(["OPEN", "CLOSED"]).optional(),
1680
+ partyId: positiveId.optional().describe(
1681
+ "Reassign the project to a different primary party. Capsule requires every project to have a party \u2014 passing `null` is rejected with 422 'party is required' (verified empirically in v1.6.3 wire-trace). Discover ids via search_parties / filter_parties."
1682
+ ),
1683
+ ownerId: positiveId.nullable().optional().describe(
1640
1684
  "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
1685
  ),
1642
- teamId: z5.number().int().positive().nullable().optional().describe(
1686
+ teamId: positiveId.nullable().optional().describe(
1643
1687
  "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
1688
  ),
1645
- stageId: z5.number().int().positive().optional().describe(
1689
+ stageId: positiveId.optional().describe(
1646
1690
  "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
1691
  ),
1648
- expectedCloseOn: z5.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("YYYY-MM-DD"),
1649
- fields: z5.array(CustomFieldWriteSchema).optional().describe(
1692
+ expectedCloseOn: z8.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("YYYY-MM-DD"),
1693
+ fields: z8.array(CustomFieldWriteSchema).optional().describe(
1650
1694
  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
1695
  )
1652
1696
  });
1653
1697
  async function updateProject(input) {
1654
- const { id, ownerId, teamId, stageId, fields, ...rest } = input;
1698
+ const { id, partyId, ownerId, teamId, stageId, fields, ...rest } = input;
1655
1699
  const body = {};
1656
1700
  for (const [k, v] of Object.entries(rest)) {
1657
1701
  if (v !== void 0) body[k] = v;
1658
1702
  }
1703
+ setRef(body, "party", partyId);
1659
1704
  let resolvedTeamId = teamId;
1660
1705
  let resolvedStageId = stageId;
1661
1706
  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
- }
1707
+ const current = await readEntityRefs(`/kases/${id}`, "kase");
1708
+ if (teamId === void 0) resolvedTeamId = current.teamId;
1709
+ if (stageId === void 0) resolvedStageId = current.stageId;
1669
1710
  }
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 };
1711
+ setNullableRef(body, "owner", ownerId);
1712
+ setNullableRef(body, "team", resolvedTeamId);
1674
1713
  if (resolvedStageId) body["stage"] = resolvedStageId;
1675
1714
  const mappedFields = mapFieldsForBody(fields);
1676
1715
  if (mappedFields !== void 0) body["fields"] = mappedFields;
1677
1716
  return capsulePut(`/kases/${id}`, { kase: body });
1678
1717
  }
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
- )
1718
+ var { schema: deleteProjectSchema, handler: deleteProject } = defineDelete({
1719
+ toolName: "delete_project",
1720
+ pathPrefix: "/kases",
1721
+ confirmHint: "Must be set to true. Permanently deletes the project (case). Consider update_project status='CLOSED' instead. Irreversible."
1684
1722
  });
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
1723
 
1696
1724
  // src/tools/tasks.ts
1697
- import { z as z6 } from "zod";
1698
- var listTasksSchema = z6.object({
1725
+ import { z as z9 } from "zod";
1726
+ var listTasksSchema = z9.object({
1699
1727
  // Note: Capsule has a third internal status `PENDING` (a task that's
1700
1728
  // part of an active track but not yet "open"), but it can only be
1701
1729
  // reached via track machinery — it is NOT directly settable by
1702
1730
  // /tasks PUT, and a list filter for it returns the same as OPEN
1703
1731
  // anyway. We expose only the two values that are actually filterable
1704
1732
  // by the v2 API.
1705
- status: z6.enum(["OPEN", "COMPLETED"]).optional().describe(
1733
+ status: z9.enum(["OPEN", "COMPLETED"]).optional().describe(
1706
1734
  "Defaults to OPEN when omitted. Pass COMPLETED to filter to completed tasks, or 'OPEN' explicitly."
1707
1735
  ),
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)
1736
+ ownerId: positiveId.optional().describe("Filter to tasks owned by this user ID"),
1737
+ page: z9.number().int().positive().optional().default(1),
1738
+ perPage: z9.number().int().min(1).max(100).optional().default(25)
1711
1739
  });
1712
1740
  async function listTasks(input) {
1713
1741
  const { data, nextPage } = await capsuleGet("/tasks", {
@@ -1721,15 +1749,15 @@ async function listTasks(input) {
1721
1749
  });
1722
1750
  return { ...data, nextPage };
1723
1751
  }
1724
- var getTaskSchema = z6.object({
1725
- id: z6.number().int().positive().describe("Task ID")
1752
+ var getTaskSchema = z9.object({
1753
+ id: positiveId.describe("Task ID")
1726
1754
  });
1727
1755
  async function getTask(input) {
1728
1756
  const { data } = await capsuleGet(`/tasks/${input.id}`);
1729
1757
  return data;
1730
1758
  }
1731
- var getTasksSchema = z6.object({
1732
- ids: z6.array(z6.number().int().positive()).min(1).max(50).describe(
1759
+ var getTasksSchema = z9.object({
1760
+ ids: z9.array(positiveId).min(1).max(50).describe(
1733
1761
  "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
1762
  )
1735
1763
  });
@@ -1745,17 +1773,17 @@ async function getTasks(input) {
1745
1773
  );
1746
1774
  return { tasks: responses.flatMap((r) => r.data.tasks) };
1747
1775
  }
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(
1776
+ var createTaskSchema = z9.object({
1777
+ description: z9.string().min(1),
1778
+ dueOn: z9.string().regex(/^\d{4}-\d{2}-\d{2}$/).describe("YYYY-MM-DD"),
1779
+ dueTime: z9.string().regex(/^\d{2}:\d{2}$/).optional().describe("HH:MM in user's timezone"),
1780
+ detail: z9.string().optional(),
1781
+ ownerId: positiveId.optional().describe(
1754
1782
  "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
1783
  ),
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)")
1784
+ partyId: positiveId.optional().describe("Link task to a party (mutually exclusive with opportunityId/projectId)"),
1785
+ opportunityId: positiveId.optional().describe("Link task to an opportunity (mutually exclusive with partyId/projectId)"),
1786
+ projectId: positiveId.optional().describe("Link task to a project (mutually exclusive with partyId/opportunityId)")
1759
1787
  });
1760
1788
  async function createTask(input) {
1761
1789
  const linked = [input.partyId, input.opportunityId, input.projectId].filter(Boolean);
@@ -1764,79 +1792,86 @@ async function createTask(input) {
1764
1792
  }
1765
1793
  const { ownerId, partyId, opportunityId, projectId, ...rest } = input;
1766
1794
  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 };
1795
+ setRef(body, "owner", ownerId);
1796
+ setRef(body, "party", partyId);
1797
+ setRef(body, "opportunity", opportunityId);
1798
+ setRef(body, "kase", projectId);
1771
1799
  return capsulePost("/tasks", { task: body });
1772
1800
  }
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(),
1801
+ var updateTaskSchema = z9.object({
1802
+ id: positiveId,
1803
+ description: z9.string().min(1).optional(),
1804
+ dueOn: z9.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("YYYY-MM-DD"),
1805
+ dueTime: z9.string().regex(/^\d{2}:\d{2}$/).optional().describe("HH:MM in user's timezone"),
1806
+ detail: z9.string().optional(),
1779
1807
  // Capsule rejects direct sets of `PENDING` (which is a track-machinery
1780
1808
  // internal state) with 422 "cannot set task status to PENDING".
1781
1809
  // Only OPEN and COMPLETED are settable here.
1782
- status: z6.enum(["OPEN", "COMPLETED"]).optional().describe(
1810
+ status: z9.enum(["OPEN", "COMPLETED"]).optional().describe(
1783
1811
  "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
1812
  ),
1785
- ownerId: z6.number().int().positive().optional().describe(
1813
+ ownerId: positiveId.optional().describe(
1786
1814
  "Reassign owner to user ID. Once set, this connector cannot clear an owner back to null \u2014 use Capsule's web UI for that."
1815
+ ),
1816
+ partyId: positiveId.nullable().optional().describe(
1817
+ "Re-link the task to a party by id, or `null` to orphan it. Mutually exclusive with `opportunityId` / `projectId` \u2014 Capsule enforces 'task can be related to at most one entity' server-side (422 if two parent-refs are set at once, verified in v1.6.3 wire-trace). To swap parent type atomically, pass the old one as `null` and the new one as an id in the same call."
1818
+ ),
1819
+ opportunityId: positiveId.nullable().optional().describe(
1820
+ "Re-link the task to an opportunity by id, or `null` to orphan it. Mutually exclusive with `partyId` / `projectId` \u2014 see `partyId` for the XOR semantic."
1821
+ ),
1822
+ projectId: positiveId.nullable().optional().describe(
1823
+ "Re-link the task to a project (kase) by id, or `null` to orphan it. Mutually exclusive with `partyId` / `opportunityId` \u2014 see `partyId` for the XOR semantic."
1787
1824
  )
1788
1825
  });
1789
1826
  async function updateTask(input) {
1790
- const { id, ownerId, ...rest } = input;
1827
+ const { id, ownerId, partyId, opportunityId, projectId, ...rest } = input;
1828
+ const setCount = [partyId, opportunityId, projectId].filter((v) => typeof v === "number").length;
1829
+ if (setCount > 1) {
1830
+ throw new Error(
1831
+ "update_task: provide at most one of partyId, opportunityId, or projectId (Capsule rejects multi-parent tasks with 422 'task can be related to at most one entity')"
1832
+ );
1833
+ }
1791
1834
  const body = {};
1792
1835
  for (const [k, v] of Object.entries(rest)) {
1793
1836
  if (v !== void 0) body[k] = v;
1794
1837
  }
1795
- if (ownerId) body["owner"] = { id: ownerId };
1838
+ setRef(body, "owner", ownerId);
1839
+ setNullableRef(body, "party", partyId);
1840
+ setNullableRef(body, "opportunity", opportunityId);
1841
+ setNullableRef(body, "kase", projectId);
1796
1842
  return capsulePut(`/tasks/${id}`, { task: body });
1797
1843
  }
1798
- var completeTaskSchema = z6.object({
1799
- id: z6.number().int().positive()
1844
+ var completeTaskSchema = z9.object({
1845
+ id: positiveId
1800
1846
  });
1801
1847
  async function completeTask(input) {
1802
1848
  return capsulePut(`/tasks/${input.id}`, {
1803
1849
  task: { status: "COMPLETED" }
1804
1850
  });
1805
1851
  }
1806
- var batchCompleteTaskSchema = z6.object({
1807
- ids: z6.array(z6.number().int().positive()).min(1).max(50).describe(
1852
+ var batchCompleteTaskSchema = z9.object({
1853
+ ids: z9.array(positiveId).min(1).max(50).describe(
1808
1854
  "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
1855
  )
1810
1856
  });
1811
1857
  async function batchCompleteTask(input, opts = {}) {
1812
1858
  return batchExecute("batch_complete_task", input.ids, (id) => completeTask({ id }), opts);
1813
1859
  }
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
- )
1860
+ var { schema: deleteTaskSchema, handler: deleteTask } = defineDelete({
1861
+ toolName: "delete_task",
1862
+ pathPrefix: "/tasks",
1863
+ confirmHint: "Must be set to true. Permanently deletes the task. To mark done without losing history use complete_task. Irreversible."
1819
1864
  });
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
1865
 
1831
1866
  // src/tools/entries.ts
1832
- import { z as z7 } from "zod";
1867
+ import { z as z10 } from "zod";
1833
1868
  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)
1869
+ page: z10.number().int().positive().optional().default(1),
1870
+ perPage: z10.number().int().min(1).max(100).optional().default(25),
1871
+ embed: z10.string().optional().describe(EMBED_ATTACHMENTS_PARTICIPANTS_DESCRIPTION)
1837
1872
  };
1838
- var listPartyEntriesSchema = z7.object({
1839
- partyId: z7.number().int().positive(),
1873
+ var listPartyEntriesSchema = z10.object({
1874
+ partyId: positiveId,
1840
1875
  ...listEntriesPagination
1841
1876
  });
1842
1877
  async function listPartyEntries(input) {
@@ -1846,8 +1881,8 @@ async function listPartyEntries(input) {
1846
1881
  );
1847
1882
  return { ...data, nextPage };
1848
1883
  }
1849
- var listOpportunityEntriesSchema = z7.object({
1850
- opportunityId: z7.number().int().positive(),
1884
+ var listOpportunityEntriesSchema = z10.object({
1885
+ opportunityId: positiveId,
1851
1886
  ...listEntriesPagination
1852
1887
  });
1853
1888
  async function listOpportunityEntries(input) {
@@ -1857,8 +1892,8 @@ async function listOpportunityEntries(input) {
1857
1892
  );
1858
1893
  return { ...data, nextPage };
1859
1894
  }
1860
- var listProjectEntriesSchema = z7.object({
1861
- projectId: z7.number().int().positive(),
1895
+ var listProjectEntriesSchema = z10.object({
1896
+ projectId: positiveId,
1862
1897
  ...listEntriesPagination
1863
1898
  });
1864
1899
  async function listProjectEntries(input) {
@@ -1868,9 +1903,9 @@ async function listProjectEntries(input) {
1868
1903
  );
1869
1904
  return { ...data, nextPage };
1870
1905
  }
1871
- var getEntrySchema = z7.object({
1872
- id: z7.number().int().positive(),
1873
- embed: z7.string().optional().describe(EMBED_ATTACHMENTS_PARTICIPANTS_DESCRIPTION)
1906
+ var getEntrySchema = z10.object({
1907
+ id: positiveId,
1908
+ embed: z10.string().optional().describe(EMBED_ATTACHMENTS_PARTICIPANTS_DESCRIPTION)
1874
1909
  });
1875
1910
  async function getEntry(input) {
1876
1911
  const { data } = await capsuleGet(`/entries/${input.id}`, {
@@ -1878,7 +1913,7 @@ async function getEntry(input) {
1878
1913
  });
1879
1914
  return data;
1880
1915
  }
1881
- var listEntriesSchema = z7.object({
1916
+ var listEntriesSchema = z10.object({
1882
1917
  ...listEntriesPagination
1883
1918
  });
1884
1919
  async function listEntries(input) {
@@ -1889,14 +1924,14 @@ async function listEntries(input) {
1889
1924
  });
1890
1925
  return { ...data, nextPage };
1891
1926
  }
1892
- var addNoteSchema = z7.object({
1893
- content: z7.string().min(1).describe(
1927
+ var addNoteSchema = z10.object({
1928
+ content: z10.string().min(1).describe(
1894
1929
  "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
1930
  ),
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(
1931
+ partyId: positiveId.optional().describe("Link note to a party (mutually exclusive with opportunityId/projectId)"),
1932
+ opportunityId: positiveId.optional().describe("Link note to an opportunity (mutually exclusive with partyId/projectId)"),
1933
+ projectId: positiveId.optional().describe("Link note to a project (mutually exclusive with partyId/opportunityId)"),
1934
+ 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
1935
  "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
1936
  )
1902
1937
  });
@@ -1907,18 +1942,18 @@ async function addNote(input) {
1907
1942
  throw new Error("Provide exactly one of partyId, opportunityId, or projectId");
1908
1943
  }
1909
1944
  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 };
1945
+ setRef(body, "party", partyId);
1946
+ setRef(body, "opportunity", opportunityId);
1947
+ setRef(body, "kase", projectId);
1913
1948
  if (entryAt !== void 0) body["entryAt"] = entryAt;
1914
1949
  return capsulePost("/entries", { entry: body });
1915
1950
  }
1916
- var updateEntrySchema = z7.object({
1917
- id: z7.number().int().positive().describe("Entry ID to update"),
1918
- content: z7.string().min(1).optional().describe(
1951
+ var updateEntrySchema = z10.object({
1952
+ id: positiveId.describe("Entry ID to update"),
1953
+ content: z10.string().min(1).optional().describe(
1919
1954
  "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
1955
  ),
1921
- subject: z7.string().optional().describe(
1956
+ subject: z10.string().optional().describe(
1922
1957
  "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
1958
  )
1924
1959
  });
@@ -1932,30 +1967,20 @@ async function updateEntry(input) {
1932
1967
  }
1933
1968
  return capsulePut(`/entries/${id}`, { entry: body });
1934
1969
  }
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
- )
1970
+ var { schema: deleteEntrySchema, handler: deleteEntry } = defineDelete({
1971
+ toolName: "delete_entry",
1972
+ pathPrefix: "/entries",
1973
+ confirmHint: "Must be set to true. Permanently deletes the entry \u2014 use this to remove a note from a party/opportunity/project. Irreversible.",
1974
+ idDescription: "Entry (note/email/task-record) ID"
1940
1975
  });
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
1976
 
1952
1977
  // src/tools/pipelines.ts
1953
- import { z as z8 } from "zod";
1978
+ import { z as z11 } from "zod";
1954
1979
  var paginationFields = {
1955
- page: z8.number().int().positive().optional(),
1956
- perPage: z8.number().int().min(1).max(100).optional()
1980
+ page: z11.number().int().positive().optional(),
1981
+ perPage: z11.number().int().min(1).max(100).optional()
1957
1982
  };
1958
- var listPipelinesSchema = z8.object({ ...paginationFields });
1983
+ var listPipelinesSchema = z11.object({ ...paginationFields });
1959
1984
  async function listPipelines(input) {
1960
1985
  const { data, nextPage } = await capsuleGetCached("/pipelines", {
1961
1986
  page: input.page ?? 1,
@@ -1963,8 +1988,8 @@ async function listPipelines(input) {
1963
1988
  });
1964
1989
  return { ...data, nextPage };
1965
1990
  }
1966
- var listMilestonesSchema = z8.object({
1967
- pipelineId: z8.number().int().positive(),
1991
+ var listMilestonesSchema = z11.object({
1992
+ pipelineId: positiveId,
1968
1993
  ...paginationFields
1969
1994
  });
1970
1995
  async function listMilestones(input) {
@@ -1976,12 +2001,12 @@ async function listMilestones(input) {
1976
2001
  }
1977
2002
 
1978
2003
  // src/tools/boards.ts
1979
- import { z as z9 } from "zod";
2004
+ import { z as z12 } from "zod";
1980
2005
  var paginationFields2 = {
1981
- page: z9.number().int().positive().optional(),
1982
- perPage: z9.number().int().min(1).max(100).optional()
2006
+ page: z12.number().int().positive().optional(),
2007
+ perPage: z12.number().int().min(1).max(100).optional()
1983
2008
  };
1984
- var listBoardsSchema = z9.object({ ...paginationFields2 });
2009
+ var listBoardsSchema = z12.object({ ...paginationFields2 });
1985
2010
  async function listBoards(input) {
1986
2011
  const { data, nextPage } = await capsuleGetCached("/boards", {
1987
2012
  page: input.page ?? 1,
@@ -1989,8 +2014,8 @@ async function listBoards(input) {
1989
2014
  });
1990
2015
  return { ...data, nextPage };
1991
2016
  }
1992
- var listStagesSchema = z9.object({
1993
- boardId: z9.number().int().positive().optional().describe(
2017
+ var listStagesSchema = z12.object({
2018
+ boardId: positiveId.optional().describe(
1994
2019
  "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
2020
  ),
1996
2021
  ...paginationFields2
@@ -2005,7 +2030,7 @@ async function listStages(input) {
2005
2030
  }
2006
2031
 
2007
2032
  // src/tools/tags.ts
2008
- import { z as z10 } from "zod";
2033
+ import { z as z13 } from "zod";
2009
2034
  var TAG_LIST_PATH = {
2010
2035
  parties: "/parties/tags",
2011
2036
  opportunities: "/opportunities/tags",
@@ -2016,11 +2041,11 @@ var ENTITY_TO_WRAPPER = {
2016
2041
  opportunities: "opportunity",
2017
2042
  kases: "kase"
2018
2043
  };
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()
2044
+ var TagEntity = z13.enum(["parties", "opportunities", "kases"]).describe("Which entity type. Use 'kases' for projects (Capsule's legacy path name).");
2045
+ var listTagsSchema = z13.object({
2046
+ entity: z13.enum(["parties", "opportunities", "kases"]).describe("The resource type to list tags for"),
2047
+ page: z13.number().int().positive().optional(),
2048
+ perPage: z13.number().int().min(1).max(100).optional()
2024
2049
  });
2025
2050
  async function listTags(input) {
2026
2051
  const path = TAG_LIST_PATH[input.entity];
@@ -2030,10 +2055,10 @@ async function listTags(input) {
2030
2055
  });
2031
2056
  return { ...data, nextPage };
2032
2057
  }
2033
- var addTagSchema = z10.object({
2058
+ var addTagSchema = z13.object({
2034
2059
  entity: TagEntity,
2035
- entityId: z10.number().int().positive().describe("The party/opportunity/kase id."),
2036
- tagName: z10.string().min(1).describe(
2060
+ entityId: positiveId.describe("The party/opportunity/kase id."),
2061
+ tagName: z13.string().min(1).describe(
2037
2062
  "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
2063
  )
2039
2064
  });
@@ -2046,10 +2071,10 @@ async function addTag(input) {
2046
2071
  invalidateByPrefix(TAG_LIST_PATH[entity], "add_tag");
2047
2072
  return result;
2048
2073
  }
2049
- var removeTagByIdSchema = z10.object({
2074
+ var removeTagByIdSchema = z13.object({
2050
2075
  entity: TagEntity,
2051
- entityId: z10.number().int().positive().describe("The party/opportunity/kase id."),
2052
- tagId: z10.number().int().positive().describe(
2076
+ entityId: positiveId.describe("The party/opportunity/kase id."),
2077
+ tagId: positiveId.describe(
2053
2078
  "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
2079
  )
2055
2080
  });
@@ -2077,28 +2102,24 @@ async function removeTagById(input) {
2077
2102
  invalidateByPrefix(TAG_LIST_PATH[entity], "remove_tag_by_id");
2078
2103
  return result;
2079
2104
  }
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
- )
2105
+ var { schema: batchAddTagSchema, handler: batchAddTag } = defineBatch({
2106
+ toolName: "batch_add_tag",
2107
+ itemSchema: addTagSchema,
2108
+ 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.",
2109
+ itemHandler: addTag
2084
2110
  });
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
- )
2111
+ var { schema: batchRemoveTagByIdSchema, handler: batchRemoveTagById } = defineBatch({
2112
+ toolName: "batch_remove_tag_by_id",
2113
+ itemSchema: removeTagByIdSchema,
2114
+ 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.",
2115
+ itemHandler: removeTagById
2092
2116
  });
2093
- async function batchRemoveTagById(input, opts = {}) {
2094
- return batchExecute("batch_remove_tag_by_id", input.items, (item) => removeTagById(item), opts);
2095
- }
2096
2117
 
2097
2118
  // 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()
2119
+ import { z as z14 } from "zod";
2120
+ var listUsersSchema = z14.object({
2121
+ page: z14.number().int().positive().optional(),
2122
+ perPage: z14.number().int().min(1).max(100).optional()
2102
2123
  });
2103
2124
  async function listUsers(input) {
2104
2125
  const { data, nextPage } = await capsuleGetCached("/users", {
@@ -2107,32 +2128,32 @@ async function listUsers(input) {
2107
2128
  });
2108
2129
  return { ...data, nextPage };
2109
2130
  }
2110
- var getCurrentUserSchema = z11.object({});
2131
+ var getCurrentUserSchema = z14.object({});
2111
2132
  async function getCurrentUser(_input) {
2112
2133
  const { data } = await capsuleGet("/users/current");
2113
2134
  return data;
2114
2135
  }
2115
2136
 
2116
2137
  // src/tools/filters.ts
2117
- import { z as z12 } from "zod";
2118
- var FilterConditionSchema = z12.object({
2119
- field: z12.string().describe(
2138
+ import { z as z15 } from "zod";
2139
+ var FilterConditionSchema = z15.object({
2140
+ field: z15.string().describe(
2120
2141
  "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
2142
  ),
2122
- operator: z12.string().describe(
2143
+ operator: z15.string().describe(
2123
2144
  "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
2145
  ),
2125
- value: z12.union([z12.string(), z12.number(), z12.boolean(), z12.null()]).describe(
2146
+ value: z15.union([z15.string(), z15.number(), z15.boolean(), z15.null()]).describe(
2126
2147
  "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
2148
  )
2128
2149
  });
2129
- var FilterInputSchema = z12.object({
2130
- conditions: z12.array(FilterConditionSchema).min(1).describe(
2150
+ var FilterInputSchema = z15.object({
2151
+ conditions: z15.array(FilterConditionSchema).min(1).describe(
2131
2152
  "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
2153
  ),
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)
2154
+ embed: z15.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
2155
+ page: z15.number().int().positive().optional().default(1),
2156
+ perPage: z15.number().int().min(1).max(100).optional().default(25)
2136
2157
  });
2137
2158
  async function runFilter(entityPath, input) {
2138
2159
  const { data, nextPage } = await capsuleSearch(
@@ -2163,12 +2184,12 @@ async function filterProjects(input) {
2163
2184
  }
2164
2185
 
2165
2186
  // src/tools/metadata.ts
2166
- import { z as z13 } from "zod";
2187
+ import { z as z16 } from "zod";
2167
2188
  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.")
2189
+ page: z16.number().int().positive().optional(),
2190
+ perPage: z16.number().int().min(1).max(100).optional().describe("Page size, max 100. Defaults to 100 for reference data.")
2170
2191
  };
2171
- var listTeamsSchema = z13.object({ ...paginationFields3 });
2192
+ var listTeamsSchema = z16.object({ ...paginationFields3 });
2172
2193
  async function listTeams(input) {
2173
2194
  const { data, nextPage } = await capsuleGetCached("/teams", {
2174
2195
  page: input.page ?? 1,
@@ -2176,7 +2197,7 @@ async function listTeams(input) {
2176
2197
  });
2177
2198
  return { ...data, nextPage };
2178
2199
  }
2179
- var listLostReasonsSchema = z13.object({ ...paginationFields3 });
2200
+ var listLostReasonsSchema = z16.object({ ...paginationFields3 });
2180
2201
  async function listLostReasons(input) {
2181
2202
  const { data, nextPage } = await capsuleGetCached("/lostreasons", {
2182
2203
  page: input.page ?? 1,
@@ -2184,7 +2205,7 @@ async function listLostReasons(input) {
2184
2205
  });
2185
2206
  return { ...data, nextPage };
2186
2207
  }
2187
- var listActivityTypesSchema = z13.object({ ...paginationFields3 });
2208
+ var listActivityTypesSchema = z16.object({ ...paginationFields3 });
2188
2209
  async function listActivityTypes(input) {
2189
2210
  const { data, nextPage } = await capsuleGetCached(
2190
2211
  "/activitytypes",
@@ -2195,12 +2216,12 @@ async function listActivityTypes(input) {
2195
2216
  );
2196
2217
  return { ...data, nextPage };
2197
2218
  }
2198
- var getSiteSchema = z13.object({});
2219
+ var getSiteSchema = z16.object({});
2199
2220
  async function getSite(_input) {
2200
2221
  const { data } = await capsuleGetCached("/site");
2201
2222
  return data;
2202
2223
  }
2203
- var listTrackDefinitionsSchema = z13.object({ ...paginationFields3 });
2224
+ var listTrackDefinitionsSchema = z16.object({ ...paginationFields3 });
2204
2225
  async function listTrackDefinitions(input) {
2205
2226
  const { data, nextPage } = await capsuleGetCached(
2206
2227
  "/trackdefinitions",
@@ -2208,7 +2229,7 @@ async function listTrackDefinitions(input) {
2208
2229
  );
2209
2230
  return { ...data, nextPage };
2210
2231
  }
2211
- var listCategoriesSchema = z13.object({ ...paginationFields3 });
2232
+ var listCategoriesSchema = z16.object({ ...paginationFields3 });
2212
2233
  async function listCategories(input) {
2213
2234
  const { data, nextPage } = await capsuleGetCached("/categories", {
2214
2235
  page: input.page ?? 1,
@@ -2216,7 +2237,7 @@ async function listCategories(input) {
2216
2237
  });
2217
2238
  return { ...data, nextPage };
2218
2239
  }
2219
- var listGoalsSchema = z13.object({ ...paginationFields3 });
2240
+ var listGoalsSchema = z16.object({ ...paginationFields3 });
2220
2241
  async function listGoals(input) {
2221
2242
  const { data, nextPage } = await capsuleGetCached("/goals", {
2222
2243
  page: input.page ?? 1,
@@ -2226,14 +2247,14 @@ async function listGoals(input) {
2226
2247
  }
2227
2248
 
2228
2249
  // src/tools/audit.ts
2229
- import { z as z14 } from "zod";
2230
- var listEmployeesSchema = z14.object({
2231
- partyId: z14.number().int().positive().describe(
2250
+ import { z as z17 } from "zod";
2251
+ var listEmployeesSchema = z17.object({
2252
+ partyId: positiveId.describe(
2232
2253
  "The organisation's party id. Returns the people whose `organisation` field links to this party."
2233
2254
  ),
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)
2255
+ page: z17.number().int().positive().optional().default(1),
2256
+ perPage: z17.number().int().min(1).max(100).optional().default(25),
2257
+ embed: z17.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
2237
2258
  });
2238
2259
  async function listEmployees(input) {
2239
2260
  const { data, nextPage } = await capsuleGet(
@@ -2242,15 +2263,15 @@ async function listEmployees(input) {
2242
2263
  );
2243
2264
  return { ...data, nextPage };
2244
2265
  }
2245
- var DeletedSinceSchema = z14.string().describe(
2266
+ var DeletedSinceSchema = z17.string().describe(
2246
2267
  "REQUIRED. ISO-8601 timestamp; only deletions on or after this point are returned. Example: '2026-01-01T00:00:00Z'."
2247
2268
  );
2248
2269
  var DeletedPagination = {
2249
2270
  since: DeletedSinceSchema,
2250
- page: z14.number().int().positive().optional().default(1),
2251
- perPage: z14.number().int().min(1).max(100).optional().default(25)
2271
+ page: z17.number().int().positive().optional().default(1),
2272
+ perPage: z17.number().int().min(1).max(100).optional().default(25)
2252
2273
  };
2253
- var listDeletedPartiesSchema = z14.object(DeletedPagination);
2274
+ var listDeletedPartiesSchema = z17.object(DeletedPagination);
2254
2275
  async function listDeletedParties(input) {
2255
2276
  const { data, nextPage } = await capsuleGet("/parties/deleted", {
2256
2277
  since: input.since,
@@ -2259,7 +2280,7 @@ async function listDeletedParties(input) {
2259
2280
  });
2260
2281
  return { ...data, nextPage };
2261
2282
  }
2262
- var listDeletedOpportunitiesSchema = z14.object(DeletedPagination);
2283
+ var listDeletedOpportunitiesSchema = z17.object(DeletedPagination);
2263
2284
  async function listDeletedOpportunities(input) {
2264
2285
  const { data, nextPage } = await capsuleGet("/opportunities/deleted", {
2265
2286
  since: input.since,
@@ -2268,7 +2289,7 @@ async function listDeletedOpportunities(input) {
2268
2289
  });
2269
2290
  return { ...data, nextPage };
2270
2291
  }
2271
- var listDeletedProjectsSchema = z14.object(DeletedPagination);
2292
+ var listDeletedProjectsSchema = z17.object(DeletedPagination);
2272
2293
  async function listDeletedProjects(input) {
2273
2294
  const { data, nextPage } = await capsuleGet("/kases/deleted", {
2274
2295
  since: input.since,
@@ -2279,14 +2300,14 @@ async function listDeletedProjects(input) {
2279
2300
  }
2280
2301
 
2281
2302
  // 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({
2303
+ import { z as z18 } from "zod";
2304
+ var RelationshipEntity = z18.enum(["opportunities", "kases"]).describe("Which entity has the additional-party links. Use 'kases' for projects.");
2305
+ var listAdditionalPartiesSchema = z18.object({
2285
2306
  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)
2307
+ entityId: positiveId.describe("ID of the opportunity or project."),
2308
+ embed: z18.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
2309
+ page: z18.number().int().positive().optional().default(1),
2310
+ perPage: z18.number().int().min(1).max(100).optional().default(25)
2290
2311
  });
2291
2312
  async function listAdditionalParties(input) {
2292
2313
  const { data, nextPage } = await capsuleGet(
@@ -2295,10 +2316,12 @@ async function listAdditionalParties(input) {
2295
2316
  );
2296
2317
  return { ...data, nextPage };
2297
2318
  }
2298
- var addAdditionalPartySchema = z15.object({
2319
+ var addAdditionalPartySchema = z18.object({
2299
2320
  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.")
2321
+ entityId: positiveId,
2322
+ partyId: positiveId.describe(
2323
+ "ID of the party (person or organisation) to link as an additional party."
2324
+ )
2302
2325
  });
2303
2326
  async function addAdditionalParty(input) {
2304
2327
  try {
@@ -2326,10 +2349,10 @@ async function addAdditionalParty(input) {
2326
2349
  throw err;
2327
2350
  }
2328
2351
  }
2329
- var removeAdditionalPartySchema = z15.object({
2352
+ var removeAdditionalPartySchema = z18.object({
2330
2353
  entity: RelationshipEntity,
2331
- entityId: z15.number().int().positive(),
2332
- partyId: z15.number().int().positive(),
2354
+ entityId: positiveId,
2355
+ partyId: positiveId,
2333
2356
  confirm: confirmFlag().describe(
2334
2357
  "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
2358
  )
@@ -2356,11 +2379,11 @@ async function removeAdditionalParty(input) {
2356
2379
  })
2357
2380
  );
2358
2381
  }
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)
2382
+ var listAssociatedProjectsSchema = z18.object({
2383
+ opportunityId: positiveId,
2384
+ embed: z18.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
2385
+ page: z18.number().int().positive().optional().default(1),
2386
+ perPage: z18.number().int().min(1).max(100).optional().default(25)
2364
2387
  });
2365
2388
  async function listAssociatedProjects(input) {
2366
2389
  const { data, nextPage } = await capsuleGet(
@@ -2371,9 +2394,9 @@ async function listAssociatedProjects(input) {
2371
2394
  }
2372
2395
 
2373
2396
  // 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({
2397
+ import { z as z19 } from "zod";
2398
+ var CustomFieldEntity = z19.enum(["parties", "opportunities", "kases"]).describe("Which entity type's custom field schema to inspect. Use 'kases' for projects.");
2399
+ var listCustomFieldsSchema = z19.object({
2377
2400
  entity: CustomFieldEntity
2378
2401
  });
2379
2402
  async function listCustomFields(input) {
@@ -2382,9 +2405,9 @@ async function listCustomFields(input) {
2382
2405
  );
2383
2406
  return data;
2384
2407
  }
2385
- var getCustomFieldSchema = z16.object({
2408
+ var getCustomFieldSchema = z19.object({
2386
2409
  entity: CustomFieldEntity,
2387
- fieldId: z16.number().int().positive().describe("Custom field definition id.")
2410
+ fieldId: positiveId.describe("Custom field definition id.")
2388
2411
  });
2389
2412
  async function getCustomField(input) {
2390
2413
  const { data } = await capsuleGetCached(
@@ -2394,11 +2417,11 @@ async function getCustomField(input) {
2394
2417
  }
2395
2418
 
2396
2419
  // 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({
2420
+ import { z as z20 } from "zod";
2421
+ var TrackEntity = z20.enum(["parties", "opportunities", "kases"]).describe("Use 'kases' for projects.");
2422
+ var listEntityTracksSchema = z20.object({
2400
2423
  entity: TrackEntity,
2401
- entityId: z17.number().int().positive()
2424
+ entityId: positiveId
2402
2425
  });
2403
2426
  async function listEntityTracks(input) {
2404
2427
  const { data } = await capsuleGet(
@@ -2406,20 +2429,20 @@ async function listEntityTracks(input) {
2406
2429
  );
2407
2430
  return data;
2408
2431
  }
2409
- var showTrackSchema = z17.object({
2410
- trackId: z17.number().int().positive()
2432
+ var showTrackSchema = z20.object({
2433
+ trackId: positiveId
2411
2434
  });
2412
2435
  async function showTrack(input) {
2413
2436
  const { data } = await capsuleGet(`/tracks/${input.trackId}`);
2414
2437
  return data;
2415
2438
  }
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(
2439
+ var applyTrackSchema = z20.object({
2440
+ entity: z20.enum(["opportunities", "kases"]).describe("Which entity to apply the track to. Use 'kases' for projects."),
2441
+ entityId: positiveId,
2442
+ trackDefinitionId: positiveId.describe(
2420
2443
  "The trackDefinition to apply (from list_track_definitions). Auto-creates task definitions on the target entity per the track's rules."
2421
2444
  ),
2422
- startDate: z17.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe(
2445
+ startDate: z20.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe(
2423
2446
  "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
2447
  )
2425
2448
  });
@@ -2432,9 +2455,9 @@ async function applyTrack(input) {
2432
2455
  if (input.startDate !== void 0) track["trackDateOn"] = input.startDate;
2433
2456
  return capsulePost("/tracks", { track });
2434
2457
  }
2435
- var updateTrackSchema = z17.object({
2436
- trackId: z17.number().int().positive(),
2437
- fields: z17.record(z17.string(), z17.unknown()).describe(
2458
+ var updateTrackSchema = z20.object({
2459
+ trackId: positiveId,
2460
+ fields: z20.record(z20.string(), z20.unknown()).describe(
2438
2461
  "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
2462
  )
2440
2463
  });
@@ -2446,8 +2469,8 @@ async function updateTrack(input) {
2446
2469
  track: input.fields
2447
2470
  });
2448
2471
  }
2449
- var removeTrackSchema = z17.object({
2450
- trackId: z17.number().int().positive(),
2472
+ var removeTrackSchema = z20.object({
2473
+ trackId: positiveId,
2451
2474
  confirm: confirmFlag().describe(
2452
2475
  "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
2476
  )
@@ -2464,13 +2487,13 @@ async function removeTrack(input) {
2464
2487
  }
2465
2488
 
2466
2489
  // src/tools/attachments.ts
2467
- import { z as z18 } from "zod";
2490
+ import { z as z21 } from "zod";
2468
2491
  var DEFAULT_MAX_SIZE_BYTES = 5 * 1024 * 1024;
2469
2492
  var HARD_MAX_SIZE_BYTES = 25 * 1024 * 1024;
2470
2493
  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(
2494
+ var getAttachmentSchema = z21.object({
2495
+ id: positiveId.describe("Attachment ID."),
2496
+ maxSizeBytes: z21.number().int().positive().max(HARD_MAX_SIZE_BYTES).optional().describe(
2474
2497
  `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
2498
  )
2476
2499
  });
@@ -2485,22 +2508,22 @@ async function getAttachment(input) {
2485
2508
  }
2486
2509
  return { contentType, buffer, sizeBytes };
2487
2510
  }
2488
- var uploadAttachmentSchema = z18.object({
2489
- filename: z18.string().min(1).describe(
2511
+ var uploadAttachmentSchema = z21.object({
2512
+ filename: z21.string().min(1).describe(
2490
2513
  "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
2514
  ),
2492
- contentType: z18.string().min(1).describe(
2515
+ contentType: z21.string().min(1).describe(
2493
2516
  "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
2517
  ),
2495
- dataBase64: z18.string().min(1).max(HARD_MAX_BASE64_CHARS).describe(
2518
+ dataBase64: z21.string().min(1).max(HARD_MAX_BASE64_CHARS).describe(
2496
2519
  "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
2520
  ),
2498
- content: z18.string().optional().describe(
2521
+ content: z21.string().optional().describe(
2499
2522
  "Body text for the note that will hold the attachment. Defaults to '[attachment]' if omitted."
2500
2523
  ),
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()
2524
+ partyId: positiveId.optional().describe("Link the new note to a party (mutually exclusive with opportunityId / projectId)."),
2525
+ opportunityId: positiveId.optional(),
2526
+ projectId: positiveId.optional()
2504
2527
  });
2505
2528
  function isValidBase64(s) {
2506
2529
  if (!/^[A-Za-z0-9+/]*={0,2}$/.test(s)) return false;
@@ -2550,23 +2573,23 @@ async function uploadAttachment(input) {
2550
2573
  }
2551
2574
 
2552
2575
  // src/tools/saved-filters.ts
2553
- import { z as z19 } from "zod";
2554
- var EntitySchema = z19.enum(["parties", "opportunities", "kases"]).describe(
2576
+ import { z as z22 } from "zod";
2577
+ var EntitySchema = z22.enum(["parties", "opportunities", "kases"]).describe(
2555
2578
  "Which entity type the filter operates over. Use 'kases' for projects (Capsule's legacy name)."
2556
2579
  );
2557
- var listSavedFiltersSchema = z19.object({
2580
+ var listSavedFiltersSchema = z22.object({
2558
2581
  entity: EntitySchema
2559
2582
  });
2560
2583
  async function listSavedFilters(input) {
2561
2584
  const { data } = await capsuleGetCached(`/${input.entity}/filters`);
2562
2585
  return data;
2563
2586
  }
2564
- var runSavedFilterSchema = z19.object({
2587
+ var runSavedFilterSchema = z22.object({
2565
2588
  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)
2589
+ id: positiveId.describe("The saved filter id (from list_saved_filters)."),
2590
+ embed: z22.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
2591
+ page: z22.number().int().positive().optional().default(1),
2592
+ perPage: z22.number().int().min(1).max(100).optional().default(25)
2570
2593
  });
2571
2594
  async function runSavedFilter(input) {
2572
2595
  const { data, nextPage } = await capsuleGet(
@@ -2584,7 +2607,7 @@ function createCapsuleMcpServer(opts) {
2584
2607
  const server2 = new McpServer(
2585
2608
  {
2586
2609
  name: "capsulemcp",
2587
- version: "1.6.1",
2610
+ version: "1.6.3",
2588
2611
  description: "Read and (optionally) modify Capsule CRM data \u2014 parties, opportunities, projects, tasks, timeline entries, pipelines, tags.",
2589
2612
  websiteUrl: "https://github.com/soil-dev/capsulemcp",
2590
2613
  icons: ICONS
@@ -2687,7 +2710,7 @@ function createCapsuleMcpServer(opts) {
2687
2710
  registerTool(
2688
2711
  server2,
2689
2712
  "update_party",
2690
- "Update top-level fields on an existing party (about, firstName/lastName/name/title/jobTitle, ownerId). Only the fields you provide are changed. Child arrays (emailAddresses / phoneNumbers / addresses / websites) on this tool are APPEND-ONLY: items are merged into the existing list, not replaced. For surgical changes \u2014 replacing one email, removing one phone number, fixing the type on one address \u2014 use the dedicated atomic tools: add_party_email_address / remove_party_email_address_by_id (and the phone/address/website equivalents).",
2713
+ "Update top-level fields on an existing party (about, firstName/lastName/name/title/jobTitle, ownerId, organisationId). For PERSON parties, organisationId links to an organisation or null unlinks the person from its organisation; for ORGANISATION parties Capsule silently ignores organisationId. Only the fields you provide are changed. Child arrays (emailAddresses / phoneNumbers / addresses / websites) on this tool are APPEND-ONLY: items are merged into the existing list, not replaced. For surgical changes \u2014 replacing one email, removing one phone number, fixing the type on one address \u2014 use the dedicated atomic tools: add_party_email_address / remove_party_email_address_by_id (and the phone/address/website equivalents).",
2691
2714
  updatePartySchema,
2692
2715
  updateParty
2693
2716
  );
@@ -2822,7 +2845,7 @@ function createCapsuleMcpServer(opts) {
2822
2845
  registerTool(
2823
2846
  server2,
2824
2847
  "update_opportunity",
2825
- "Update fields on an existing opportunity. Only the fields you provide are changed. Closed (Won/Lost) opportunities ARE editable \u2014 Capsule does not enforce closed-record immutability, so `value`, `description`, etc. can be changed on a Won opp without warning. If the workflow needs historical revenue numbers to be stable, enforce that caller-side.",
2848
+ "Update fields on an existing opportunity, including the parent-reference field `partyId` to reassign the opp to a different primary party. Only the fields you provide are changed. Closed (Won/Lost) opportunities ARE editable \u2014 Capsule does not enforce closed-record immutability, so `value`, `description`, etc. can be changed on a Won opp without warning. If the workflow needs historical revenue numbers to be stable, enforce that caller-side. Capsule requires every opportunity to have a party \u2014 passing `partyId: null` is rejected with 422 'party is required'.",
2826
2849
  updateOpportunitySchema,
2827
2850
  updateOpportunity
2828
2851
  );
@@ -2887,7 +2910,7 @@ function createCapsuleMcpServer(opts) {
2887
2910
  registerTool(
2888
2911
  server2,
2889
2912
  "update_project",
2890
- "Update fields on an existing project. Only the fields you provide are changed. Use status='CLOSED' to close a project. CLOSED projects remain fully editable \u2014 Capsule does not enforce closed-record immutability. Stage moves and description edits on a CLOSED project are accepted without warning.",
2913
+ "Update fields on an existing project, including the parent-reference field `partyId` to reassign the project to a different primary party. Only the fields you provide are changed. Use status='CLOSED' to close a project. CLOSED projects remain fully editable \u2014 Capsule does not enforce closed-record immutability. Stage moves and description edits on a CLOSED project are accepted without warning. Capsule requires every project to have a party \u2014 passing `partyId: null` is rejected with 422 'party is required'.",
2891
2914
  updateProjectSchema,
2892
2915
  updateProject
2893
2916
  );
@@ -2966,7 +2989,7 @@ function createCapsuleMcpServer(opts) {
2966
2989
  registerTool(
2967
2990
  server2,
2968
2991
  "update_task",
2969
- "Update fields on an existing task: `description`, `dueOn`, `dueTime`, `detail`, `status` (OPEN or COMPLETED), and `ownerId`. Only the fields you provide are changed. To mark a task done, prefer the dedicated `complete_task` tool \u2014 it's idempotent (a no-op success on an already-completed task) and semantically clearer than `update_task status=COMPLETED`. Capsule rejects directly setting status=PENDING (which exists only internally for track-driven tasks); use OPEN or COMPLETED. Completed tasks remain fully editable \u2014 Capsule does not enforce closed-record immutability.",
2992
+ "Update fields on an existing task: `description`, `dueOn`, `dueTime`, `detail`, `status` (OPEN or COMPLETED), `ownerId`, and the parent-reference fields `partyId`, `opportunityId`, `projectId`. Pass a parent id to re-link the task, or null on a parent field to orphan/unlink it; at most one parent id may be set in a single call, though null+id swaps are allowed. Only the fields you provide are changed. To mark a task done, prefer the dedicated `complete_task` tool \u2014 it's idempotent (a no-op success on an already-completed task) and semantically clearer than `update_task status=COMPLETED`. Capsule rejects directly setting status=PENDING (which exists only internally for track-driven tasks); use OPEN or COMPLETED. Completed tasks remain fully editable \u2014 Capsule does not enforce closed-record immutability.",
2970
2993
  updateTaskSchema,
2971
2994
  updateTask
2972
2995
  );
@@ -3169,7 +3192,7 @@ function createCapsuleMcpServer(opts) {
3169
3192
  registerTool(
3170
3193
  server2,
3171
3194
  "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.",
3195
+ "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
3196
  listTeamsSchema,
3174
3197
  listTeams
3175
3198
  );