capsulemcp 1.6.0 → 1.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -286,11 +286,22 @@ async function doFetch(url, options) {
286
286
  "Rate limit exceeded after one retry. Please slow down your requests."
287
287
  );
288
288
  }
289
- emitCapsuleRequest(method, url, retried.res, Date.now() - startedAt, true);
290
- return retried;
289
+ return { ...retried, startedAt, method, url, retriedAfter429: true };
290
+ }
291
+ return { ...first, startedAt, method, url, retriedAfter429: false };
292
+ }
293
+ async function consumeBody(start, body) {
294
+ try {
295
+ return await body();
296
+ } finally {
297
+ emitCapsuleRequest(
298
+ start.method,
299
+ start.url,
300
+ start.res,
301
+ Date.now() - start.startedAt,
302
+ start.retriedAfter429
303
+ );
291
304
  }
292
- emitCapsuleRequest(method, url, first.res, Date.now() - startedAt, false);
293
- return first;
294
305
  }
295
306
  function emitCapsuleRequest(method, url, res, durationMs, retriedAfter429) {
296
307
  let path = "";
@@ -340,13 +351,15 @@ function buildUrl(path, params) {
340
351
  async function capsuleGet(path, params) {
341
352
  const token = getToken();
342
353
  const url = buildUrl(path, params);
343
- const { res, cleanup } = await doFetch(url, { headers: baseHeaders(token) });
354
+ const start = await doFetch(url, { headers: baseHeaders(token) });
344
355
  try {
345
- const data = await handleResponse(res);
346
- const nextPage = parseNextPage(res.headers.get("Link"));
347
- return { data, nextPage };
356
+ return await consumeBody(start, async () => {
357
+ const data = await handleResponse(start.res);
358
+ const nextPage = parseNextPage(start.res.headers.get("Link"));
359
+ return { data, nextPage };
360
+ });
348
361
  } finally {
349
- cleanup();
362
+ start.cleanup();
350
363
  }
351
364
  }
352
365
  async function capsuleGetCached(path, params) {
@@ -381,123 +394,130 @@ async function capsulePost(path, body) {
381
394
  if (isReadOnly()) throw new CapsuleReadOnlyError("POST");
382
395
  const token = getToken();
383
396
  const url = buildUrl(path);
384
- const { res, cleanup } = await doFetch(url, {
397
+ const start = await doFetch(url, {
385
398
  method: "POST",
386
399
  headers: { ...baseHeaders(token), "Content-Type": "application/json" },
387
400
  body: JSON.stringify(body)
388
401
  });
389
402
  try {
390
- return await handleResponse(res);
403
+ return await consumeBody(start, () => handleResponse(start.res));
391
404
  } finally {
392
- cleanup();
405
+ start.cleanup();
393
406
  }
394
407
  }
395
408
  async function capsulePostNoContent(path) {
396
409
  if (isReadOnly()) throw new CapsuleReadOnlyError("POST");
397
410
  const token = getToken();
398
411
  const url = buildUrl(path);
399
- const { res, cleanup } = await doFetch(url, {
412
+ const start = await doFetch(url, {
400
413
  method: "POST",
401
414
  headers: baseHeaders(token)
402
415
  });
403
416
  try {
404
- if (res.status === 204) return;
405
- await throwForStatus(res);
406
- await mapAbort(res.text());
417
+ await consumeBody(start, async () => {
418
+ if (start.res.status === 204) return;
419
+ await throwForStatus(start.res);
420
+ await mapAbort(start.res.text());
421
+ });
407
422
  } finally {
408
- cleanup();
423
+ start.cleanup();
409
424
  }
410
425
  }
411
426
  async function capsuleSearch(path, body, params) {
412
427
  const token = getToken();
413
428
  const url = buildUrl(path, params);
414
- const { res, cleanup } = await doFetch(url, {
429
+ const start = await doFetch(url, {
415
430
  method: "POST",
416
431
  headers: { ...baseHeaders(token), "Content-Type": "application/json" },
417
432
  body: JSON.stringify(body)
418
433
  });
419
434
  try {
420
- const data = await handleResponse(res);
421
- const nextPage = parseNextPage(res.headers.get("Link"));
422
- return { data, nextPage };
435
+ return await consumeBody(start, async () => {
436
+ const data = await handleResponse(start.res);
437
+ const nextPage = parseNextPage(start.res.headers.get("Link"));
438
+ return { data, nextPage };
439
+ });
423
440
  } finally {
424
- cleanup();
441
+ start.cleanup();
425
442
  }
426
443
  }
427
444
  async function capsulePut(path, body) {
428
445
  if (isReadOnly()) throw new CapsuleReadOnlyError("PUT");
429
446
  const token = getToken();
430
447
  const url = buildUrl(path);
431
- const { res, cleanup } = await doFetch(url, {
448
+ const start = await doFetch(url, {
432
449
  method: "PUT",
433
450
  headers: { ...baseHeaders(token), "Content-Type": "application/json" },
434
451
  body: JSON.stringify(body)
435
452
  });
436
453
  try {
437
- return await handleResponse(res);
454
+ return await consumeBody(start, () => handleResponse(start.res));
438
455
  } finally {
439
- cleanup();
456
+ start.cleanup();
440
457
  }
441
458
  }
442
459
  async function capsuleGetBinary(path, maxBytes) {
443
460
  const token = getToken();
444
461
  const url = buildUrl(path);
445
- const { res, cleanup } = await doFetch(url, { headers: baseHeaders(token) });
462
+ const start = await doFetch(url, { headers: baseHeaders(token) });
446
463
  try {
447
- await throwForStatus(res);
448
- const contentType = res.headers.get("Content-Type") ?? "application/octet-stream";
449
- const declared = res.headers.get("Content-Length");
450
- const declaredBytes = declared ? Number(declared) : NaN;
451
- if (maxBytes !== void 0 && Number.isFinite(declaredBytes) && declaredBytes > maxBytes) {
452
- if (res.body) await res.body.cancel().catch(() => {
453
- });
454
- return {
455
- contentType,
456
- buffer: Buffer.alloc(0),
457
- truncated: true,
458
- sizeBytes: declaredBytes
459
- };
460
- }
461
- if (maxBytes !== void 0 && res.body) {
462
- const reader = res.body.getReader();
463
- const chunks = [];
464
- let total = 0;
465
- let truncated = false;
466
- while (true) {
467
- const { done, value } = await mapAbort(reader.read());
468
- if (done) break;
469
- total += value.byteLength;
470
- if (total > maxBytes) {
471
- truncated = true;
472
- await reader.cancel().catch(() => {
473
- });
474
- break;
475
- }
476
- chunks.push(value);
477
- }
478
- if (truncated) {
464
+ return await consumeBody(start, async () => {
465
+ const res = start.res;
466
+ await throwForStatus(res);
467
+ const contentType = res.headers.get("Content-Type") ?? "application/octet-stream";
468
+ const declared = res.headers.get("Content-Length");
469
+ const declaredBytes = declared ? Number(declared) : NaN;
470
+ if (maxBytes !== void 0 && Number.isFinite(declaredBytes) && declaredBytes > maxBytes) {
471
+ if (res.body) await res.body.cancel().catch(() => {
472
+ });
479
473
  return {
480
474
  contentType,
481
475
  buffer: Buffer.alloc(0),
482
476
  truncated: true,
483
- sizeBytes: total
477
+ sizeBytes: declaredBytes
484
478
  };
485
479
  }
486
- const buffer2 = Buffer.concat(chunks.map((c) => Buffer.from(c)));
487
- return { contentType, buffer: buffer2, sizeBytes: buffer2.length };
488
- }
489
- const arrayBuffer = await mapAbort(res.arrayBuffer());
490
- const buffer = Buffer.from(arrayBuffer);
491
- return { contentType, buffer, sizeBytes: buffer.length };
480
+ if (maxBytes !== void 0 && res.body) {
481
+ const reader = res.body.getReader();
482
+ const chunks = [];
483
+ let total = 0;
484
+ let truncated = false;
485
+ while (true) {
486
+ const { done, value } = await mapAbort(reader.read());
487
+ if (done) break;
488
+ total += value.byteLength;
489
+ if (total > maxBytes) {
490
+ truncated = true;
491
+ await reader.cancel().catch(() => {
492
+ });
493
+ break;
494
+ }
495
+ chunks.push(value);
496
+ }
497
+ if (truncated) {
498
+ return {
499
+ contentType,
500
+ buffer: Buffer.alloc(0),
501
+ truncated: true,
502
+ sizeBytes: total
503
+ };
504
+ }
505
+ const buffer2 = Buffer.concat(chunks.map((c) => Buffer.from(c)));
506
+ return { contentType, buffer: buffer2, sizeBytes: buffer2.length };
507
+ }
508
+ const arrayBuffer = await mapAbort(res.arrayBuffer());
509
+ const buffer = Buffer.from(arrayBuffer);
510
+ return { contentType, buffer, sizeBytes: buffer.length };
511
+ });
492
512
  } finally {
493
- cleanup();
513
+ start.cleanup();
494
514
  }
495
515
  }
496
516
  async function capsulePostBinary(path, body, contentType, filename) {
497
517
  if (isReadOnly()) throw new CapsuleReadOnlyError("POST");
498
518
  const token = getToken();
499
519
  const url = buildUrl(path);
500
- const { res, cleanup } = await doFetch(url, {
520
+ const start = await doFetch(url, {
501
521
  method: "POST",
502
522
  headers: {
503
523
  ...baseHeaders(token),
@@ -508,25 +528,27 @@ async function capsulePostBinary(path, body, contentType, filename) {
508
528
  body
509
529
  });
510
530
  try {
511
- return await handleResponse(res);
531
+ return await consumeBody(start, () => handleResponse(start.res));
512
532
  } finally {
513
- cleanup();
533
+ start.cleanup();
514
534
  }
515
535
  }
516
536
  async function capsuleDelete(path) {
517
537
  if (isReadOnly()) throw new CapsuleReadOnlyError("DELETE");
518
538
  const token = getToken();
519
539
  const url = buildUrl(path);
520
- const { res, cleanup } = await doFetch(url, {
540
+ const start = await doFetch(url, {
521
541
  method: "DELETE",
522
542
  headers: baseHeaders(token)
523
543
  });
524
544
  try {
525
- if (res.status === 204) return;
526
- await throwForStatus(res);
527
- await mapAbort(res.text());
545
+ await consumeBody(start, async () => {
546
+ if (start.res.status === 204) return;
547
+ await throwForStatus(start.res);
548
+ await mapAbort(start.res.text());
549
+ });
528
550
  } finally {
529
- cleanup();
551
+ start.cleanup();
530
552
  }
531
553
  }
532
554
 
@@ -849,18 +871,19 @@ function registerToolTask(server2, name, description, schema, handler) {
849
871
  }
850
872
 
851
873
  // src/tools/parties.ts
852
- import { z as z3 } from "zod";
874
+ import { z as z6 } from "zod";
853
875
 
854
- // src/tools/descriptions.ts
855
- var EMBED_TAGS_FIELDS_DESCRIPTION = "Comma-separated embeds, e.g. 'tags,fields'";
856
- 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
+ }
857
884
 
858
- // src/tools/confirm-flag.ts
885
+ // src/tools/define-batch.ts
859
886
  import { z } from "zod";
860
- var CONFIRM_REQUIRED_MESSAGE = "confirm: true is required to perform this destructive operation (set the parameter explicitly to acknowledge the destructive intent)";
861
- function confirmFlag() {
862
- return z.literal(true, { error: () => CONFIRM_REQUIRED_MESSAGE });
863
- }
864
887
 
865
888
  // src/capsule/batch.ts
866
889
  function chunk(arr, size) {
@@ -954,6 +977,39 @@ function topFailureReasons(results, n) {
954
977
  return Array.from(counts.values()).sort((a, b) => b.count - a.count).slice(0, n);
955
978
  }
956
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
+
957
1013
  // src/capsule/idempotent.ts
958
1014
  var isCapsule404 = (err) => err instanceof CapsuleApiError && err.status === 404;
959
1015
  var isCapsuleTagNotFound = (err) => err instanceof CapsuleApiError && err.status === 422 && /tag not found/i.test(err.message);
@@ -976,13 +1032,33 @@ async function idempotentWithResult(op, success, alreadyDone, isAlreadyDoneError
976
1032
  }
977
1033
  }
978
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
+
979
1055
  // src/tools/custom-field-helpers.ts
980
- import { z as z2 } from "zod";
981
- var CustomFieldWriteSchema = z2.object({
982
- definitionId: z2.number().int().positive().describe(
1056
+ import { z as z5 } from "zod";
1057
+ var CustomFieldWriteSchema = z5.object({
1058
+ definitionId: positiveId.describe(
983
1059
  "The custom-field definition id from list_custom_fields. Identifies which field on the entity to set."
984
1060
  ),
985
- 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(
986
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."
987
1063
  )
988
1064
  });
@@ -998,24 +1074,24 @@ function mapFieldsForBody(fields) {
998
1074
  }
999
1075
 
1000
1076
  // src/tools/parties.ts
1001
- var EmailAddressSchema = z3.object({
1002
- address: z3.string().email(),
1003
- type: z3.string().optional()
1077
+ var EmailAddressSchema = z6.object({
1078
+ address: z6.string().email(),
1079
+ type: z6.string().optional()
1004
1080
  });
1005
- var PhoneNumberSchema = z3.object({
1081
+ var PhoneNumberSchema = z6.object({
1006
1082
  // Capsule rejects empty strings with `phoneNumber.number: number is
1007
1083
  // required`. Enforce at the schema layer to catch typos pre-call,
1008
1084
  // matching how EmailAddressSchema's address field behaves.
1009
- number: z3.string().min(1),
1010
- type: z3.string().optional()
1085
+ number: z6.string().min(1),
1086
+ type: z6.string().optional()
1011
1087
  });
1012
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.";
1013
- var AddressSchema = z3.object({
1014
- street: z3.string().optional(),
1015
- city: z3.string().optional(),
1016
- state: z3.string().optional(),
1017
- country: z3.string().optional().describe(CountryDescription),
1018
- 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()
1019
1095
  });
1020
1096
  function validateWebsiteAddress(data, ctx) {
1021
1097
  const isUrlService = data.service === void 0 || data.service === "URL";
@@ -1038,7 +1114,7 @@ function validateWebsiteAddress(data, ctx) {
1038
1114
  });
1039
1115
  }
1040
1116
  }
1041
- var WebsiteServiceEnum = z3.enum([
1117
+ var WebsiteServiceEnum = z6.enum([
1042
1118
  "URL",
1043
1119
  "SKYPE",
1044
1120
  "TWITTER",
@@ -1057,19 +1133,19 @@ var WebsiteServiceEnum = z3.enum([
1057
1133
  "BLUESKY",
1058
1134
  "SNAPCHAT"
1059
1135
  ]);
1060
- var WebsiteSchema = z3.object({
1061
- address: z3.string().min(1).describe(
1136
+ var WebsiteSchema = z6.object({
1137
+ address: z6.string().min(1).describe(
1062
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."
1063
1139
  ),
1064
1140
  service: WebsiteServiceEnum.optional().describe(
1065
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."
1066
1142
  )
1067
1143
  }).superRefine(validateWebsiteAddress);
1068
- var searchPartiesSchema = z3.object({
1069
- q: z3.string().optional().describe("Free-text search query"),
1070
- embed: z3.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
1071
- page: z3.number().int().positive().optional().default(1),
1072
- 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)
1073
1149
  });
1074
1150
  async function searchParties(input) {
1075
1151
  const path = input.q ? "/parties/search" : "/parties";
@@ -1081,9 +1157,9 @@ async function searchParties(input) {
1081
1157
  });
1082
1158
  return { ...data, nextPage };
1083
1159
  }
1084
- var getPartySchema = z3.object({
1085
- id: z3.number().int().positive().describe("Party ID"),
1086
- 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)
1087
1163
  });
1088
1164
  async function getParty(input) {
1089
1165
  const { data } = await capsuleGet(`/parties/${input.id}`, {
@@ -1091,11 +1167,11 @@ async function getParty(input) {
1091
1167
  });
1092
1168
  return data;
1093
1169
  }
1094
- var getPartiesSchema = z3.object({
1095
- 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(
1096
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."
1097
1173
  ),
1098
- embed: z3.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
1174
+ embed: z6.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
1099
1175
  });
1100
1176
  async function getParties(input) {
1101
1177
  const { ids, embed } = input;
@@ -1113,10 +1189,10 @@ async function getParties(input) {
1113
1189
  );
1114
1190
  return { parties: responses.flatMap((r) => r.data.parties) };
1115
1191
  }
1116
- var listPartyOpportunitiesSchema = z3.object({
1117
- partyId: z3.number().int().positive(),
1118
- page: z3.number().int().positive().optional().default(1),
1119
- 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)
1120
1196
  });
1121
1197
  async function listPartyOpportunities(input) {
1122
1198
  const { data, nextPage } = await capsuleGet(
@@ -1125,10 +1201,10 @@ async function listPartyOpportunities(input) {
1125
1201
  );
1126
1202
  return { ...data, nextPage };
1127
1203
  }
1128
- var listPartyProjectsSchema = z3.object({
1129
- partyId: z3.number().int().positive(),
1130
- page: z3.number().int().positive().optional().default(1),
1131
- 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)
1132
1208
  });
1133
1209
  async function listPartyProjects(input) {
1134
1210
  const { data, nextPage } = await capsuleGet(
@@ -1138,50 +1214,50 @@ async function listPartyProjects(input) {
1138
1214
  return { ...data, nextPage };
1139
1215
  }
1140
1216
  var PartyWriteBaseSchema = {
1141
- about: z3.string().optional(),
1142
- emailAddresses: z3.array(EmailAddressSchema).optional().describe(
1217
+ about: z6.string().optional(),
1218
+ emailAddresses: z6.array(EmailAddressSchema).optional().describe(
1143
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)."
1144
1220
  ),
1145
- phoneNumbers: z3.array(PhoneNumberSchema).optional().describe(
1221
+ phoneNumbers: z6.array(PhoneNumberSchema).optional().describe(
1146
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."
1147
1223
  ),
1148
- addresses: z3.array(AddressSchema).optional().describe(
1224
+ addresses: z6.array(AddressSchema).optional().describe(
1149
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)."
1150
1226
  ),
1151
- websites: z3.array(WebsiteSchema).optional().describe(
1227
+ websites: z6.array(WebsiteSchema).optional().describe(
1152
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."
1153
1229
  ),
1154
- ownerId: z3.number().int().positive().optional().describe(
1230
+ ownerId: positiveId.optional().describe(
1155
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."
1156
1232
  )
1157
1233
  };
1158
- var createPartySchema = z3.object({
1159
- type: z3.enum(["person", "organisation"]),
1234
+ var createPartySchema = z6.object({
1235
+ type: z6.enum(["person", "organisation"]),
1160
1236
  // person
1161
- firstName: z3.string().optional(),
1162
- lastName: z3.string().optional(),
1163
- title: z3.string().optional(),
1164
- jobTitle: z3.string().optional(),
1165
- 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"),
1166
1242
  // organisation
1167
- name: z3.string().optional(),
1243
+ name: z6.string().optional(),
1168
1244
  ...PartyWriteBaseSchema
1169
1245
  });
1170
1246
  async function createParty(input) {
1171
1247
  const { ownerId, organisationId, ...rest } = input;
1172
1248
  const body = { ...rest };
1173
- if (ownerId) body["owner"] = { id: ownerId };
1174
- if (organisationId) body["organisation"] = { id: organisationId };
1249
+ setRef(body, "owner", ownerId);
1250
+ setRef(body, "organisation", organisationId);
1175
1251
  return capsulePost("/parties", { party: body });
1176
1252
  }
1177
- var updatePartySchema = z3.object({
1178
- id: z3.number().int().positive(),
1179
- firstName: z3.string().optional(),
1180
- lastName: z3.string().optional(),
1181
- title: z3.string().optional(),
1182
- jobTitle: z3.string().optional(),
1183
- name: z3.string().optional(),
1184
- fields: z3.array(CustomFieldWriteSchema).optional().describe(fieldsArrayDescriptor("get_party")),
1253
+ var updatePartySchema = z6.object({
1254
+ id: positiveId,
1255
+ firstName: z6.string().optional(),
1256
+ lastName: z6.string().optional(),
1257
+ title: z6.string().optional(),
1258
+ jobTitle: z6.string().optional(),
1259
+ name: z6.string().optional(),
1260
+ fields: z6.array(CustomFieldWriteSchema).optional().describe(fieldsArrayDescriptor("get_party")),
1185
1261
  ...PartyWriteBaseSchema
1186
1262
  });
1187
1263
  async function updateParty(input) {
@@ -1190,39 +1266,26 @@ async function updateParty(input) {
1190
1266
  for (const [k, v] of Object.entries(rest)) {
1191
1267
  if (v !== void 0) body[k] = v;
1192
1268
  }
1193
- if (ownerId) body["owner"] = { id: ownerId };
1269
+ setRef(body, "owner", ownerId);
1194
1270
  const mappedFields = mapFieldsForBody(fields);
1195
1271
  if (mappedFields !== void 0) body["fields"] = mappedFields;
1196
1272
  return capsulePut(`/parties/${id}`, { party: body });
1197
1273
  }
1198
- var batchUpdatePartySchema = z3.object({
1199
- items: z3.array(updatePartySchema).min(1).max(50).describe(
1200
- "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)."
1201
- )
1274
+ var { schema: batchUpdatePartySchema, handler: batchUpdateParty } = defineBatch({
1275
+ toolName: "batch_update_party",
1276
+ itemSchema: updatePartySchema,
1277
+ itemDescription: "Array of 1\u201350 update_party inputs. Each item is the same shape as a single update_party call \u2014 id is required, every other field is optional. Capped at 50 so a single tool call can't burn an outsized share of Capsule's hourly per-token rate budget (~4000 req/h).",
1278
+ itemHandler: updateParty
1202
1279
  });
1203
- async function batchUpdateParty(input, opts = {}) {
1204
- return batchExecute("batch_update_party", input.items, (item) => updateParty(item), opts);
1205
- }
1206
- var deletePartySchema = z3.object({
1207
- id: z3.number().int().positive(),
1208
- confirm: confirmFlag().describe(
1209
- "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."
1210
- )
1280
+ var { schema: deletePartySchema, handler: deleteParty } = defineDelete({
1281
+ toolName: "delete_party",
1282
+ pathPrefix: "/parties",
1283
+ confirmHint: "Must be set to true. Deletes the party AND all linked notes, tasks, opportunities, and projects (kases). Deleting an ORGANISATION does NOT delete people linked to it via organisationId \u2014 their `organisation` field is silently cleared to null and they survive as standalone records. Irreversible."
1211
1284
  });
1212
- async function deleteParty(input) {
1213
- if (input.confirm !== true) {
1214
- throw new Error("delete_party requires confirm: true");
1215
- }
1216
- return idempotent(
1217
- () => capsuleDelete(`/parties/${input.id}`),
1218
- () => ({ deleted: true, alreadyDeleted: false, id: input.id }),
1219
- () => ({ deleted: true, alreadyDeleted: true, id: input.id })
1220
- );
1221
- }
1222
- var addPartyEmailAddressSchema = z3.object({
1223
- partyId: z3.number().int().positive(),
1224
- address: z3.string().email(),
1225
- type: z3.string().optional().describe("Free-form label, e.g. 'Work', 'Home'.")
1285
+ var addPartyEmailAddressSchema = z6.object({
1286
+ partyId: positiveId,
1287
+ address: z6.string().email(),
1288
+ type: z6.string().optional().describe("Free-form label, e.g. 'Work', 'Home'.")
1226
1289
  });
1227
1290
  async function addPartyEmailAddress(input) {
1228
1291
  const { partyId, address, type } = input;
@@ -1232,9 +1295,9 @@ async function addPartyEmailAddress(input) {
1232
1295
  party: { emailAddresses: [item] }
1233
1296
  });
1234
1297
  }
1235
- var removePartyEmailAddressByIdSchema = z3.object({
1236
- partyId: z3.number().int().positive(),
1237
- emailAddressId: z3.number().int().positive().describe(
1298
+ var removePartyEmailAddressByIdSchema = z6.object({
1299
+ partyId: positiveId,
1300
+ emailAddressId: positiveId.describe(
1238
1301
  "Capsule's id for the email-address row. Read it from get_party (each entry in emailAddresses carries an id)."
1239
1302
  )
1240
1303
  });
@@ -1254,10 +1317,10 @@ async function removePartyEmailAddressById(input) {
1254
1317
  () => ({ removed: true, alreadyRemoved: true, partyId, emailAddressId })
1255
1318
  );
1256
1319
  }
1257
- var addPartyPhoneNumberSchema = z3.object({
1258
- partyId: z3.number().int().positive(),
1259
- number: z3.string().min(1),
1260
- type: z3.string().optional().describe("Free-form label, e.g. 'Work', 'Mobile'.")
1320
+ var addPartyPhoneNumberSchema = z6.object({
1321
+ partyId: positiveId,
1322
+ number: z6.string().min(1),
1323
+ type: z6.string().optional().describe("Free-form label, e.g. 'Work', 'Mobile'.")
1261
1324
  });
1262
1325
  async function addPartyPhoneNumber(input) {
1263
1326
  const { partyId, number, type } = input;
@@ -1267,9 +1330,9 @@ async function addPartyPhoneNumber(input) {
1267
1330
  party: { phoneNumbers: [item] }
1268
1331
  });
1269
1332
  }
1270
- var removePartyPhoneNumberByIdSchema = z3.object({
1271
- partyId: z3.number().int().positive(),
1272
- phoneNumberId: z3.number().int().positive().describe(
1333
+ var removePartyPhoneNumberByIdSchema = z6.object({
1334
+ partyId: positiveId,
1335
+ phoneNumberId: positiveId.describe(
1273
1336
  "Capsule's id for the phone-number row. Read it from get_party (each entry in phoneNumbers carries an id)."
1274
1337
  )
1275
1338
  });
@@ -1289,14 +1352,14 @@ async function removePartyPhoneNumberById(input) {
1289
1352
  () => ({ removed: true, alreadyRemoved: true, partyId, phoneNumberId })
1290
1353
  );
1291
1354
  }
1292
- var addPartyAddressSchema = z3.object({
1293
- partyId: z3.number().int().positive(),
1294
- street: z3.string().optional(),
1295
- city: z3.string().optional(),
1296
- state: z3.string().optional(),
1297
- country: z3.string().optional().describe(CountryDescription),
1298
- zip: z3.string().optional(),
1299
- type: z3.string().optional().describe("Free-form label, e.g. 'Office', 'Home'.")
1355
+ var addPartyAddressSchema = z6.object({
1356
+ partyId: positiveId,
1357
+ street: z6.string().optional(),
1358
+ city: z6.string().optional(),
1359
+ state: z6.string().optional(),
1360
+ country: z6.string().optional().describe(CountryDescription),
1361
+ zip: z6.string().optional(),
1362
+ type: z6.string().optional().describe("Free-form label, e.g. 'Office', 'Home'.")
1300
1363
  });
1301
1364
  async function addPartyAddress(input) {
1302
1365
  const { partyId, ...rest } = input;
@@ -1308,9 +1371,9 @@ async function addPartyAddress(input) {
1308
1371
  party: { addresses: [item] }
1309
1372
  });
1310
1373
  }
1311
- var removePartyAddressByIdSchema = z3.object({
1312
- partyId: z3.number().int().positive(),
1313
- addressId: z3.number().int().positive().describe(
1374
+ var removePartyAddressByIdSchema = z6.object({
1375
+ partyId: positiveId,
1376
+ addressId: positiveId.describe(
1314
1377
  "Capsule's id for the address row. Read it from get_party (each entry in addresses carries an id)."
1315
1378
  )
1316
1379
  });
@@ -1330,9 +1393,9 @@ async function removePartyAddressById(input) {
1330
1393
  () => ({ removed: true, alreadyRemoved: true, partyId, addressId })
1331
1394
  );
1332
1395
  }
1333
- var addPartyWebsiteSchema = z3.object({
1334
- partyId: z3.number().int().positive(),
1335
- address: z3.string().min(1).describe(
1396
+ var addPartyWebsiteSchema = z6.object({
1397
+ partyId: positiveId,
1398
+ address: z6.string().min(1).describe(
1336
1399
  "The website address. A URL when service='URL', or a handle (e.g. '@acmeco') for social services."
1337
1400
  ),
1338
1401
  service: WebsiteServiceEnum.optional().describe("Defaults to 'URL' if omitted.")
@@ -1345,9 +1408,9 @@ async function addPartyWebsite(input) {
1345
1408
  party: { websites: [item] }
1346
1409
  });
1347
1410
  }
1348
- var removePartyWebsiteByIdSchema = z3.object({
1349
- partyId: z3.number().int().positive(),
1350
- websiteId: z3.number().int().positive().describe(
1411
+ var removePartyWebsiteByIdSchema = z6.object({
1412
+ partyId: positiveId,
1413
+ websiteId: positiveId.describe(
1351
1414
  "Capsule's id for the website row. Read it from get_party (each entry in websites carries an id)."
1352
1415
  )
1353
1416
  });
@@ -1369,20 +1432,32 @@ async function removePartyWebsiteById(input) {
1369
1432
  }
1370
1433
 
1371
1434
  // src/tools/opportunities.ts
1372
- import { z as z4 } from "zod";
1373
- var OpportunityValueSchema = z4.object({
1374
- amount: z4.number().nonnegative(),
1375
- currency: z4.string({
1435
+ import { z as z7 } from "zod";
1436
+
1437
+ // src/tools/preserve-refs.ts
1438
+ async function readEntityRefs(path, responseKey) {
1439
+ const { data } = await capsuleGet(path);
1440
+ const entity = data[responseKey];
1441
+ return {
1442
+ teamId: entity?.team?.id ?? void 0,
1443
+ stageId: entity?.stage?.id ?? void 0
1444
+ };
1445
+ }
1446
+
1447
+ // src/tools/opportunities.ts
1448
+ var OpportunityValueSchema = z7.object({
1449
+ amount: z7.number().nonnegative(),
1450
+ currency: z7.string({
1376
1451
  error: (iss) => iss.code === "invalid_type" && iss.input === void 0 ? "currency is required when amount is set (3-letter ISO 4217 code, e.g. 'USD', 'EUR', 'GBP')" : void 0
1377
1452
  }).length(3).describe(
1378
1453
  "ISO 4217 currency code (3 letters), e.g. 'GBP', 'USD', 'EUR'. Required when amount is set."
1379
1454
  )
1380
1455
  });
1381
- var searchOpportunitiesSchema = z4.object({
1382
- q: z4.string().optional().describe("Free-text search query"),
1383
- embed: z4.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
1384
- page: z4.number().int().positive().optional().default(1),
1385
- perPage: z4.number().int().min(1).max(100).optional().default(25)
1456
+ var searchOpportunitiesSchema = z7.object({
1457
+ q: z7.string().optional().describe("Free-text search query"),
1458
+ embed: z7.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
1459
+ page: z7.number().int().positive().optional().default(1),
1460
+ perPage: z7.number().int().min(1).max(100).optional().default(25)
1386
1461
  });
1387
1462
  async function searchOpportunities(input) {
1388
1463
  const path = input.q ? "/opportunities/search" : "/opportunities";
@@ -1394,9 +1469,9 @@ async function searchOpportunities(input) {
1394
1469
  });
1395
1470
  return { ...data, nextPage };
1396
1471
  }
1397
- var getOpportunitySchema = z4.object({
1398
- id: z4.number().int().positive(),
1399
- embed: z4.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
1472
+ var getOpportunitySchema = z7.object({
1473
+ id: positiveId,
1474
+ embed: z7.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
1400
1475
  });
1401
1476
  async function getOpportunity(input) {
1402
1477
  const { data } = await capsuleGet(`/opportunities/${input.id}`, {
@@ -1404,11 +1479,11 @@ async function getOpportunity(input) {
1404
1479
  });
1405
1480
  return data;
1406
1481
  }
1407
- var getOpportunitiesSchema = z4.object({
1408
- ids: z4.array(z4.number().int().positive()).min(1).max(50).describe(
1482
+ var getOpportunitiesSchema = z7.object({
1483
+ ids: z7.array(positiveId).min(1).max(50).describe(
1409
1484
  "Array of opportunity IDs (1\u201350). Capsule's native batch-fetch endpoint caps at 10 per request; the connector transparently splits larger sets into 10-id chunks and fans out the Capsule calls in parallel."
1410
1485
  ),
1411
- embed: z4.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
1486
+ embed: z7.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
1412
1487
  });
1413
1488
  async function getOpportunities(input) {
1414
1489
  const { ids, embed } = input;
@@ -1429,102 +1504,96 @@ async function getOpportunities(input) {
1429
1504
  );
1430
1505
  return { opportunities: responses.flatMap((r) => r.data.opportunities) };
1431
1506
  }
1432
- var createOpportunitySchema = z4.object({
1433
- name: z4.string().min(1),
1434
- partyId: z4.number().int().positive().describe("ID of the party this opportunity belongs to"),
1435
- milestoneId: z4.number().int().positive().describe(
1436
- "ID of the pipeline milestone to place this opportunity at. The milestone implicitly determines the pipeline \u2014 there is no separate pipelineId parameter. Discover via list_pipelines / list_milestones."
1507
+ var createOpportunitySchema = z7.object({
1508
+ name: z7.string().min(1),
1509
+ partyId: positiveId.describe("ID of the party this opportunity belongs to"),
1510
+ milestoneId: positiveId.describe(
1511
+ "ID of the pipeline milestone to place this opportunity at. The milestone implicitly determines the pipeline \u2014 there is no separate pipelineId parameter. Discover via list_pipelines / list_milestones. NOTE: some Capsule tenants configure **pipeline / milestone-reached automation rules** that mutate `owner` and/or `team` immediately after creation \u2014 e.g. an 'Assign to a Team' action that fires on entry to a specific milestone and has been observed to clear `owner` as an automation side-effect. If you observe a newly-created opp landing with `owner: null` despite passing `ownerId`, the cause is almost certainly a milestone automation on the destination pipeline rather than the connector. Documented workaround: follow `create_opportunity` with an immediate `batch_update_opportunity({items: [{id, ownerId, teamId}]})` carrying both fields \u2014 PUT does not re-fire milestone-reached triggers, so the owner sticks."
1437
1512
  ),
1438
- description: z4.string().optional(),
1513
+ description: z7.string().optional(),
1439
1514
  value: OpportunityValueSchema.optional(),
1440
- expectedCloseOn: z4.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("YYYY-MM-DD"),
1441
- probability: z4.number().int().min(0).max(100).optional(),
1442
- ownerId: z4.number().int().positive().optional().describe(
1443
- "Assign to user ID. Defaults to the API-token owner when omitted \u2014 note that opportunities do NOT inherit owner from the linked party, even though one might expect it. Once set, this connector cannot clear the owner back to null (use Capsule's web UI). Discover IDs via list_users."
1515
+ expectedCloseOn: z7.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("YYYY-MM-DD"),
1516
+ probability: z7.number().int().min(0).max(100).optional(),
1517
+ ownerId: positiveId.optional().describe(
1518
+ "Assign to user ID. Defaults to the API-token owner when omitted \u2014 note that opportunities do NOT inherit owner from the linked party, even though one might expect it. Once set, this connector cannot clear the owner back to null (use Capsule's web UI). Discover IDs via list_users. WARNING: tenant pipeline / milestone-reached automation can mutate this field post-create \u2014 see the `milestoneId` description for details and the chained-PUT workaround."
1519
+ ),
1520
+ teamId: positiveId.optional().describe(
1521
+ "Assign to team ID (discover via list_teams). Independent from `ownerId` \u2014 setting one does NOT clear the other on create. Three ownership shapes are valid: owner alone, team alone, or owner+team (the owner must be a member of the team; users can belong to multiple teams \u2014 422 'owner is not a member of the team' otherwise)."
1444
1522
  )
1445
1523
  });
1446
1524
  async function createOpportunity(input) {
1447
- const { partyId, milestoneId, ownerId, ...rest } = input;
1525
+ const { partyId, milestoneId, ownerId, teamId, ...rest } = input;
1448
1526
  const body = {
1449
1527
  ...rest,
1450
1528
  party: { id: partyId },
1451
1529
  milestone: { id: milestoneId }
1452
1530
  };
1453
- if (ownerId) body["owner"] = { id: ownerId };
1531
+ setRef(body, "owner", ownerId);
1532
+ setRef(body, "team", teamId);
1454
1533
  return capsulePost("/opportunities", { opportunity: body });
1455
1534
  }
1456
- var updateOpportunitySchema = z4.object({
1457
- id: z4.number().int().positive(),
1458
- name: z4.string().min(1).optional(),
1459
- milestoneId: z4.number().int().positive().optional().describe(
1460
- "Move the opportunity to this milestone. Side effects depend on the target: closing milestones (Won/Lost) auto-set `closedOn` to today and `probability` to the milestone default (100/0), preserving `lastOpenMilestone` as the previous open stage; moving back to an open milestone clears `closedOn` and re-applies the milestone's default probability (Won/Lost is reversible \u2014 no separate reopen tool). WARNING: Capsule does NOT validate that the new milestone belongs to the opportunity's current pipeline. Passing a milestoneId from a different pipeline silently relocates the opportunity across pipelines, and `lastOpenMilestone` may then reference a milestone in the previous pipeline. Verify against the opportunity's current pipeline (read the opp first, list its pipeline's milestones via list_milestones) before passing a cross-pipeline id."
1535
+ var updateOpportunitySchema = z7.object({
1536
+ id: positiveId,
1537
+ name: z7.string().min(1).optional(),
1538
+ milestoneId: positiveId.optional().describe(
1539
+ "Move the opportunity to this milestone. Side effects depend on the target: closing milestones (Won/Lost) auto-set `closedOn` to today and `probability` to the milestone default (100/0), preserving `lastOpenMilestone` as the previous open stage; moving back to an open milestone clears `closedOn` and re-applies the milestone's default probability (Won/Lost is reversible \u2014 no separate reopen tool). WARNING: Capsule does NOT validate that the new milestone belongs to the opportunity's current pipeline. Passing a milestoneId from a different pipeline silently relocates the opportunity across pipelines, and `lastOpenMilestone` may then reference a milestone in the previous pipeline. Verify against the opportunity's current pipeline (read the opp first, list its pipeline's milestones via list_milestones) before passing a cross-pipeline id. NOTE: changing `milestoneId` can fire **pipeline / milestone-reached automations** that mutate `owner` / `team` on the destination milestone (same shape as `create_opportunity` \u2014 see its `milestoneId` description for the owner-clearing automation caveat). If a milestone-change-and-owner-set in the same call lands with `owner: null`, follow up with a second `update_opportunity` (or `batch_update_opportunity`) carrying both `ownerId` and `teamId` \u2014 milestone-reached triggers only fire on the transition, so a subsequent PUT preserves your values."
1461
1540
  ),
1462
- description: z4.string().optional(),
1541
+ description: z7.string().optional(),
1463
1542
  value: OpportunityValueSchema.optional(),
1464
- expectedCloseOn: z4.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional(),
1465
- probability: z4.number().int().min(0).max(100).optional().describe(
1543
+ expectedCloseOn: z7.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional(),
1544
+ probability: z7.number().int().min(0).max(100).optional().describe(
1466
1545
  "Win probability 0\u2013100. On an open milestone this overrides the milestone's default probability. CANNOT be set in the same call as a closing milestone (Won/Lost) \u2014 Capsule processes the milestone change first, the opportunity becomes closed, then the probability update is rejected as edit-on-closed-opp with 422 'probability can be updated only for open opportunity'. To close an opportunity, leave probability out of the call: it auto-snaps to 100% (Won) or 0% (Lost)."
1467
1546
  ),
1468
- lostReasonId: z4.number().int().positive().optional().describe(
1547
+ lostReasonId: positiveId.optional().describe(
1469
1548
  "Reason the opportunity was lost. Only meaningful when transitioning to a Lost milestone \u2014 Capsule silently drops it for other milestones. Without this set, a connector-driven Lost-close leaves `lostReason: null`. Discover IDs via list_lostreasons."
1470
1549
  ),
1471
- ownerId: z4.number().int().positive().optional().describe(
1472
- "Reassign owner to user ID. Once set, this connector cannot clear an owner back to null \u2014 use Capsule's web UI for that."
1550
+ ownerId: positiveId.optional().describe(
1551
+ "Reassign owner to user ID. Once set, this connector cannot clear an owner back to null \u2014 use Capsule's web UI for that. When you supply `ownerId` and omit `teamId`, the connector fetches the opportunity's current team and includes it in the PUT body to preserve it across the owner change. Without this defensive read, Capsule's PUT would clear the existing team (see NOTES-ON-CAPSULE-API.md \xA727 \u2014 same asymmetric semantic as /kases). Supply `teamId` explicitly on the same call to change the team instead."
1552
+ ),
1553
+ teamId: positiveId.nullable().optional().describe(
1554
+ "Reassign team: pass a team ID (discover via list_teams) to set, or `null` to unassign. Capsule preserves the existing owner across a team change (server-side), so `update_opportunity { teamId }` alone is safe \u2014 the owner is carried through. Owner must be a member of the new team or Capsule returns 422 'owner is not a member of the team'. Independent from `ownerId` \u2014 setting `teamId` does NOT clear the owner."
1473
1555
  ),
1474
- fields: z4.array(CustomFieldWriteSchema).optional().describe(fieldsArrayDescriptor("get_opportunity"))
1556
+ fields: z7.array(CustomFieldWriteSchema).optional().describe(fieldsArrayDescriptor("get_opportunity"))
1475
1557
  });
1476
1558
  async function updateOpportunity(input) {
1477
- const { id, milestoneId, ownerId, lostReasonId, fields, ...rest } = input;
1559
+ const { id, milestoneId, ownerId, teamId, lostReasonId, fields, ...rest } = input;
1478
1560
  const body = {};
1479
1561
  for (const [k, v] of Object.entries(rest)) {
1480
1562
  if (v !== void 0) body[k] = v;
1481
1563
  }
1482
- if (milestoneId) body["milestone"] = { id: milestoneId };
1483
- if (ownerId) body["owner"] = { id: ownerId };
1484
- if (lostReasonId) body["lostReason"] = { id: lostReasonId };
1564
+ setRef(body, "milestone", milestoneId);
1565
+ let resolvedTeamId = teamId;
1566
+ if (ownerId !== void 0 && teamId === void 0) {
1567
+ ({ teamId: resolvedTeamId } = await readEntityRefs(`/opportunities/${id}`, "opportunity"));
1568
+ }
1569
+ setRef(body, "owner", ownerId);
1570
+ setNullableRef(body, "team", resolvedTeamId);
1571
+ setRef(body, "lostReason", lostReasonId);
1485
1572
  const mappedFields = mapFieldsForBody(fields);
1486
1573
  if (mappedFields !== void 0) body["fields"] = mappedFields;
1487
1574
  return capsulePut(`/opportunities/${id}`, {
1488
1575
  opportunity: body
1489
1576
  });
1490
1577
  }
1491
- var batchUpdateOpportunitySchema = z4.object({
1492
- items: z4.array(updateOpportunitySchema).min(1).max(50).describe(
1493
- "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."
1494
- )
1578
+ var { schema: batchUpdateOpportunitySchema, handler: batchUpdateOpportunity } = defineBatch({
1579
+ toolName: "batch_update_opportunity",
1580
+ itemSchema: updateOpportunitySchema,
1581
+ itemDescription: "Array of 1\u201350 update_opportunity inputs. Each item is the same shape as a single update_opportunity call \u2014 id is required, every other field is optional. Capped at 50 so a single tool call can't burn an outsized share of Capsule's hourly per-token rate budget.",
1582
+ itemHandler: updateOpportunity
1495
1583
  });
1496
- async function batchUpdateOpportunity(input, opts = {}) {
1497
- return batchExecute(
1498
- "batch_update_opportunity",
1499
- input.items,
1500
- (item) => updateOpportunity(item),
1501
- opts
1502
- );
1503
- }
1504
- var deleteOpportunitySchema = z4.object({
1505
- id: z4.number().int().positive(),
1506
- confirm: confirmFlag().describe(
1507
- "Must be set to true. Permanently deletes the opportunity. Irreversible."
1508
- )
1584
+ var { schema: deleteOpportunitySchema, handler: deleteOpportunity } = defineDelete({
1585
+ toolName: "delete_opportunity",
1586
+ pathPrefix: "/opportunities",
1587
+ confirmHint: "Must be set to true. Permanently deletes the opportunity. Irreversible."
1509
1588
  });
1510
- async function deleteOpportunity(input) {
1511
- if (input.confirm !== true) {
1512
- throw new Error("delete_opportunity requires confirm: true");
1513
- }
1514
- return idempotent(
1515
- () => capsuleDelete(`/opportunities/${input.id}`),
1516
- () => ({ deleted: true, alreadyDeleted: false, id: input.id }),
1517
- () => ({ deleted: true, alreadyDeleted: true, id: input.id })
1518
- );
1519
- }
1520
1589
 
1521
1590
  // src/tools/projects.ts
1522
- import { z as z5 } from "zod";
1523
- var listProjectsSchema = z5.object({
1524
- status: z5.enum(["OPEN", "CLOSED"]).optional(),
1525
- embed: z5.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
1526
- page: z5.number().int().positive().optional().default(1),
1527
- perPage: z5.number().int().min(1).max(100).optional().default(25)
1591
+ import { z as z8 } from "zod";
1592
+ var listProjectsSchema = z8.object({
1593
+ status: z8.enum(["OPEN", "CLOSED"]).optional(),
1594
+ embed: z8.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
1595
+ page: z8.number().int().positive().optional().default(1),
1596
+ perPage: z8.number().int().min(1).max(100).optional().default(25)
1528
1597
  });
1529
1598
  async function listProjects(input) {
1530
1599
  const { data, nextPage } = await capsuleGet("/kases", {
@@ -1535,9 +1604,9 @@ async function listProjects(input) {
1535
1604
  });
1536
1605
  return { ...data, nextPage };
1537
1606
  }
1538
- var getProjectSchema = z5.object({
1539
- id: z5.number().int().positive(),
1540
- embed: z5.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
1607
+ var getProjectSchema = z8.object({
1608
+ id: positiveId,
1609
+ embed: z8.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
1541
1610
  });
1542
1611
  async function getProject(input) {
1543
1612
  const { data } = await capsuleGet(`/kases/${input.id}`, {
@@ -1545,11 +1614,11 @@ async function getProject(input) {
1545
1614
  });
1546
1615
  return data;
1547
1616
  }
1548
- var getProjectsSchema = z5.object({
1549
- ids: z5.array(z5.number().int().positive()).min(1).max(50).describe(
1617
+ var getProjectsSchema = z8.object({
1618
+ ids: z8.array(positiveId).min(1).max(50).describe(
1550
1619
  "Array of project IDs (1\u201350). Capsule's native batch-fetch endpoint caps at 10 per request; the connector transparently splits larger sets into 10-id chunks and fans out the Capsule calls in parallel."
1551
1620
  ),
1552
- embed: z5.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
1621
+ embed: z8.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
1553
1622
  });
1554
1623
  async function getProjects(input) {
1555
1624
  const { ids, embed } = input;
@@ -1567,21 +1636,21 @@ async function getProjects(input) {
1567
1636
  );
1568
1637
  return { kases: responses.flatMap((r) => r.data.kases) };
1569
1638
  }
1570
- var createProjectSchema = z5.object({
1571
- name: z5.string().min(1),
1572
- partyId: z5.number().int().positive().describe("ID of the party linked to this project"),
1573
- description: z5.string().optional(),
1574
- status: z5.enum(["OPEN", "CLOSED"]).optional().describe("Defaults to OPEN when omitted."),
1575
- ownerId: z5.number().int().positive().optional().describe(
1639
+ var createProjectSchema = z8.object({
1640
+ name: z8.string().min(1),
1641
+ partyId: positiveId.describe("ID of the party linked to this project"),
1642
+ description: z8.string().optional(),
1643
+ status: z8.enum(["OPEN", "CLOSED"]).optional().describe("Defaults to OPEN when omitted."),
1644
+ ownerId: positiveId.optional().describe(
1576
1645
  "Assign to user ID. Defaults to the API-token owner when omitted, same as create_party / create_opportunity / create_task. NOTE: some Capsule tenants configure board-level **automation rules** that mutate `owner` (and `team`) on project creation \u2014 e.g. an automation that clears `owner` when a project enters a particular board. If you observe a project landing with unexpected `owner: null` after a create_project with `ownerId`, check the target board's automation configuration. Capsule's API itself does not drop `ownerId` when `stageId` is also supplied."
1577
1646
  ),
1578
- teamId: z5.number().int().positive().optional().describe(
1647
+ teamId: positiveId.optional().describe(
1579
1648
  "Assign to team ID (discover via list_teams). Capsule projects must always have at least one of {owner, team} set \u2014 Capsule returns 422 'owner or team is required' otherwise. Three ownership shapes are valid: owner alone, team alone, or owner+team (the user must be a member of the team \u2014 users can belong to multiple teams; 422 'owner is not a member of the team' otherwise). Tenant-specific board automations may set the team field on project creation (e.g. 'when project enters board X, set team to T'). If you observe a team set despite omitting `teamId`, check the target board's automation rules."
1580
1649
  ),
1581
- stageId: z5.number().int().positive().optional().describe(
1650
+ stageId: positiveId.optional().describe(
1582
1651
  "Stage (board column) to place the project on. Discover IDs via list_stages \u2014 each stage belongs to one Board, so picking a stageId implicitly picks the board. If omitted, the project is created with no stage assignment (and won't appear on any board). NOTE: tenant-specific board automation rules may run on project creation and mutate `owner` / `team` fields. See `create_project.ownerId` / `create_project.teamId` for the automation caveat. Capsule's create endpoint itself preserves the `ownerId` / `teamId` you supply \u2014 any clearing you observe traces to board automations, not the API."
1583
1652
  ),
1584
- expectedCloseOn: z5.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("YYYY-MM-DD")
1653
+ expectedCloseOn: z8.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("YYYY-MM-DD")
1585
1654
  });
1586
1655
  async function createProject(input) {
1587
1656
  const { partyId, ownerId, teamId, status, stageId, ...rest } = input;
@@ -1590,27 +1659,27 @@ async function createProject(input) {
1590
1659
  status: status ?? "OPEN",
1591
1660
  party: { id: partyId }
1592
1661
  };
1593
- if (ownerId) body["owner"] = { id: ownerId };
1594
- if (teamId) body["team"] = { id: teamId };
1662
+ setRef(body, "owner", ownerId);
1663
+ setRef(body, "team", teamId);
1595
1664
  if (stageId) body["stage"] = stageId;
1596
1665
  return capsulePost("/kases", { kase: body });
1597
1666
  }
1598
- var updateProjectSchema = z5.object({
1599
- id: z5.number().int().positive(),
1600
- name: z5.string().min(1).optional(),
1601
- description: z5.string().optional(),
1602
- status: z5.enum(["OPEN", "CLOSED"]).optional(),
1603
- ownerId: z5.number().int().positive().nullable().optional().describe(
1667
+ var updateProjectSchema = z8.object({
1668
+ id: positiveId,
1669
+ name: z8.string().min(1).optional(),
1670
+ description: z8.string().optional(),
1671
+ status: z8.enum(["OPEN", "CLOSED"]).optional(),
1672
+ ownerId: positiveId.nullable().optional().describe(
1604
1673
  "Reassign owner: pass a user ID to set, or `null` to unassign (matches the 'Unassign' option in Capsule's web UI). When you supply `ownerId` and omit `teamId` and/or `stageId`, the connector fetches the project's current omitted fields and includes them in the PUT body \u2014 this preserves them across the owner change (without it, Capsule's PUT would clear team; stage carry is defensive against the symmetric clear). Supply `teamId` and/or `stageId` explicitly on the same call to change them instead. `teamId: null` clears the team as part of an owner change. Constraints (Capsule enforces, 422 on violation): owner must be a member of the team if both are set; a project must always have at least one of {owner, team} set (cannot clear both)."
1605
1674
  ),
1606
- teamId: z5.number().int().positive().nullable().optional().describe(
1675
+ teamId: positiveId.nullable().optional().describe(
1607
1676
  "Reassign team: pass a team ID (discover via list_teams) to set, or `null` to unassign. Capsule preserves the existing owner across a team change (server-side), so `update_project { teamId }` alone is safe \u2014 the owner is carried through. Owner must be a member of the new team or Capsule returns 422 'owner is not a member of the team'. A project must always have at least one of {owner, team} set \u2014 `teamId: null` on a project whose owner is already null returns 422 'owner or team is required'."
1608
1677
  ),
1609
- stageId: z5.number().int().positive().optional().describe(
1678
+ stageId: positiveId.optional().describe(
1610
1679
  "Move the project to this stage (board column). Discover IDs via list_stages. Owner and team are preserved across stage-only updates (Capsule's PUT semantic). WARNING (cross-board): Capsule does NOT validate that the new stage belongs to the project's current board \u2014 passing a stageId from a different board silently relocates the project across boards. Team and other board-derived defaults are NOT updated to match the new board. Verify against the project's current board (read the project first, list its board's stages) before passing a cross-board id."
1611
1680
  ),
1612
- expectedCloseOn: z5.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("YYYY-MM-DD"),
1613
- fields: z5.array(CustomFieldWriteSchema).optional().describe(
1681
+ expectedCloseOn: z8.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("YYYY-MM-DD"),
1682
+ fields: z8.array(CustomFieldWriteSchema).optional().describe(
1614
1683
  fieldsArrayDescriptor("get_project") + " Project-specific: setting a field whose definition lives under a 'data tag' populates the row's internal tagId but does NOT auto-add the data tag to the project's tags array \u2014 use add_tag explicitly if you want it visible via embed=tags."
1615
1684
  )
1616
1685
  });
@@ -1623,55 +1692,38 @@ async function updateProject(input) {
1623
1692
  let resolvedTeamId = teamId;
1624
1693
  let resolvedStageId = stageId;
1625
1694
  if (ownerId !== void 0 && (teamId === void 0 || stageId === void 0)) {
1626
- const { data } = await capsuleGet(`/kases/${id}`);
1627
- if (teamId === void 0) {
1628
- resolvedTeamId = data.kase?.team?.id ?? void 0;
1629
- }
1630
- if (stageId === void 0) {
1631
- resolvedStageId = data.kase?.stage?.id ?? void 0;
1632
- }
1695
+ const current = await readEntityRefs(`/kases/${id}`, "kase");
1696
+ if (teamId === void 0) resolvedTeamId = current.teamId;
1697
+ if (stageId === void 0) resolvedStageId = current.stageId;
1633
1698
  }
1634
- if (ownerId === null) body["owner"] = null;
1635
- else if (ownerId !== void 0) body["owner"] = { id: ownerId };
1636
- if (resolvedTeamId === null) body["team"] = null;
1637
- else if (resolvedTeamId !== void 0) body["team"] = { id: resolvedTeamId };
1699
+ setNullableRef(body, "owner", ownerId);
1700
+ setNullableRef(body, "team", resolvedTeamId);
1638
1701
  if (resolvedStageId) body["stage"] = resolvedStageId;
1639
1702
  const mappedFields = mapFieldsForBody(fields);
1640
1703
  if (mappedFields !== void 0) body["fields"] = mappedFields;
1641
1704
  return capsulePut(`/kases/${id}`, { kase: body });
1642
1705
  }
1643
- var deleteProjectSchema = z5.object({
1644
- id: z5.number().int().positive(),
1645
- confirm: confirmFlag().describe(
1646
- "Must be set to true. Permanently deletes the project (case). Consider update_project status='CLOSED' instead. Irreversible."
1647
- )
1706
+ var { schema: deleteProjectSchema, handler: deleteProject } = defineDelete({
1707
+ toolName: "delete_project",
1708
+ pathPrefix: "/kases",
1709
+ confirmHint: "Must be set to true. Permanently deletes the project (case). Consider update_project status='CLOSED' instead. Irreversible."
1648
1710
  });
1649
- async function deleteProject(input) {
1650
- if (input.confirm !== true) {
1651
- throw new Error("delete_project requires confirm: true");
1652
- }
1653
- return idempotent(
1654
- () => capsuleDelete(`/kases/${input.id}`),
1655
- () => ({ deleted: true, alreadyDeleted: false, id: input.id }),
1656
- () => ({ deleted: true, alreadyDeleted: true, id: input.id })
1657
- );
1658
- }
1659
1711
 
1660
1712
  // src/tools/tasks.ts
1661
- import { z as z6 } from "zod";
1662
- var listTasksSchema = z6.object({
1713
+ import { z as z9 } from "zod";
1714
+ var listTasksSchema = z9.object({
1663
1715
  // Note: Capsule has a third internal status `PENDING` (a task that's
1664
1716
  // part of an active track but not yet "open"), but it can only be
1665
1717
  // reached via track machinery — it is NOT directly settable by
1666
1718
  // /tasks PUT, and a list filter for it returns the same as OPEN
1667
1719
  // anyway. We expose only the two values that are actually filterable
1668
1720
  // by the v2 API.
1669
- status: z6.enum(["OPEN", "COMPLETED"]).optional().describe(
1721
+ status: z9.enum(["OPEN", "COMPLETED"]).optional().describe(
1670
1722
  "Defaults to OPEN when omitted. Pass COMPLETED to filter to completed tasks, or 'OPEN' explicitly."
1671
1723
  ),
1672
- ownerId: z6.number().int().positive().optional().describe("Filter to tasks owned by this user ID"),
1673
- page: z6.number().int().positive().optional().default(1),
1674
- perPage: z6.number().int().min(1).max(100).optional().default(25)
1724
+ ownerId: positiveId.optional().describe("Filter to tasks owned by this user ID"),
1725
+ page: z9.number().int().positive().optional().default(1),
1726
+ perPage: z9.number().int().min(1).max(100).optional().default(25)
1675
1727
  });
1676
1728
  async function listTasks(input) {
1677
1729
  const { data, nextPage } = await capsuleGet("/tasks", {
@@ -1685,15 +1737,15 @@ async function listTasks(input) {
1685
1737
  });
1686
1738
  return { ...data, nextPage };
1687
1739
  }
1688
- var getTaskSchema = z6.object({
1689
- id: z6.number().int().positive().describe("Task ID")
1740
+ var getTaskSchema = z9.object({
1741
+ id: positiveId.describe("Task ID")
1690
1742
  });
1691
1743
  async function getTask(input) {
1692
1744
  const { data } = await capsuleGet(`/tasks/${input.id}`);
1693
1745
  return data;
1694
1746
  }
1695
- var getTasksSchema = z6.object({
1696
- ids: z6.array(z6.number().int().positive()).min(1).max(50).describe(
1747
+ var getTasksSchema = z9.object({
1748
+ ids: z9.array(positiveId).min(1).max(50).describe(
1697
1749
  "Array of task IDs (1\u201350). Capsule's native batch-fetch endpoint caps at 10 per request; the connector transparently splits larger sets into 10-id chunks and fans out the Capsule calls in parallel."
1698
1750
  )
1699
1751
  });
@@ -1709,17 +1761,17 @@ async function getTasks(input) {
1709
1761
  );
1710
1762
  return { tasks: responses.flatMap((r) => r.data.tasks) };
1711
1763
  }
1712
- var createTaskSchema = z6.object({
1713
- description: z6.string().min(1),
1714
- dueOn: z6.string().regex(/^\d{4}-\d{2}-\d{2}$/).describe("YYYY-MM-DD"),
1715
- dueTime: z6.string().regex(/^\d{2}:\d{2}$/).optional().describe("HH:MM in user's timezone"),
1716
- detail: z6.string().optional(),
1717
- ownerId: z6.number().int().positive().optional().describe(
1764
+ var createTaskSchema = z9.object({
1765
+ description: z9.string().min(1),
1766
+ dueOn: z9.string().regex(/^\d{4}-\d{2}-\d{2}$/).describe("YYYY-MM-DD"),
1767
+ dueTime: z9.string().regex(/^\d{2}:\d{2}$/).optional().describe("HH:MM in user's timezone"),
1768
+ detail: z9.string().optional(),
1769
+ ownerId: positiveId.optional().describe(
1718
1770
  "Assign to user ID. Defaults to the API-token owner when omitted. Once set, this connector cannot clear the owner back to null \u2014 use Capsule's web UI for that."
1719
1771
  ),
1720
- partyId: z6.number().int().positive().optional().describe("Link task to a party (mutually exclusive with opportunityId/projectId)"),
1721
- opportunityId: z6.number().int().positive().optional().describe("Link task to an opportunity (mutually exclusive with partyId/projectId)"),
1722
- projectId: z6.number().int().positive().optional().describe("Link task to a project (mutually exclusive with partyId/opportunityId)")
1772
+ partyId: positiveId.optional().describe("Link task to a party (mutually exclusive with opportunityId/projectId)"),
1773
+ opportunityId: positiveId.optional().describe("Link task to an opportunity (mutually exclusive with partyId/projectId)"),
1774
+ projectId: positiveId.optional().describe("Link task to a project (mutually exclusive with partyId/opportunityId)")
1723
1775
  });
1724
1776
  async function createTask(input) {
1725
1777
  const linked = [input.partyId, input.opportunityId, input.projectId].filter(Boolean);
@@ -1728,25 +1780,25 @@ async function createTask(input) {
1728
1780
  }
1729
1781
  const { ownerId, partyId, opportunityId, projectId, ...rest } = input;
1730
1782
  const body = { ...rest };
1731
- if (ownerId) body["owner"] = { id: ownerId };
1732
- if (partyId) body["party"] = { id: partyId };
1733
- if (opportunityId) body["opportunity"] = { id: opportunityId };
1734
- if (projectId) body["kase"] = { id: projectId };
1783
+ setRef(body, "owner", ownerId);
1784
+ setRef(body, "party", partyId);
1785
+ setRef(body, "opportunity", opportunityId);
1786
+ setRef(body, "kase", projectId);
1735
1787
  return capsulePost("/tasks", { task: body });
1736
1788
  }
1737
- var updateTaskSchema = z6.object({
1738
- id: z6.number().int().positive(),
1739
- description: z6.string().min(1).optional(),
1740
- dueOn: z6.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("YYYY-MM-DD"),
1741
- dueTime: z6.string().regex(/^\d{2}:\d{2}$/).optional().describe("HH:MM in user's timezone"),
1742
- detail: z6.string().optional(),
1789
+ var updateTaskSchema = z9.object({
1790
+ id: positiveId,
1791
+ description: z9.string().min(1).optional(),
1792
+ dueOn: z9.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("YYYY-MM-DD"),
1793
+ dueTime: z9.string().regex(/^\d{2}:\d{2}$/).optional().describe("HH:MM in user's timezone"),
1794
+ detail: z9.string().optional(),
1743
1795
  // Capsule rejects direct sets of `PENDING` (which is a track-machinery
1744
1796
  // internal state) with 422 "cannot set task status to PENDING".
1745
1797
  // Only OPEN and COMPLETED are settable here.
1746
- status: z6.enum(["OPEN", "COMPLETED"]).optional().describe(
1798
+ status: z9.enum(["OPEN", "COMPLETED"]).optional().describe(
1747
1799
  "Set to OPEN or COMPLETED. (PENDING exists internally for track-driven tasks but cannot be set directly via this tool \u2014 Capsule rejects it.) Setting status: OPEN on an already-open task is a true no-op (does not advance updatedAt)."
1748
1800
  ),
1749
- ownerId: z6.number().int().positive().optional().describe(
1801
+ ownerId: positiveId.optional().describe(
1750
1802
  "Reassign owner to user ID. Once set, this connector cannot clear an owner back to null \u2014 use Capsule's web UI for that."
1751
1803
  )
1752
1804
  });
@@ -1756,51 +1808,40 @@ async function updateTask(input) {
1756
1808
  for (const [k, v] of Object.entries(rest)) {
1757
1809
  if (v !== void 0) body[k] = v;
1758
1810
  }
1759
- if (ownerId) body["owner"] = { id: ownerId };
1811
+ setRef(body, "owner", ownerId);
1760
1812
  return capsulePut(`/tasks/${id}`, { task: body });
1761
1813
  }
1762
- var completeTaskSchema = z6.object({
1763
- id: z6.number().int().positive()
1814
+ var completeTaskSchema = z9.object({
1815
+ id: positiveId
1764
1816
  });
1765
1817
  async function completeTask(input) {
1766
1818
  return capsulePut(`/tasks/${input.id}`, {
1767
1819
  task: { status: "COMPLETED" }
1768
1820
  });
1769
1821
  }
1770
- var batchCompleteTaskSchema = z6.object({
1771
- ids: z6.array(z6.number().int().positive()).min(1).max(50).describe(
1822
+ var batchCompleteTaskSchema = z9.object({
1823
+ ids: z9.array(positiveId).min(1).max(50).describe(
1772
1824
  "Array of 1\u201350 task ids to mark COMPLETED in parallel. Each id resolves to one PUT /tasks/{id}; failures (e.g. 404 for a deleted task) surface per-item in the result array, the rest still complete. Capped at 50."
1773
1825
  )
1774
1826
  });
1775
1827
  async function batchCompleteTask(input, opts = {}) {
1776
1828
  return batchExecute("batch_complete_task", input.ids, (id) => completeTask({ id }), opts);
1777
1829
  }
1778
- var deleteTaskSchema = z6.object({
1779
- id: z6.number().int().positive(),
1780
- confirm: confirmFlag().describe(
1781
- "Must be set to true. Permanently deletes the task. To mark done without losing history use complete_task. Irreversible."
1782
- )
1830
+ var { schema: deleteTaskSchema, handler: deleteTask } = defineDelete({
1831
+ toolName: "delete_task",
1832
+ pathPrefix: "/tasks",
1833
+ confirmHint: "Must be set to true. Permanently deletes the task. To mark done without losing history use complete_task. Irreversible."
1783
1834
  });
1784
- async function deleteTask(input) {
1785
- if (input.confirm !== true) {
1786
- throw new Error("delete_task requires confirm: true");
1787
- }
1788
- return idempotent(
1789
- () => capsuleDelete(`/tasks/${input.id}`),
1790
- () => ({ deleted: true, alreadyDeleted: false, id: input.id }),
1791
- () => ({ deleted: true, alreadyDeleted: true, id: input.id })
1792
- );
1793
- }
1794
1835
 
1795
1836
  // src/tools/entries.ts
1796
- import { z as z7 } from "zod";
1837
+ import { z as z10 } from "zod";
1797
1838
  var listEntriesPagination = {
1798
- page: z7.number().int().positive().optional().default(1),
1799
- perPage: z7.number().int().min(1).max(100).optional().default(25),
1800
- embed: z7.string().optional().describe(EMBED_ATTACHMENTS_PARTICIPANTS_DESCRIPTION)
1839
+ page: z10.number().int().positive().optional().default(1),
1840
+ perPage: z10.number().int().min(1).max(100).optional().default(25),
1841
+ embed: z10.string().optional().describe(EMBED_ATTACHMENTS_PARTICIPANTS_DESCRIPTION)
1801
1842
  };
1802
- var listPartyEntriesSchema = z7.object({
1803
- partyId: z7.number().int().positive(),
1843
+ var listPartyEntriesSchema = z10.object({
1844
+ partyId: positiveId,
1804
1845
  ...listEntriesPagination
1805
1846
  });
1806
1847
  async function listPartyEntries(input) {
@@ -1810,8 +1851,8 @@ async function listPartyEntries(input) {
1810
1851
  );
1811
1852
  return { ...data, nextPage };
1812
1853
  }
1813
- var listOpportunityEntriesSchema = z7.object({
1814
- opportunityId: z7.number().int().positive(),
1854
+ var listOpportunityEntriesSchema = z10.object({
1855
+ opportunityId: positiveId,
1815
1856
  ...listEntriesPagination
1816
1857
  });
1817
1858
  async function listOpportunityEntries(input) {
@@ -1821,8 +1862,8 @@ async function listOpportunityEntries(input) {
1821
1862
  );
1822
1863
  return { ...data, nextPage };
1823
1864
  }
1824
- var listProjectEntriesSchema = z7.object({
1825
- projectId: z7.number().int().positive(),
1865
+ var listProjectEntriesSchema = z10.object({
1866
+ projectId: positiveId,
1826
1867
  ...listEntriesPagination
1827
1868
  });
1828
1869
  async function listProjectEntries(input) {
@@ -1832,9 +1873,9 @@ async function listProjectEntries(input) {
1832
1873
  );
1833
1874
  return { ...data, nextPage };
1834
1875
  }
1835
- var getEntrySchema = z7.object({
1836
- id: z7.number().int().positive(),
1837
- embed: z7.string().optional().describe(EMBED_ATTACHMENTS_PARTICIPANTS_DESCRIPTION)
1876
+ var getEntrySchema = z10.object({
1877
+ id: positiveId,
1878
+ embed: z10.string().optional().describe(EMBED_ATTACHMENTS_PARTICIPANTS_DESCRIPTION)
1838
1879
  });
1839
1880
  async function getEntry(input) {
1840
1881
  const { data } = await capsuleGet(`/entries/${input.id}`, {
@@ -1842,7 +1883,7 @@ async function getEntry(input) {
1842
1883
  });
1843
1884
  return data;
1844
1885
  }
1845
- var listEntriesSchema = z7.object({
1886
+ var listEntriesSchema = z10.object({
1846
1887
  ...listEntriesPagination
1847
1888
  });
1848
1889
  async function listEntries(input) {
@@ -1853,14 +1894,14 @@ async function listEntries(input) {
1853
1894
  });
1854
1895
  return { ...data, nextPage };
1855
1896
  }
1856
- var addNoteSchema = z7.object({
1857
- content: z7.string().min(1).describe(
1897
+ var addNoteSchema = z10.object({
1898
+ content: z10.string().min(1).describe(
1858
1899
  "Note body text. Stored verbatim and treated as MARKDOWN \u2014 Capsule's web UI renders the markdown when displaying. Pass markdown source ('# Heading', '**bold**', '- bullet'), not HTML."
1859
1900
  ),
1860
- partyId: z7.number().int().positive().optional().describe("Link note to a party (mutually exclusive with opportunityId/projectId)"),
1861
- opportunityId: z7.number().int().positive().optional().describe("Link note to an opportunity (mutually exclusive with partyId/projectId)"),
1862
- projectId: z7.number().int().positive().optional().describe("Link note to a project (mutually exclusive with partyId/opportunityId)"),
1863
- entryAt: z7.string().regex(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:?\d{2})$/).optional().describe(
1901
+ partyId: positiveId.optional().describe("Link note to a party (mutually exclusive with opportunityId/projectId)"),
1902
+ opportunityId: positiveId.optional().describe("Link note to an opportunity (mutually exclusive with partyId/projectId)"),
1903
+ projectId: positiveId.optional().describe("Link note to a project (mutually exclusive with partyId/opportunityId)"),
1904
+ entryAt: z10.string().regex(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:?\d{2})$/).optional().describe(
1864
1905
  "ISO-8601 timestamp for when this note actually happened (e.g. '2024-03-15T14:30:00Z'). Defaults to now. Use this for backdating historical notes when migrating from another system. `entryAt` is preserved across subsequent update_entry calls; only `updatedAt` advances on edits. Note attribution flows to the API-token owner \u2014 there is no way to record a note as authored by a different user via this connector (a `creatorId` parameter would enable audit-attribution spoofing on shared-connector deployments, so it is intentionally not exposed)."
1865
1906
  )
1866
1907
  });
@@ -1871,18 +1912,18 @@ async function addNote(input) {
1871
1912
  throw new Error("Provide exactly one of partyId, opportunityId, or projectId");
1872
1913
  }
1873
1914
  const body = { type: "note", content };
1874
- if (partyId) body["party"] = { id: partyId };
1875
- if (opportunityId) body["opportunity"] = { id: opportunityId };
1876
- if (projectId) body["kase"] = { id: projectId };
1915
+ setRef(body, "party", partyId);
1916
+ setRef(body, "opportunity", opportunityId);
1917
+ setRef(body, "kase", projectId);
1877
1918
  if (entryAt !== void 0) body["entryAt"] = entryAt;
1878
1919
  return capsulePost("/entries", { entry: body });
1879
1920
  }
1880
- var updateEntrySchema = z7.object({
1881
- id: z7.number().int().positive().describe("Entry ID to update"),
1882
- content: z7.string().min(1).optional().describe(
1921
+ var updateEntrySchema = z10.object({
1922
+ id: positiveId.describe("Entry ID to update"),
1923
+ content: z10.string().min(1).optional().describe(
1883
1924
  "New body text for the entry. For notes, this is the markdown content; for emails, the body. Provide only if you want to change it."
1884
1925
  ),
1885
- subject: z7.string().optional().describe(
1926
+ subject: z10.string().optional().describe(
1886
1927
  "New subject line. Mostly meaningful on email-type entries; on plain notes Capsule accepts the call (HTTP 200) but **does not store the subject and does not advance `updatedAt`** \u2014 a true no-op for inapplicable fields. `entryAt` (when the note was authored) is preserved across edits; `updatedAt` advances only when an applicable field actually changes. To sort/filter by 'when did this happen', use `entryAt`; for 'last touched', use `updatedAt`."
1887
1928
  )
1888
1929
  });
@@ -1896,30 +1937,20 @@ async function updateEntry(input) {
1896
1937
  }
1897
1938
  return capsulePut(`/entries/${id}`, { entry: body });
1898
1939
  }
1899
- var deleteEntrySchema = z7.object({
1900
- id: z7.number().int().positive().describe("Entry (note/email/task-record) ID"),
1901
- confirm: confirmFlag().describe(
1902
- "Must be set to true. Permanently deletes the entry \u2014 use this to remove a note from a party/opportunity/project. Irreversible."
1903
- )
1940
+ var { schema: deleteEntrySchema, handler: deleteEntry } = defineDelete({
1941
+ toolName: "delete_entry",
1942
+ pathPrefix: "/entries",
1943
+ confirmHint: "Must be set to true. Permanently deletes the entry \u2014 use this to remove a note from a party/opportunity/project. Irreversible.",
1944
+ idDescription: "Entry (note/email/task-record) ID"
1904
1945
  });
1905
- async function deleteEntry(input) {
1906
- if (input.confirm !== true) {
1907
- throw new Error("delete_entry requires confirm: true");
1908
- }
1909
- return idempotent(
1910
- () => capsuleDelete(`/entries/${input.id}`),
1911
- () => ({ deleted: true, alreadyDeleted: false, id: input.id }),
1912
- () => ({ deleted: true, alreadyDeleted: true, id: input.id })
1913
- );
1914
- }
1915
1946
 
1916
1947
  // src/tools/pipelines.ts
1917
- import { z as z8 } from "zod";
1948
+ import { z as z11 } from "zod";
1918
1949
  var paginationFields = {
1919
- page: z8.number().int().positive().optional(),
1920
- perPage: z8.number().int().min(1).max(100).optional()
1950
+ page: z11.number().int().positive().optional(),
1951
+ perPage: z11.number().int().min(1).max(100).optional()
1921
1952
  };
1922
- var listPipelinesSchema = z8.object({ ...paginationFields });
1953
+ var listPipelinesSchema = z11.object({ ...paginationFields });
1923
1954
  async function listPipelines(input) {
1924
1955
  const { data, nextPage } = await capsuleGetCached("/pipelines", {
1925
1956
  page: input.page ?? 1,
@@ -1927,8 +1958,8 @@ async function listPipelines(input) {
1927
1958
  });
1928
1959
  return { ...data, nextPage };
1929
1960
  }
1930
- var listMilestonesSchema = z8.object({
1931
- pipelineId: z8.number().int().positive(),
1961
+ var listMilestonesSchema = z11.object({
1962
+ pipelineId: positiveId,
1932
1963
  ...paginationFields
1933
1964
  });
1934
1965
  async function listMilestones(input) {
@@ -1940,12 +1971,12 @@ async function listMilestones(input) {
1940
1971
  }
1941
1972
 
1942
1973
  // src/tools/boards.ts
1943
- import { z as z9 } from "zod";
1974
+ import { z as z12 } from "zod";
1944
1975
  var paginationFields2 = {
1945
- page: z9.number().int().positive().optional(),
1946
- perPage: z9.number().int().min(1).max(100).optional()
1976
+ page: z12.number().int().positive().optional(),
1977
+ perPage: z12.number().int().min(1).max(100).optional()
1947
1978
  };
1948
- var listBoardsSchema = z9.object({ ...paginationFields2 });
1979
+ var listBoardsSchema = z12.object({ ...paginationFields2 });
1949
1980
  async function listBoards(input) {
1950
1981
  const { data, nextPage } = await capsuleGetCached("/boards", {
1951
1982
  page: input.page ?? 1,
@@ -1953,8 +1984,8 @@ async function listBoards(input) {
1953
1984
  });
1954
1985
  return { ...data, nextPage };
1955
1986
  }
1956
- var listStagesSchema = z9.object({
1957
- boardId: z9.number().int().positive().optional().describe(
1987
+ var listStagesSchema = z12.object({
1988
+ boardId: positiveId.optional().describe(
1958
1989
  "Optional. If provided, returns only the stages defined on that specific board (uses /boards/{id}/stages). Omit to get all stages across all boards in one call."
1959
1990
  ),
1960
1991
  ...paginationFields2
@@ -1969,7 +2000,7 @@ async function listStages(input) {
1969
2000
  }
1970
2001
 
1971
2002
  // src/tools/tags.ts
1972
- import { z as z10 } from "zod";
2003
+ import { z as z13 } from "zod";
1973
2004
  var TAG_LIST_PATH = {
1974
2005
  parties: "/parties/tags",
1975
2006
  opportunities: "/opportunities/tags",
@@ -1980,11 +2011,11 @@ var ENTITY_TO_WRAPPER = {
1980
2011
  opportunities: "opportunity",
1981
2012
  kases: "kase"
1982
2013
  };
1983
- var TagEntity = z10.enum(["parties", "opportunities", "kases"]).describe("Which entity type. Use 'kases' for projects (Capsule's legacy path name).");
1984
- var listTagsSchema = z10.object({
1985
- entity: z10.enum(["parties", "opportunities", "kases"]).describe("The resource type to list tags for"),
1986
- page: z10.number().int().positive().optional(),
1987
- perPage: z10.number().int().min(1).max(100).optional()
2014
+ var TagEntity = z13.enum(["parties", "opportunities", "kases"]).describe("Which entity type. Use 'kases' for projects (Capsule's legacy path name).");
2015
+ var listTagsSchema = z13.object({
2016
+ entity: z13.enum(["parties", "opportunities", "kases"]).describe("The resource type to list tags for"),
2017
+ page: z13.number().int().positive().optional(),
2018
+ perPage: z13.number().int().min(1).max(100).optional()
1988
2019
  });
1989
2020
  async function listTags(input) {
1990
2021
  const path = TAG_LIST_PATH[input.entity];
@@ -1994,10 +2025,10 @@ async function listTags(input) {
1994
2025
  });
1995
2026
  return { ...data, nextPage };
1996
2027
  }
1997
- var addTagSchema = z10.object({
2028
+ var addTagSchema = z13.object({
1998
2029
  entity: TagEntity,
1999
- entityId: z10.number().int().positive().describe("The party/opportunity/kase id."),
2000
- tagName: z10.string().min(1).describe(
2030
+ entityId: positiveId.describe("The party/opportunity/kase id."),
2031
+ tagName: z13.string().min(1).describe(
2001
2032
  "Name of the tag to attach. Capsule resolves by name: if a tag with this name already exists in the tenant it is attached to the entity; if not, Capsule creates the tag and attaches it. Names are tenant-global. Capsule matches case-INSENSITIVELY when resolving (so 'VIP' and 'vip' attach the same tag), preserving the canonical casing from whichever variant was created first. To ensure consistent casing in your tag list, call list_tags first and reuse the exact name from there. Idempotent \u2014 re-attaching an already-attached tag is harmless."
2002
2033
  )
2003
2034
  });
@@ -2010,10 +2041,10 @@ async function addTag(input) {
2010
2041
  invalidateByPrefix(TAG_LIST_PATH[entity], "add_tag");
2011
2042
  return result;
2012
2043
  }
2013
- var removeTagByIdSchema = z10.object({
2044
+ var removeTagByIdSchema = z13.object({
2014
2045
  entity: TagEntity,
2015
- entityId: z10.number().int().positive().describe("The party/opportunity/kase id."),
2016
- tagId: z10.number().int().positive().describe(
2046
+ entityId: positiveId.describe("The party/opportunity/kase id."),
2047
+ tagId: positiveId.describe(
2017
2048
  "The tag's id. Read via get_party / get_opportunity / get_project with embed='tags' \u2014 each tag entry in the response has an `id` field. list_tags returns the same ids for the same tags, so either source works; reading via embed first is the safer pattern because it confirms the tag is actually attached to this entity before you try to remove it (otherwise Capsule returns 422 'tag not found to delete'). Removing detaches the tag from this entity only; the tag definition itself persists in the tenant for other entities that share it."
2018
2049
  )
2019
2050
  });
@@ -2041,28 +2072,24 @@ async function removeTagById(input) {
2041
2072
  invalidateByPrefix(TAG_LIST_PATH[entity], "remove_tag_by_id");
2042
2073
  return result;
2043
2074
  }
2044
- var batchAddTagSchema = z10.object({
2045
- items: z10.array(addTagSchema).min(1).max(50).describe(
2046
- "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."
2047
- )
2075
+ var { schema: batchAddTagSchema, handler: batchAddTag } = defineBatch({
2076
+ toolName: "batch_add_tag",
2077
+ itemSchema: addTagSchema,
2078
+ itemDescription: "Array of 1\u201350 add_tag inputs. Useful for mass-tagging \u2014 e.g. 'tag these 20 contacts as RSAC26'. Each item is the same shape as a single add_tag call. The list_tags cache is invalidated for each affected entity type. Capped at 50.",
2079
+ itemHandler: addTag
2048
2080
  });
2049
- async function batchAddTag(input, opts = {}) {
2050
- return batchExecute("batch_add_tag", input.items, (item) => addTag(item), opts);
2051
- }
2052
- var batchRemoveTagByIdSchema = z10.object({
2053
- items: z10.array(removeTagByIdSchema).min(1).max(50).describe(
2054
- "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."
2055
- )
2081
+ var { schema: batchRemoveTagByIdSchema, handler: batchRemoveTagById } = defineBatch({
2082
+ toolName: "batch_remove_tag_by_id",
2083
+ itemSchema: removeTagByIdSchema,
2084
+ itemDescription: "Array of 1\u201350 remove_tag_by_id inputs. Each item is the same shape as a single remove_tag_by_id call. Detaches the tag from each specified entity; the tag definition itself persists in the tenant. Capped at 50.",
2085
+ itemHandler: removeTagById
2056
2086
  });
2057
- async function batchRemoveTagById(input, opts = {}) {
2058
- return batchExecute("batch_remove_tag_by_id", input.items, (item) => removeTagById(item), opts);
2059
- }
2060
2087
 
2061
2088
  // src/tools/users.ts
2062
- import { z as z11 } from "zod";
2063
- var listUsersSchema = z11.object({
2064
- page: z11.number().int().positive().optional(),
2065
- perPage: z11.number().int().min(1).max(100).optional()
2089
+ import { z as z14 } from "zod";
2090
+ var listUsersSchema = z14.object({
2091
+ page: z14.number().int().positive().optional(),
2092
+ perPage: z14.number().int().min(1).max(100).optional()
2066
2093
  });
2067
2094
  async function listUsers(input) {
2068
2095
  const { data, nextPage } = await capsuleGetCached("/users", {
@@ -2071,32 +2098,32 @@ async function listUsers(input) {
2071
2098
  });
2072
2099
  return { ...data, nextPage };
2073
2100
  }
2074
- var getCurrentUserSchema = z11.object({});
2101
+ var getCurrentUserSchema = z14.object({});
2075
2102
  async function getCurrentUser(_input) {
2076
2103
  const { data } = await capsuleGet("/users/current");
2077
2104
  return data;
2078
2105
  }
2079
2106
 
2080
2107
  // src/tools/filters.ts
2081
- import { z as z12 } from "zod";
2082
- var FilterConditionSchema = z12.object({
2083
- field: z12.string().describe(
2108
+ import { z as z15 } from "zod";
2109
+ var FilterConditionSchema = z15.object({
2110
+ field: z15.string().describe(
2084
2111
  "The Capsule filter-side field name (these differ from response field names \u2014 e.g. response.createdAt is filter-side 'addedOn', response.lastContactedAt is filter-side 'lastContactedOn'). Common: 'addedOn' (date created), 'updatedOn' (date last modified), 'lastContactedOn' (parties only), 'name', 'tag', 'owner', 'team', 'type' (parties: person|organisation), 'milestone' (opportunities), 'status' (opp/project: OPEN|CLOSED), 'closedOn' (opp/project), 'expectedCloseOn' (opp/project), 'hasTags', 'hasEmailAddress' (parties), 'isOpen', 'isStale' (opportunities), 'custom:{fieldId}'. Full per-entity list: https://developer.capsulecrm.com/v2/reference/filters"
2085
2112
  ),
2086
- operator: z12.string().describe(
2113
+ operator: z15.string().describe(
2087
2114
  "The filter operator. Common: 'is', 'is not' (use value=null to test for null), 'contains', 'does not contain', 'is greater than', 'is less than', 'is within last' (date fields, value=integer days), 'is more than' (date fields, value=integer days ago), 'starts with', 'ends with'. Operator validity depends on the field's type."
2088
2115
  ),
2089
- value: z12.union([z12.string(), z12.number(), z12.boolean(), z12.null()]).describe(
2116
+ value: z15.union([z15.string(), z15.number(), z15.boolean(), z15.null()]).describe(
2090
2117
  "The value to compare against. For 'is within last' on date fields, pass an integer number of days. For tag filters, pass the tag name (string) or tag id (number). For 'is not' null tests, pass null literally."
2091
2118
  )
2092
2119
  });
2093
- var FilterInputSchema = z12.object({
2094
- conditions: z12.array(FilterConditionSchema).min(1).describe(
2120
+ var FilterInputSchema = z15.object({
2121
+ conditions: z15.array(FilterConditionSchema).min(1).describe(
2095
2122
  "Array of filter conditions. All conditions are ANDed together. To get newest records, use a date condition like {field: 'addedOn', operator: 'is within last', value: 7} and pick the highest-id row from the result (Capsule IDs are monotonic)."
2096
2123
  ),
2097
- embed: z12.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
2098
- page: z12.number().int().positive().optional().default(1),
2099
- perPage: z12.number().int().min(1).max(100).optional().default(25)
2124
+ embed: z15.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
2125
+ page: z15.number().int().positive().optional().default(1),
2126
+ perPage: z15.number().int().min(1).max(100).optional().default(25)
2100
2127
  });
2101
2128
  async function runFilter(entityPath, input) {
2102
2129
  const { data, nextPage } = await capsuleSearch(
@@ -2127,12 +2154,12 @@ async function filterProjects(input) {
2127
2154
  }
2128
2155
 
2129
2156
  // src/tools/metadata.ts
2130
- import { z as z13 } from "zod";
2157
+ import { z as z16 } from "zod";
2131
2158
  var paginationFields3 = {
2132
- page: z13.number().int().positive().optional(),
2133
- perPage: z13.number().int().min(1).max(100).optional().describe("Page size, max 100. Defaults to 100 for reference data.")
2159
+ page: z16.number().int().positive().optional(),
2160
+ perPage: z16.number().int().min(1).max(100).optional().describe("Page size, max 100. Defaults to 100 for reference data.")
2134
2161
  };
2135
- var listTeamsSchema = z13.object({ ...paginationFields3 });
2162
+ var listTeamsSchema = z16.object({ ...paginationFields3 });
2136
2163
  async function listTeams(input) {
2137
2164
  const { data, nextPage } = await capsuleGetCached("/teams", {
2138
2165
  page: input.page ?? 1,
@@ -2140,7 +2167,7 @@ async function listTeams(input) {
2140
2167
  });
2141
2168
  return { ...data, nextPage };
2142
2169
  }
2143
- var listLostReasonsSchema = z13.object({ ...paginationFields3 });
2170
+ var listLostReasonsSchema = z16.object({ ...paginationFields3 });
2144
2171
  async function listLostReasons(input) {
2145
2172
  const { data, nextPage } = await capsuleGetCached("/lostreasons", {
2146
2173
  page: input.page ?? 1,
@@ -2148,7 +2175,7 @@ async function listLostReasons(input) {
2148
2175
  });
2149
2176
  return { ...data, nextPage };
2150
2177
  }
2151
- var listActivityTypesSchema = z13.object({ ...paginationFields3 });
2178
+ var listActivityTypesSchema = z16.object({ ...paginationFields3 });
2152
2179
  async function listActivityTypes(input) {
2153
2180
  const { data, nextPage } = await capsuleGetCached(
2154
2181
  "/activitytypes",
@@ -2159,12 +2186,12 @@ async function listActivityTypes(input) {
2159
2186
  );
2160
2187
  return { ...data, nextPage };
2161
2188
  }
2162
- var getSiteSchema = z13.object({});
2189
+ var getSiteSchema = z16.object({});
2163
2190
  async function getSite(_input) {
2164
2191
  const { data } = await capsuleGetCached("/site");
2165
2192
  return data;
2166
2193
  }
2167
- var listTrackDefinitionsSchema = z13.object({ ...paginationFields3 });
2194
+ var listTrackDefinitionsSchema = z16.object({ ...paginationFields3 });
2168
2195
  async function listTrackDefinitions(input) {
2169
2196
  const { data, nextPage } = await capsuleGetCached(
2170
2197
  "/trackdefinitions",
@@ -2172,7 +2199,7 @@ async function listTrackDefinitions(input) {
2172
2199
  );
2173
2200
  return { ...data, nextPage };
2174
2201
  }
2175
- var listCategoriesSchema = z13.object({ ...paginationFields3 });
2202
+ var listCategoriesSchema = z16.object({ ...paginationFields3 });
2176
2203
  async function listCategories(input) {
2177
2204
  const { data, nextPage } = await capsuleGetCached("/categories", {
2178
2205
  page: input.page ?? 1,
@@ -2180,7 +2207,7 @@ async function listCategories(input) {
2180
2207
  });
2181
2208
  return { ...data, nextPage };
2182
2209
  }
2183
- var listGoalsSchema = z13.object({ ...paginationFields3 });
2210
+ var listGoalsSchema = z16.object({ ...paginationFields3 });
2184
2211
  async function listGoals(input) {
2185
2212
  const { data, nextPage } = await capsuleGetCached("/goals", {
2186
2213
  page: input.page ?? 1,
@@ -2190,14 +2217,14 @@ async function listGoals(input) {
2190
2217
  }
2191
2218
 
2192
2219
  // src/tools/audit.ts
2193
- import { z as z14 } from "zod";
2194
- var listEmployeesSchema = z14.object({
2195
- partyId: z14.number().int().positive().describe(
2220
+ import { z as z17 } from "zod";
2221
+ var listEmployeesSchema = z17.object({
2222
+ partyId: positiveId.describe(
2196
2223
  "The organisation's party id. Returns the people whose `organisation` field links to this party."
2197
2224
  ),
2198
- page: z14.number().int().positive().optional().default(1),
2199
- perPage: z14.number().int().min(1).max(100).optional().default(25),
2200
- embed: z14.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
2225
+ page: z17.number().int().positive().optional().default(1),
2226
+ perPage: z17.number().int().min(1).max(100).optional().default(25),
2227
+ embed: z17.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
2201
2228
  });
2202
2229
  async function listEmployees(input) {
2203
2230
  const { data, nextPage } = await capsuleGet(
@@ -2206,15 +2233,15 @@ async function listEmployees(input) {
2206
2233
  );
2207
2234
  return { ...data, nextPage };
2208
2235
  }
2209
- var DeletedSinceSchema = z14.string().describe(
2236
+ var DeletedSinceSchema = z17.string().describe(
2210
2237
  "REQUIRED. ISO-8601 timestamp; only deletions on or after this point are returned. Example: '2026-01-01T00:00:00Z'."
2211
2238
  );
2212
2239
  var DeletedPagination = {
2213
2240
  since: DeletedSinceSchema,
2214
- page: z14.number().int().positive().optional().default(1),
2215
- perPage: z14.number().int().min(1).max(100).optional().default(25)
2241
+ page: z17.number().int().positive().optional().default(1),
2242
+ perPage: z17.number().int().min(1).max(100).optional().default(25)
2216
2243
  };
2217
- var listDeletedPartiesSchema = z14.object(DeletedPagination);
2244
+ var listDeletedPartiesSchema = z17.object(DeletedPagination);
2218
2245
  async function listDeletedParties(input) {
2219
2246
  const { data, nextPage } = await capsuleGet("/parties/deleted", {
2220
2247
  since: input.since,
@@ -2223,7 +2250,7 @@ async function listDeletedParties(input) {
2223
2250
  });
2224
2251
  return { ...data, nextPage };
2225
2252
  }
2226
- var listDeletedOpportunitiesSchema = z14.object(DeletedPagination);
2253
+ var listDeletedOpportunitiesSchema = z17.object(DeletedPagination);
2227
2254
  async function listDeletedOpportunities(input) {
2228
2255
  const { data, nextPage } = await capsuleGet("/opportunities/deleted", {
2229
2256
  since: input.since,
@@ -2232,7 +2259,7 @@ async function listDeletedOpportunities(input) {
2232
2259
  });
2233
2260
  return { ...data, nextPage };
2234
2261
  }
2235
- var listDeletedProjectsSchema = z14.object(DeletedPagination);
2262
+ var listDeletedProjectsSchema = z17.object(DeletedPagination);
2236
2263
  async function listDeletedProjects(input) {
2237
2264
  const { data, nextPage } = await capsuleGet("/kases/deleted", {
2238
2265
  since: input.since,
@@ -2243,14 +2270,14 @@ async function listDeletedProjects(input) {
2243
2270
  }
2244
2271
 
2245
2272
  // src/tools/relationships.ts
2246
- import { z as z15 } from "zod";
2247
- var RelationshipEntity = z15.enum(["opportunities", "kases"]).describe("Which entity has the additional-party links. Use 'kases' for projects.");
2248
- var listAdditionalPartiesSchema = z15.object({
2273
+ import { z as z18 } from "zod";
2274
+ var RelationshipEntity = z18.enum(["opportunities", "kases"]).describe("Which entity has the additional-party links. Use 'kases' for projects.");
2275
+ var listAdditionalPartiesSchema = z18.object({
2249
2276
  entity: RelationshipEntity,
2250
- entityId: z15.number().int().positive().describe("ID of the opportunity or project."),
2251
- embed: z15.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
2252
- page: z15.number().int().positive().optional().default(1),
2253
- perPage: z15.number().int().min(1).max(100).optional().default(25)
2277
+ entityId: positiveId.describe("ID of the opportunity or project."),
2278
+ embed: z18.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
2279
+ page: z18.number().int().positive().optional().default(1),
2280
+ perPage: z18.number().int().min(1).max(100).optional().default(25)
2254
2281
  });
2255
2282
  async function listAdditionalParties(input) {
2256
2283
  const { data, nextPage } = await capsuleGet(
@@ -2259,10 +2286,12 @@ async function listAdditionalParties(input) {
2259
2286
  );
2260
2287
  return { ...data, nextPage };
2261
2288
  }
2262
- var addAdditionalPartySchema = z15.object({
2289
+ var addAdditionalPartySchema = z18.object({
2263
2290
  entity: RelationshipEntity,
2264
- entityId: z15.number().int().positive(),
2265
- partyId: z15.number().int().positive().describe("ID of the party (person or organisation) to link as an additional party.")
2291
+ entityId: positiveId,
2292
+ partyId: positiveId.describe(
2293
+ "ID of the party (person or organisation) to link as an additional party."
2294
+ )
2266
2295
  });
2267
2296
  async function addAdditionalParty(input) {
2268
2297
  try {
@@ -2290,10 +2319,10 @@ async function addAdditionalParty(input) {
2290
2319
  throw err;
2291
2320
  }
2292
2321
  }
2293
- var removeAdditionalPartySchema = z15.object({
2322
+ var removeAdditionalPartySchema = z18.object({
2294
2323
  entity: RelationshipEntity,
2295
- entityId: z15.number().int().positive(),
2296
- partyId: z15.number().int().positive(),
2324
+ entityId: positiveId,
2325
+ partyId: positiveId,
2297
2326
  confirm: confirmFlag().describe(
2298
2327
  "Must be set to true. Removes the link between the entity and the additional party. The party itself is not deleted. Reversible by re-adding the link."
2299
2328
  )
@@ -2320,11 +2349,11 @@ async function removeAdditionalParty(input) {
2320
2349
  })
2321
2350
  );
2322
2351
  }
2323
- var listAssociatedProjectsSchema = z15.object({
2324
- opportunityId: z15.number().int().positive(),
2325
- embed: z15.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
2326
- page: z15.number().int().positive().optional().default(1),
2327
- perPage: z15.number().int().min(1).max(100).optional().default(25)
2352
+ var listAssociatedProjectsSchema = z18.object({
2353
+ opportunityId: positiveId,
2354
+ embed: z18.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
2355
+ page: z18.number().int().positive().optional().default(1),
2356
+ perPage: z18.number().int().min(1).max(100).optional().default(25)
2328
2357
  });
2329
2358
  async function listAssociatedProjects(input) {
2330
2359
  const { data, nextPage } = await capsuleGet(
@@ -2335,9 +2364,9 @@ async function listAssociatedProjects(input) {
2335
2364
  }
2336
2365
 
2337
2366
  // src/tools/custom-fields.ts
2338
- import { z as z16 } from "zod";
2339
- var CustomFieldEntity = z16.enum(["parties", "opportunities", "kases"]).describe("Which entity type's custom field schema to inspect. Use 'kases' for projects.");
2340
- var listCustomFieldsSchema = z16.object({
2367
+ import { z as z19 } from "zod";
2368
+ var CustomFieldEntity = z19.enum(["parties", "opportunities", "kases"]).describe("Which entity type's custom field schema to inspect. Use 'kases' for projects.");
2369
+ var listCustomFieldsSchema = z19.object({
2341
2370
  entity: CustomFieldEntity
2342
2371
  });
2343
2372
  async function listCustomFields(input) {
@@ -2346,9 +2375,9 @@ async function listCustomFields(input) {
2346
2375
  );
2347
2376
  return data;
2348
2377
  }
2349
- var getCustomFieldSchema = z16.object({
2378
+ var getCustomFieldSchema = z19.object({
2350
2379
  entity: CustomFieldEntity,
2351
- fieldId: z16.number().int().positive().describe("Custom field definition id.")
2380
+ fieldId: positiveId.describe("Custom field definition id.")
2352
2381
  });
2353
2382
  async function getCustomField(input) {
2354
2383
  const { data } = await capsuleGetCached(
@@ -2358,11 +2387,11 @@ async function getCustomField(input) {
2358
2387
  }
2359
2388
 
2360
2389
  // src/tools/tracks.ts
2361
- import { z as z17 } from "zod";
2362
- var TrackEntity = z17.enum(["parties", "opportunities", "kases"]).describe("Use 'kases' for projects.");
2363
- var listEntityTracksSchema = z17.object({
2390
+ import { z as z20 } from "zod";
2391
+ var TrackEntity = z20.enum(["parties", "opportunities", "kases"]).describe("Use 'kases' for projects.");
2392
+ var listEntityTracksSchema = z20.object({
2364
2393
  entity: TrackEntity,
2365
- entityId: z17.number().int().positive()
2394
+ entityId: positiveId
2366
2395
  });
2367
2396
  async function listEntityTracks(input) {
2368
2397
  const { data } = await capsuleGet(
@@ -2370,20 +2399,20 @@ async function listEntityTracks(input) {
2370
2399
  );
2371
2400
  return data;
2372
2401
  }
2373
- var showTrackSchema = z17.object({
2374
- trackId: z17.number().int().positive()
2402
+ var showTrackSchema = z20.object({
2403
+ trackId: positiveId
2375
2404
  });
2376
2405
  async function showTrack(input) {
2377
2406
  const { data } = await capsuleGet(`/tracks/${input.trackId}`);
2378
2407
  return data;
2379
2408
  }
2380
- var applyTrackSchema = z17.object({
2381
- entity: z17.enum(["opportunities", "kases"]).describe("Which entity to apply the track to. Use 'kases' for projects."),
2382
- entityId: z17.number().int().positive(),
2383
- trackDefinitionId: z17.number().int().positive().describe(
2409
+ var applyTrackSchema = z20.object({
2410
+ entity: z20.enum(["opportunities", "kases"]).describe("Which entity to apply the track to. Use 'kases' for projects."),
2411
+ entityId: positiveId,
2412
+ trackDefinitionId: positiveId.describe(
2384
2413
  "The trackDefinition to apply (from list_track_definitions). Auto-creates task definitions on the target entity per the track's rules."
2385
2414
  ),
2386
- startDate: z17.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe(
2415
+ startDate: z20.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe(
2387
2416
  "Optional ISO-8601 date (YYYY-MM-DD) the track should start from \u2014 drives task due-date calculations (each task's `dueOn` is computed as startDate + the track-definition's `daysAfter` offset). Defaults to today if omitted. Useful for scheduling a renewal-queue track against a future contract end-date, or backfilling tracks for historical projects."
2388
2417
  )
2389
2418
  });
@@ -2396,9 +2425,9 @@ async function applyTrack(input) {
2396
2425
  if (input.startDate !== void 0) track["trackDateOn"] = input.startDate;
2397
2426
  return capsulePost("/tracks", { track });
2398
2427
  }
2399
- var updateTrackSchema = z17.object({
2400
- trackId: z17.number().int().positive(),
2401
- fields: z17.record(z17.string(), z17.unknown()).describe(
2428
+ var updateTrackSchema = z20.object({
2429
+ trackId: positiveId,
2430
+ fields: z20.record(z20.string(), z20.unknown()).describe(
2402
2431
  "Object of fields to update on the track. Capsule's PUT semantics are partial \u2014 only the fields you provide are changed. Common: { complete: true } to mark a track completed. Capsule rejects unknown keys; consult Capsule's docs for the full updatable set."
2403
2432
  )
2404
2433
  });
@@ -2410,8 +2439,8 @@ async function updateTrack(input) {
2410
2439
  track: input.fields
2411
2440
  });
2412
2441
  }
2413
- var removeTrackSchema = z17.object({
2414
- trackId: z17.number().int().positive(),
2442
+ var removeTrackSchema = z20.object({
2443
+ trackId: positiveId,
2415
2444
  confirm: confirmFlag().describe(
2416
2445
  "Must be set to true. Removes the track instance from its entity. **Capsule also deletes the auto-tasks the track created when it was applied** \u2014 they go with the track and become unreachable (404 on GET /tasks/{id}, gone from list_tasks on the parent entity). If you need any of those tasks to outlive the track, copy their content into fresh tasks (or use the web UI) before calling remove_track."
2417
2446
  )
@@ -2428,13 +2457,13 @@ async function removeTrack(input) {
2428
2457
  }
2429
2458
 
2430
2459
  // src/tools/attachments.ts
2431
- import { z as z18 } from "zod";
2460
+ import { z as z21 } from "zod";
2432
2461
  var DEFAULT_MAX_SIZE_BYTES = 5 * 1024 * 1024;
2433
2462
  var HARD_MAX_SIZE_BYTES = 25 * 1024 * 1024;
2434
2463
  var HARD_MAX_BASE64_CHARS = Math.ceil(HARD_MAX_SIZE_BYTES / 3) * 4;
2435
- var getAttachmentSchema = z18.object({
2436
- id: z18.number().int().positive().describe("Attachment ID."),
2437
- maxSizeBytes: z18.number().int().positive().max(HARD_MAX_SIZE_BYTES).optional().describe(
2464
+ var getAttachmentSchema = z21.object({
2465
+ id: positiveId.describe("Attachment ID."),
2466
+ maxSizeBytes: z21.number().int().positive().max(HARD_MAX_SIZE_BYTES).optional().describe(
2438
2467
  `Refuse to return content over this size (default ${DEFAULT_MAX_SIZE_BYTES} bytes \u2248 5MB; max ${HARD_MAX_SIZE_BYTES} bytes \u2248 25MB). Files exceeding the cap return metadata only with a 'truncated: true' flag.`
2439
2468
  )
2440
2469
  });
@@ -2449,22 +2478,22 @@ async function getAttachment(input) {
2449
2478
  }
2450
2479
  return { contentType, buffer, sizeBytes };
2451
2480
  }
2452
- var uploadAttachmentSchema = z18.object({
2453
- filename: z18.string().min(1).describe(
2481
+ var uploadAttachmentSchema = z21.object({
2482
+ filename: z21.string().min(1).describe(
2454
2483
  "Filename Capsule should record (e.g. 'contract.pdf'). Capsule does NOT validate consistency between filename, contentType, and the actual bytes \u2014 a typo in either is accepted and the file is stored as labelled."
2455
2484
  ),
2456
- contentType: z18.string().min(1).describe(
2485
+ contentType: z21.string().min(1).describe(
2457
2486
  "MIME type of the file (e.g. 'application/pdf', 'image/png', 'text/plain'). Trusted by Capsule verbatim; not cross-checked against `filename` or the actual bytes."
2458
2487
  ),
2459
- dataBase64: z18.string().min(1).max(HARD_MAX_BASE64_CHARS).describe(
2488
+ dataBase64: z21.string().min(1).max(HARD_MAX_BASE64_CHARS).describe(
2460
2489
  "File contents, base64-encoded. Decoded server-side and uploaded as the request body. Maximum 25 MB per attachment (Capsule's documented limit); the connector rejects oversized base64 before uploading. The inbound HTTP body limit is ~35 MB which leaves room for the base64 expansion of a 25 MB binary."
2461
2490
  ),
2462
- content: z18.string().optional().describe(
2491
+ content: z21.string().optional().describe(
2463
2492
  "Body text for the note that will hold the attachment. Defaults to '[attachment]' if omitted."
2464
2493
  ),
2465
- partyId: z18.number().int().positive().optional().describe("Link the new note to a party (mutually exclusive with opportunityId / projectId)."),
2466
- opportunityId: z18.number().int().positive().optional(),
2467
- projectId: z18.number().int().positive().optional()
2494
+ partyId: positiveId.optional().describe("Link the new note to a party (mutually exclusive with opportunityId / projectId)."),
2495
+ opportunityId: positiveId.optional(),
2496
+ projectId: positiveId.optional()
2468
2497
  });
2469
2498
  function isValidBase64(s) {
2470
2499
  if (!/^[A-Za-z0-9+/]*={0,2}$/.test(s)) return false;
@@ -2514,23 +2543,23 @@ async function uploadAttachment(input) {
2514
2543
  }
2515
2544
 
2516
2545
  // src/tools/saved-filters.ts
2517
- import { z as z19 } from "zod";
2518
- var EntitySchema = z19.enum(["parties", "opportunities", "kases"]).describe(
2546
+ import { z as z22 } from "zod";
2547
+ var EntitySchema = z22.enum(["parties", "opportunities", "kases"]).describe(
2519
2548
  "Which entity type the filter operates over. Use 'kases' for projects (Capsule's legacy name)."
2520
2549
  );
2521
- var listSavedFiltersSchema = z19.object({
2550
+ var listSavedFiltersSchema = z22.object({
2522
2551
  entity: EntitySchema
2523
2552
  });
2524
2553
  async function listSavedFilters(input) {
2525
2554
  const { data } = await capsuleGetCached(`/${input.entity}/filters`);
2526
2555
  return data;
2527
2556
  }
2528
- var runSavedFilterSchema = z19.object({
2557
+ var runSavedFilterSchema = z22.object({
2529
2558
  entity: EntitySchema,
2530
- id: z19.number().int().positive().describe("The saved filter id (from list_saved_filters)."),
2531
- embed: z19.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
2532
- page: z19.number().int().positive().optional().default(1),
2533
- perPage: z19.number().int().min(1).max(100).optional().default(25)
2559
+ id: positiveId.describe("The saved filter id (from list_saved_filters)."),
2560
+ embed: z22.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
2561
+ page: z22.number().int().positive().optional().default(1),
2562
+ perPage: z22.number().int().min(1).max(100).optional().default(25)
2534
2563
  });
2535
2564
  async function runSavedFilter(input) {
2536
2565
  const { data, nextPage } = await capsuleGet(
@@ -2548,7 +2577,7 @@ function createCapsuleMcpServer(opts) {
2548
2577
  const server2 = new McpServer(
2549
2578
  {
2550
2579
  name: "capsulemcp",
2551
- version: "1.6.0",
2580
+ version: "1.6.2",
2552
2581
  description: "Read and (optionally) modify Capsule CRM data \u2014 parties, opportunities, projects, tasks, timeline entries, pipelines, tags.",
2553
2582
  websiteUrl: "https://github.com/soil-dev/capsulemcp",
2554
2583
  icons: ICONS
@@ -3133,7 +3162,7 @@ function createCapsuleMcpServer(opts) {
3133
3162
  registerTool(
3134
3163
  server2,
3135
3164
  "list_teams",
3136
- "List all teams configured in the Capsule account. Useful as input for filter_* queries that scope by team, and for reporting. LIMITATION: returns team identity only (id, name, description, timestamps). Capsule's v2 API does not expose team\u2194user membership through any endpoint \u2014 `GET /teams/{id}/users` 404s, `embed=users` is silently ignored, and `GET /users/{id}` doesn't include a `teams` field. To determine whether a given user belongs to a given team, either check Capsule's web UI Team Membership page, or probe via `update_project { ownerId: U, teamId: T }` and read the response \u2014 422 'owner is not a member of the team' means U \u2209 T.",
3165
+ "List all teams configured in the Capsule account. Useful as input for filter_* queries that scope by team, and for reporting. LIMITATION: returns team identity only (id, name, description, timestamps). Capsule's v2 API does not expose team\u2194user membership through any endpoint \u2014 `GET /teams/{id}/users` 404s, `embed=users` is silently ignored, and `GET /users/{id}` doesn't include a `teams` field. To determine whether a given user belongs to a given team, either check Capsule's web UI Team Membership page, or probe via `update_project { ownerId: U, teamId: T }` / `batch_update_opportunity { items: [{ id: <any opp>, ownerId: U, teamId: T }] }` and read the response \u2014 422 'owner is not a member of the team' means U \u2209 T. Both probe paths apply the same membership constraint server-side.",
3137
3166
  listTeamsSchema,
3138
3167
  listTeams
3139
3168
  );